0

Following on from Witness that an abstract type implements a typeclass I've tried to compare these two approaches side-by-side in the code snippet below:

// We want both ParamaterizedTC and WithAbstractTC (below) to check that 
// their B parameter implements AddQuotes 
abstract class AddQuotes[A] {
  def inQuotes(self: A): String = s"${self.toString}"  
}
implicit val intAddQuotes = new AddQuotes[Int] {}

abstract class ParamaterizedTC[A, _B](implicit ev: AddQuotes[_B]) {
  type B = _B
  def getB(self: A): B 
  def add1ToB(self: A): String = ev.inQuotes(getB(self)) // TC witness does not need to be at method level
}

abstract class WithAbstractTC[A] private { 
  // at this point the compiler has not established that type B implements AddQuotes, even if we have created
  // this instance via the apply[A, _B] constructor below...
  type B 
  def getB(self: A): B
  def add1ToB(self: A)(implicit ev: AddQuotes[B]): String = 
    ev.inQuotes(getB(self)) // ... so here the typeclass witness has to occur on the method level
}
object WithAbstractTC {
  // This constructor checks that B implements AddQuotes
  def apply[A, _B: AddQuotes](getB: A => _B): WithAbstractTC[A] = new WithAbstractTC[A] { 
    type B = _B 
    def getB(self: A): B = getB(self)
  }
  // But we could also have a constructor that does not check, so the compiler can never be certain that 
  // for a given instance of WithAbstractTC, type B implements AddQuotes
  def otherConstructor[A, _B](getB: A => _B): WithAbstractTC[A] { type B = _B } = new WithAbstractTC[A] { 
    type B = _B 
    def getB(self: A): B = getB(self)
  }
}

case class Container[B: AddQuotes]( get: B )

// These are both fine
implicit def containerIsParamaterized[B: AddQuotes]: ParamaterizedTC[Container[B], B] = 
  new ParamaterizedTC[Container[B], B] { def getB(self: Container[B]): B = self.get }
implicit def containerIsWithAbstract[_B: AddQuotes]: WithAbstractTC[Container[_B]] = 
  WithAbstractTC[Container[_B], _B](self => self.get)

val contIsParamaterized: ParamaterizedTC[Container[Int], Int] = 
  implicitly[ParamaterizedTC[Container[Int], Int]]
val contIsWithAbstract: WithAbstractTC[Container[Int]] = 
  implicitly[WithAbstractTC[Container[Int]]]

implicitly[AddQuotes[contIsParamaterized.B]]
implicitly[AddQuotes[contIsWithAbstract.B]] // This is not fine

My conclusion (please correct me if I'm wrong) is that if the typeclass witness exists in the public constructor (as in ParamaterizedTC below) then the compiler can always be certain that B implements AddQuotes. Whereas if this witness is put in a constructor in the typeclass companion object (like for WithAbstractTC) then it cannot. This somewhat changes the usage of a type-parameter-based approach versus the abstract-type-based approach.

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
Chris J Harris
  • 1,597
  • 2
  • 14
  • 26

2 Answers2

2

The difference is rather: in ParametrizedTC you have the implicit in scope of the class, in WithAbstractTC you don't. But nothing stops you from adding it when you have an abstract type:

abstract class WithAbstractTC2[A] private { 
  type B 
  implicit val ev: AddQuotes[B]
  def getB(self: A): B
  def add1ToB(self: A): String = 
    ev.inQuotes(getB(self))
}

def apply[A, _B](getB: A => _B)(implicit _ev: AddQuotes[_B]): WithAbstractTC2[A] = new WithAbstractTC2[A] { 
  type B = _B
  implicit val ev: AddQuotes[B] = _ev
  def getB(self: A): B = getB(self)
}

What unfortunately won't work is something like

def apply[A, _B: AddQuotes](getB: A => _B): WithAbstractTC2[A] = new WithAbstractTC2[A] { 
  type B = _B
  implicit val ev: AddQuotes[B] = implicitly[AddQuotes[_B]]
  def getB(self: A): B = getB(self)
}

because it'll pick the implicit in closest scope: the one it's trying to define.

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487
1

implicitly[AddQuotes[contIsWithAbstract.B]] refusing to compile is not connected with single/multiple constructors/apply methods or type parameter/type member difference. You just lost type refinements everywhere. Compiler can't check that you lost type refinements. You have the right to upcast a type discarding its refinement.

If you restore type refinements the code compiles

object WithAbstractTC {
  def apply[A, _B: AddQuotes](getB: A => _B): WithAbstractTC[A] {type B = _B} = 
//                                                              ^^^^^^^^^^^^^
    new WithAbstractTC[A] {
      type B = _B
      def getB(self: A): B = getB(self)
    }
  ...
}

implicit def containerIsWithAbstract[_B: AddQuotes]: 
  WithAbstractTC[Container[_B]] { type B = _B } =
//                              ^^^^^^^^^^^^^^^
  WithAbstractTC[Container[_B], _B](self => self.get)

val contIsWithAbstract: WithAbstractTC[Container[Int]] { type B = Int } =
//                                                     ^^^^^^^^^^^^^^^^
  shapeless.the[WithAbstractTC[Container[Int]]]
//^^^^^^^^^^^^^

implicitly[AddQuotes[contIsWithAbstract.B]] // compiles

Please notice that implicitly looses type refinements, shapeless.the is safe version.

When implicitly isn't specific enough https://typelevel.org/blog/2014/01/18/implicitly_existential.html

How to use class-level implicit constraint for type-member type class via abstract implicit see @AlexeyRomanov's answer.

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • this actually raises a question I had had in the back of my head for a while - is the return type type annotation in, say, `implicit def containerIsWithAbstract` necessary? Or can the compiler infer this? It seems from your answer that if a type annotation appears without type refinements, then the compiler considers the resulting type to be upcast, so in order to avoid repeating the type refinements (or accidentally up-casting by not repeating them in full), you could remove them and let the compiler infer types instead. – Chris J Harris Oct 21 '20 at 02:29
  • Also - thanks for the pointer on `shapeless.the` vs `implicitly`. I have just finished reading the shapeless for type astronauts guide, which gave a lot of food for thought, and am trying to introduce the library into my code. – Chris J Harris Oct 21 '20 at 02:34
  • @Chrisper Well, you can omit return type of implicits and rely on a type inferred (moreover, sometimes it's the only option because in Scala [there are](https://contributors.scala-lang.org/t/compiler-infers-a-type-that-has-no-syntax/4164) types not expressible in source code). But best practice is to specify types of implicits. Sometimes types of implicits are inferred not as desired (and it's better if you control types of implicits), sometimes they are inferred later than necessary during compilation (and you can have weird compile errors). – Dmytro Mitin Oct 21 '20 at 08:54
  • @Chrisper In Scala 3 implicits without type specified will be forbidden. You can introduce type `Aux` and then return type can be written more compactly: `WithAbstractTC.Aux[A, B]`. – Dmytro Mitin Oct 21 '20 at 08:54