Confustion about implicit evidence <:< and and =>

Hi All.

I am a bit confused about use of <:< and => implicit evidence or even with directly. Consider following example

class Foo(val value: Int)
trait Moo {
  def value: Int
  def show(): Unit = println(s"Moo ${value}")
}

def f(x: Foo): Unit = println(x.value)

def g[A <: Foo](x: A)(implicit evidence: A <:< Moo): Unit = {
  x.show()
  println(x.value)
}

def h[A <: Foo](x: A)(implicit evidence: A => Moo): Unit = {
  x.show()
  println(x.value)
}

def k(x: Foo with Moo): Unit = {
  x.show()
  println(x.value)
}

g(new Foo(7) with Moo)
h(new Foo(7) with Moo)
k(new Foo(7) with Moo)

Aren’t all three function signatures g(...), h(...) and k(...) functionally equivalent? Are there are cases where one can see difference from between them?

1 Like

The A => Moo evidence is, as it’s type suggests, an implicit function, meaning that not only types that are a subtype of Moo are allowed, but also types, for which an implicit conversion to Moo is available. So this is a broader restriction than the other two.

Constraining a type parameter with <: or with an implicit <:< are equivalent semantically. The difference is where they can be used. For a standalone function, there isn’t a real difference that would make <:< useful, but it allows you to restrain type parameters, that arent’t part of the method, but of a parent class.

There are examples for this in the standard library, e.g. in collection classes. You can convert a List[A] to a Map, if A is a tuple type. But you wouldn’t want to restrict A to tuples for all lists. So <:< is used, to apply this restriction on A only for usages of the toMap method, even if A is a parameter of the List.

Also see the answers to this SO question for more examples of <:< use cases.

2 Likes

You actually do not need the A <: Foo in neither of them and on its own it would be a another different function.

So, assuming this:

trait Foo {
  def show(): Unit
}

trait Bar {
  def value: Int
}

final case class Baz(value: Int) extends Foo with Bar {
  override final def show(): Unit = {
    println(s"Baz { value : ${value} }")
  }
}

Lets see what we can do:

def f(a: Foo): Foo = {
  a.show()
  a
}

Is basic subtyping polymorphism.
You can call f with any value that is a subtype of Foo, as such you can call anything that Foo defines; because due Liskov it has to have all that.
However, you lose the information of the precise subtype it was at the return, you only know you have a Foo.

def g[A <: Foo](a: A): A = {
  a.show()
  a
}

Is very similar to the previous one.
However, in this case we preserve the information of the precise subtype.
So if I call this method with a Baz I will have a return type of Baz instead of Foo.

def h[A](a: A)(implicit ev: A <:< Foo): A = {
  a.show() // We can call show, because we know that a is a subtype of Foo.
  a
}

Is also very similar to the previous one.
The difference is when we do the subtype check.
With just one type there is usually no difference, but when there is more than one there are cases when <: wont work as we want whereas <:< will do.
Check this for a detailed explanation.

def i[A](a: A)(implicit ev: A => Foo): A = {
  x.show() // We can call show because we know how to convert an A into a Foo.
  x
}

Again, very similar to the previous one.
However more flexible, since we only demand a conversion, not a subtype relationship; note that A <:< Foo implies A => B.
Also, the previous one will only be generated if the compiler can prove the subtyping relationship. Whereas, for this one anyone can provide the conversion (as long as it is in the correct implicit scope).

def j(a: Foo with Bar): Foo = {
  a.show()
  println(a.value)
  a
}

Is very similar to the first one.
Only that here we also demand a to also be a subtype of Bar not only of Foo.
(and the order matters, but that will be fixed in Scala 3).

All of them have valid use cases.
But the most common ones are f, g & h.

2 Likes

I see. So I can do something like this

case class Container[A](x: A) {
  def s()(implicit evidence: A <:< (Int, Int)): Int = x._1 + x._2
}

println(Container((7, 8)).s())

But what if I want to support method s() under the same name for let say when A <: List[Int]? It does not look like I can do this

case class Container[A](x: A) {
  def s()(implicit evidence: A <:< (Int, Int)): Int = x._1 + x._2
  def s()(implicit evidence: A <:< List[Int]): Int = x.fold(0)((x, acc) => acc + x)
}

Since I get compiler error “ambiguous reference to overloaded definition”.

Due type erasure both are the same method.

You can use the DummyImplicit workaround, but if you find your self defining one method with many alternative for different types, then a typeclass would probably be the best alternative.

1 Like