Context Bound on a Generic Class Using Implicits

299 views Asked by At

I am learning Scala in order to use it for a project.

One thing I want to get a deeper understanding of is the type system, as it is something I have never used before in my other projects.

Suppose I have set up the following code:

// priority implicits
sealed trait Stringifier[T] {
  def stringify(lst: List[T]): String
}

trait Int_Stringifier {
  implicit object IntStringifier extends  Stringifier[Int] {
    def stringify(lst: List[Int]): String = lst.toString()
  }
}

object Double_Stringifier extends Int_Stringifier {
  implicit object DoubleStringifier extends Stringifier[Double] {
    def stringify(lst: List[Double]): String = lst.toString()
  }
}

import Double_Stringifier._

object Example extends App {

  trait Animal[T0] {
    def incrementAge(): Animal[T0]
  }

  case class Food[T0: Stringifier]() {
    def getCalories  = 100
  }

  case class Dog[T0: Stringifier]
  (age: Int = 0, food: Food[T0] = Food()) extends Animal[String] {
    def incrementAge(): Dog[T0] = this.copy(age = age + 1)
  }
}

So in the example, there is a type error:

ambiguous implicit values:
[error]  both object DoubleStringifier in object Double_Stringifier of type Double_Stringifier.DoubleStringifier.type
[error]  and value evidence$2 of type Stringifier[T0]
[error]  match expected type Stringifier[T0]
[error]   (age: Int = 0, food: Food[T0] = Food()) extends Animal[String] 

Ok fair enough. But if I remove the context bound, this code compiles. I.e. if I change the code for '''Dog''' to:

case class Dog[T0]
  (age: Int = 0, food: Food[T0] = Food()) extends Animal[String] {
    def incrementAge(): Dog[T0] = this.copy(age = age + 1)
  }

Now I assumed that this would also not compile, because this type is more generic, so more ambiguous, but it does.

What is going on here? I understand that when I put the context bound, the compiler doesn't know whether it is a double or an int. But why then would an even more generic type compile? Surely if there is no context bound, I could potentially have a Dog[String] etc, which should also confuse the compiler.

From this answer: "A context bound describes an implicit value, instead of view bound's implicit conversion. It is used to declare that for some type A, there is an implicit value of type B[A] available"

2

There are 2 answers

4
Dmytro Mitin On

Now I assumed that this would also not compile, because this type is more generic, so more ambiguous, but it does.

The ambiguity was between implicits. Both

Double_Stringifier.DoubleStringifier

and anonymous evidence of Dog[T0: Stringifier] (because class Dog[T0: Stringifier](...) is desugared to class Dog[T0](...)(implicit ev: Stringifier[T0])) were the candidates.

(Int_Stringifier#IntStringifier was irrelevant because it has lower priority).

Now you removed the context bound and only one candidate for implicit parameter in Food() remains, so there's no ambiguity. I can't see how the type being more generic is relevant. More generic doesn't mean more ambiguous. Either you have ambiguity between implicits or not.


Actually if you remove import but keep context bound the anonymous evidence is not seen in default values. So it counts for ambiguity but doesn't count when is alone :)

Scala 2.13.2, 2.13.3.

4
jwvh On

It seems to me (and if I'm wrong I'm hoping @DmytroMitin will correct me), the key to understanding this is with the default value supplied for the food parameter, which makes class Dog both a definition site, requiring an implicit be available at the call site, as well as a call site, requiring an implicit must be in scope at compile time.

The import earlier in the code supplies the implicit required for the Food() call site, but the Dog constructor requires an implicit, placed in ev, from its call site. Thus the ambiguity.