List(\"a\").contains(5)
Because an Int
can never be contained in a list of String
I think you misunderstand Martin's solution, it is not B <: Eq
, it is B : Eq
, which is a shortcut for
def Contains[B >: A](x: B)(implicit ev: Eq[B])
And Eq[X]
would then contains a method
def areEqual(a: X, b: X): Boolean
This is not the same as moving the equals method of Any a little lower in the hierarchy, which would indeed solve none of the problem of having it in Any.
Why not use an equality typeclass?
scala> val l = List(1,2,3)
l: List[Int] = List(1, 2, 3)
scala> class EQ[A](a1:A) { def ===(a2:A) = a1 == a2 }
defined class EQ
scala> implicit def toEQ[A](a1:A) = new EQ(a1)
toEQ: [A](a1: A)EQ[A]
scala> l exists (1===)
res7: Boolean = true
scala> l exists ("1"===)
<console>:14: error: type mismatch;
found : java.lang.String => Boolean
required: Int => Boolean
l exists ("1"===)
^
scala> List("1","2")
res9: List[java.lang.String] = List(1, 2)
scala> res9 exists (1===)
<console>:14: error: type mismatch;
found : Int => Boolean
required: java.lang.String => Boolean
res9 exists (1===)
The examples use L
instead of List
or SeqLike
, because for this solution to be applied to preexisting contains
method of those collections, it would require a change to the preexisting library code. One of the goals is the best way to do equality, not the best compromise to interopt with the current libraries (although backwards compatibility needs to be considered). Additionally, my other goal is this answer is generally applicable for any method function that wants to selectively disable the implicit subsumption feature of the Scala compiler for any reason, not necessarily tied to the equality semantics.
case class L[+A]( elem: A )
{
def contains[B](x: B)(implicit ev: A <:< B) = elem == x
}
The above generates an error as desired, assuming the desired semantics for List.contains
is the input should be equal to and a supertype of the contained element.
L("a").contains(5)
error: could not find implicit value for parameter ev: <:<[java.lang.String,Int]
L("a").contains(5)
^
The error is not generated when implicit subsumption was not required.
scala> L("a").contains(5 : Any)
defined class L
scala> L("a").contains("")
defined class L
This disables the implicit subsumption (selectively at the method definition site), by requiring the input parameter type B
to be the same as the argument type passed as input (i.e. not implicitly subsumable with A
), and then separately require implicit evidence that B
is a, or has an implicitly subsumable, supertype of A
.]
UPDATE May 03, 2012: The code above is not complete, as is shown below that turning off all subsumption at the method definition-site does not give the desired result.
class Super
defined class Super
class Sub extends Super
defined class Sub
L(new Sub).contains(new Super)
defined class L
L(new Super).contains(new Sub)
error: could not find implicit value for parameter ev: <:<[Super,Sub]
L(new Super).contains(new Sub)
^
The only way to get the desired form of subsumption, is to also cast at the method (call) use-site.
L(new Sub).contains(new Super : Sub)
error: type mismatch;
found : Super
required: Sub
L(new Sub).contains(new Super : Sub)
^
L(new Super).contains(new Sub : Super)
defined class L
Per soc's answer, the current semantics for List.contains
is that the input should be equal to, but not necessarily a supertype of the contained element. This assumes List.contains
promises any matched item only equals and is not required to be a (subtype or) copy of an instance of the input. The current universal equality interface Any.equals : Any => Boolean
is unityped, so equality doesn't enforce a subtyping relationship. If this is the desired semantics for List.contains
, subtyping relationships can't be employed to optimize the compile-time semantics, e.g. disabling implicit subsumption, and we are stuck with the potential semantic inefficiencies that degrade runtime performance for List.contains
.
While I will be studying and thinking more about equality and contains, afaics my answer remains valid for the general purpose of selectively disabling implicit subsumption at the method definition site.
My thought process is also ongoing holistically w.r.t. the best model of equality.
Update: I added a comment below soc's answer, so I now think his point is not relevant. Equality should always be based on a subtyped relationship, which afaics is what Martin Odersky is proposing for the new equality overhaul (see also his version of contains
). Any ad-hoc polymorphic equivalence (e.g. BitInt(1) == 1
) can be handled with implicit conversions. I explained in my comment below didierd's answer that without my improvement below, afaics Martin's proposed contains
would have a semantic error, whereby a mutual implicitly subsumed supertype (other than Any
) will select the wrong implicit instance of Eq
(if one exists, else unnecessary compiler error). My solution disables the implicit subsumption for this method, which is the correct semantics for the subtyped argument of Eq.eq
.
trait Eq[A]
{
def eq(x: A, y: A) = x == y
}
implicit object EqInt extends Eq[Int]
implicit object EqString extends Eq[String]
case class L[+A]( elem: A )
{
def contains[B](x: B)(implicit ev: A <:< B, eq: Eq[B]) = eq.eq(x, elem)
}
L("a").contains("")
Note Eq.eq
can be optionally replaced by the implicit object
(not overridden because there is no virtual inheritance, see below).
Note that as desired, L("a").contains(5 : Any)
no longer compiles, because Any.equals
is no longer used.
We can abbreviate.
case class L[+A]( elem: A )
{
def contains[B : Eq](x: B)(implicit ev: A <:< B) = eq.eq(x, elem)
}
Add: The x == y
must be a virtual inheritance call, i.e. x.==
should be declared override
, because there is no virtual inheritance in the Eq
typeclass. The type parameter A
is invariant (because A
is used in the contravariant position as input parameter of Eq.eg
). Then we can define an implicit object
on an interface (a.k.a. trait
).
Thus, the Any.equals
override must still check if the concrete type of the input matches. That overhead can't be removed by the compiler.
I think I have a legitimate solution to at least some of the problem posted here - I mean, the issue with List("1").contains(1)
:
https://docs.google.com/document/d/1sC42GKY7WvztXzgWPGDqFukZ0smZFmNnQksD_lJzm20/edit
This sounds good in theory, but falls apart in real life in my opinion.
equals
is not based on types and contains
is building on top of that.
That's why code like 1 == BigInt(1)
works and returns the result most people would expect.
In my opinion it doesn't make sense to make contains
more strict than equals
.
If contains
would be made more strict, code like List[BigInt](1,2,3) contains 1
would stop working completely.
I don't think “unsafe” or “not type safe” are the right terms here, by the way.
In my library extension I use:
class TypesafeEquals[A](val a: A) {
def =*=(x: A): Boolean = a == x
def =!=(x: A): Boolean = a != x
}
implicit def any2TypesafeEquals[A](a: A) = new TypesafeEquals(a)
class RichSeq[A](val seq: Seq[A]) {
...
def containsSafely(a: A): Boolean = seq exists (a =*=)
...
}
implicit def seq2RichSeq[A](s: Seq[A]) = new RichSeq(s)
So I avoid calling contains
.