0

I believe my understanding on this is correct but I'd like to check. When creating typeclasses, it feels neater to have them take a single type parameter, like TypeClass[A]. If the typeclass needs to be parameterized in other ways, abstract types can be used, and there is a comparison of the two approaches here: Abstract types versus type parameters

So far as I have been able to figure out, one thing which is not mentioned in the link is that if using a type parameter, you can witness that the parameter implements a (different) typeclass, likeso:

trait IsValidForTC[A]
    
abstract class TCWithTypeParam[A, B] (implicit ev: IsValidForTC[B]) {} 

If I use an abstract type, I cannot be sure that it implements IsValidForTC:

abstract class TCWithAbstractType[A] (implicit ev: IsValidForTC[B]) {
    type B
} //not found: Type B

If so then this makes sense, but this difference isn't mentioned in the link above so I'd like to check.

Thanks!

Dmytro Mitin
  • 34,874
  • 2
  • 15
  • 47
Chris J Harris
  • 1,049
  • 7
  • 14

2 Answers2

2

It's your choice whether to put implicit constraints on class level or method level. This makes impact on when the implicits are resolved.

In a type-parameter type class with implicit parameter you don't constrain the type of type class (applied to type parameters), i.e. type TCWithTypeParam[A, B] can be used even if there is no implicit IsValidForTC[B] in a scope. What you do constrain is the constructor of type class. You can emulate this behavior for type-member type class in the following way. Make the constructor private and define apply method (or instance as it's called sometimes) in companion object with desired implicit constraint

abstract class TCWithAbstractType[A] private {
  type B
}

object TCWithAbstractType {
  def apply[A, _B: IsValidForTC]: TCWithAbstractType[A] { type B = _B } = 
    new TCWithAbstractType[A] { type B = _B }
}
Dmytro Mitin
  • 34,874
  • 2
  • 15
  • 47
  • thanks, yes, that's a clever solution that seems to get the best of both worlds. – Chris J Harris Oct 17 '20 at 23:47
  • it seems like one potential disadvantage of this method is that when using `TCWithAbstractType`, the compiler can't "know" that `B` is guaranteed to implement `IsValidForTC`. This seems logical because `TCWithAbstractType` might have other constructors, which do not necessarily enforce the constraint that `B` implements `IsValidForTC`. This means that the typeclass witness has to be put at the method level, which is functionally OK but requires a bit more boilerplate. Have I understood the situation correctly? Let me know if I'm not being clear and I will open a new question. – Chris J Harris Oct 20 '20 at 03:46
  • @Chrisper *"when using `TCWithAbstractType`, the compiler can't "know" that `B` is guaranteed to implement `IsValidForTC`"* Depends on using how. Using **type** `TCWithTypeParam[A, B]`/`TCWithAbstractType[A] { type B =... }` isn't constrained, **instantiating** of them is constrained. *"`TCWithAbstractType` might have other constructors"* When you create `apply` method in companion object you also make **all** constructors private. – Dmytro Mitin Oct 20 '20 at 04:17
  • @Chrisper *"has to be put at the method level, which is functionally OK but requires a bit more boilerplate"* Difference between implicits on class level vs. method level is not in less or more boilerplate, semantics of implicit resolution is different in these cases (resolution upon constructor/`apply` method call vs. upon a method call). – Dmytro Mitin Oct 20 '20 at 04:17
  • thanks - I think I understand you but I'll put together a quick example and put it in a new question just to be sure. – Chris J Harris Oct 20 '20 at 04:23
  • @Chrisper Private constructors are still accessible in companion object (and implicit instances of a type class are normally defined in companion object). You can make primary constructor used in `apply` method just `private` and all secondary constructors `private[this]` (then they will not be accessible even in companion object). – Dmytro Mitin Oct 20 '20 at 04:25
  • @Chrisper Multiple constructors of a type class sounds odd. – Dmytro Mitin Oct 20 '20 at 04:27
1

You can add the witness, but it needs to be inside the class scope so it has access to B:

abstract class TCWithAbstractType[A] {
    type B
    implicit val ev: IsValidForTC[B]
}

But in practice this is often less convenient than the type parameter, because it has to be implemented explicitly, something like

new TCWithAbstractType[A] {
    type B = ...
    implicit val ev: IsValidForTC[B] = ...
}

while a constructor parameter just gets the implicit value from outer scope.

Note: this is a partial duplicate of my answer to your follow-up question, but left here in case someone stumbles on this question first.

Alexey Romanov
  • 154,018
  • 31
  • 276
  • 433