scalaimplicittype-members

Context bounds for type members or how to defer implicit resolution until member instantiation


In the following example, is there a way to avoid that implicit resolution picks the defaultInstance and uses the intInstance instead? More background after the code:

// the following part is an external fixed API

trait TypeCls[A] {
  def foo: String
}

object TypeCls {
  def foo[A](implicit x: TypeCls[A]) = x.foo

  implicit def defaultInstance[A]: TypeCls[A] = new TypeCls[A] {
    def foo = "default"
  }

  implicit val intInstance: TypeCls[Int] = new TypeCls[Int] {
    def foo = "integer"
  }
}

trait FooM {
  type A
  def foo: String = implicitly[TypeCls[A]].foo
}

// end of external fixed API

class FooP[A:TypeCls] { // with type params, we can use context bound
  def foo: String = implicitly[TypeCls[A]].foo
}

class MyFooP extends FooP[Int]

class MyFooM extends FooM { type A = Int }

object Main extends App {

  println(s"With type parameter: ${(new MyFooP).foo}")
  println(s"With type member:    ${(new MyFooM).foo}")
}

Actual output:

With type parameter: integer
With type member:    default

Desired output:

With type parameter: integer
With type member:    integer

I am working with a third-party library that uses the above scheme to provide "default" instances for the type class TypeCls. I think the above code is a minimal example that demonstrates my problem.

Users are supposed to mix in the FooM trait and instantiate the abstract type member A. The problem is that due to the defaultInstance the call of (new MyFooM).foo does not resolve the specialized intInstance and instead commits to defaultInstance which is not what I want.

I added an alternative version using type parameters, called FooP (P = Parameter, M = Member) which avoids to resolve the defaultInstance by using a context bound on the type parameter.

Is there an equivalent way to do this with type members?

EDIT: I have an error in my simplification, actually the foo is not a def but a val, so it is not possible to add an implicit parameter. So no of the current answers are applicable.

trait FooM {
  type A
  val foo: String = implicitly[TypeCls[A]].foo
}

// end of external fixed API

class FooP[A:TypeCls] { // with type params, we can use context bound
  val foo: String = implicitly[TypeCls[A]].foo
}

Solution

  • The simplest solution in this specific case is have foo itself require an implicit instance of TypeCls[A]. The only downside is that it will be passed on every call to foo as opposed to just when instantiating FooM. So you'll have to make sure they are in scope on every call to foo. Though as long as the TypeCls instances are in the companion object, you won't have anything special to do.

    trait FooM {
      type A
      def foo(implicit e: TypeCls[A]): String = e.foo
    }
    

    UPDATE: In my above answer I managed to miss the fact that FooM cannot be modified. In addition the latest edit to the question mentions that FooM.foo is actually a val and not a def.

    Well the bad news is that the API you're using is simply broken. There is no way FooM.foo wille ever return anything useful (it will always resolve TypeCls[A] to TypeCls.defaultInstance regardless of the actual value of A). The only way out is to override foo in a derived class where the actual value of A is known, in order to be able to use the proper instance of TypeCls. Fortunately, this idea can be combined with your original workaround of using a class with a context bound (FooP in your case):

    class FooMEx[T:TypeCls] extends FooM {
      type A = T
      override val foo: String = implicitly[TypeCls[A]].foo
    }
    

    Now instead of having your classes extend FooM directly, have them extend FooMEx:

    class MyFoo extends FooMEx[Int]
    

    The only difference between FooMEx and your original FooP class is that FooMEx does extend FooM, so MyFoo is a proper instance of FooM and can thus be used with the fixed API.