9

In below example, I want to define a contains method that doesn't compile if a and b are not of the same base type.

  • In contains1 impl, if a is Seq[Int] and b is String, T is derived to be Any, and it compiles. This is not I want.
  • In contains2 impl, if a is Seq[Int] and b is String, then it doesn't compile. The behavior is what I want.
def contains1[T](a: Seq[T], b: T): Boolean = a.contains(b)

println(contains1(Seq(1,2,3), "four")) // false

def contains2[T: Ordering](a: Seq[T], b: T): Boolean = a.contains(b)

println(contains2(Seq(1,2,3), "four")) // compilation error
// cmd7.sc:1: No implicit Ordering defined for Any.
// val res7 = isMatched(Seq(1,2,3), "s")
                    ^
// Compilation Failed

However, is there a simpler way to achieve the same behaviour as in contains2? Ordering context bound confuses me as the method has nothing to do with sorting/ordering at all.

Mario Galic
  • 47,285
  • 6
  • 56
  • 98
yiksanchan
  • 1,890
  • 1
  • 13
  • 37
  • 5
    Type inference works using the unification algorithm, and it work really hard to ensure things compile... sometimes, harder than one would want, like this case. One workaround is to define it like this: `def contains[T](a: Seq[T])(b: T): Boolean = a.contains(b)`. In this case, since **b** is in a separate argument list, it is not take into account when inferring the type parameter **T**, in really the already inferred type **T** is used, as such, if `b` is not of the same _type_ as the elements in the collection, it will not compile. - Additionally, one advice, add the `-Xlint:infer-any` flag. – Luis Miguel Mejía Suárez Jun 17 '19 at 18:39
  • As @LuisMiguelMejíaSuárez says, splitting up the arguments is probably the simplest fix. As an alternative, though, you could do something like `def contains2[T: Eq](a: Seq[T], b: T): Boolean = a.exists(Eq[T].eqv(b, _))` with Cats's `Eq`, where the constraint is both meaningful and actually used in the operation. (Unfortunately `Any` has a `scala.math.Equiv` instance, so that's not really useful here.) – Travis Brown Jun 17 '19 at 18:46

1 Answers1

9

You could use generalized type constraints operator =:=.

For example:

def contains[A,B](a: Seq[A], b: B)(implicit evidence: A =:= B): Boolean = a.contains(b)

and then:

println(contains1(Seq(1,2,3), "four")) //fails with Cannot prove that Int =:= String.
println(contains1(Seq("one"), "four")) //returns false
println(contains1(Seq("one", "four"), "four")) //true

More on generalized type constraints here and here.

As LuisMiguelMejíaSuárez noticed, you could also consider using B <:< A instead of A =:= B. I won't elaborate on differences between these two because it's described in linked answer and article, but in brief, <:< would also allow all B that are a subtype of A, while =:= needs types to match exactly.

Krzysztof Atłasik
  • 21,985
  • 6
  • 54
  • 76
  • 2
    If going the **Generalized Type Constraints** way, I would use `implicit ev: B <:< A` which would give more flexibility, while maintaining _type safety_. – Luis Miguel Mejía Suárez Jun 17 '19 at 19:24
  • @LuisMiguelMejíaSuárez Sure. I edited my answer. Thanks for the remark. – Krzysztof Atłasik Jun 17 '19 at 19:36
  • _"all A that are a subtype of B"_, in this case that would not make much sense, since `A` is the type parameter of the collection. It would make more sense as _"all `B` that are a subtype of `A`"_, but then you would need to flip the arguments, as I did in my comment ;) – Luis Miguel Mejía Suárez Jun 17 '19 at 19:42
  • I guess it would make the most sense if you could do something like `A <:< B || B <:< A`, because then you could do `contains(Seq(Seq("one")), List("four"))` and `contains(Seq(List("one")), Seq("four"))`, but I think I can't be done :) – Krzysztof Atłasik Jun 17 '19 at 19:47
  • 1
    I do not know if it can be done, but I do not think it would make more sense. `A <:< B` means `A` could be a subtype of `B`, in this case that means that a collection of a "inferior" type _(in the sense of a scale of concreteness)_ could contain any "superior" value... that could lead to a: Collection of **Ints** could contain a **String**, since `B` could be inferred as **Any**... which is the original problem. On the other hand, `B <:< A` would only mean that a collection of **Pets** can contain a **Dog** _(which by the definition of the Liskov principle, must be true)_. – Luis Miguel Mejía Suárez Jun 17 '19 at 19:51