Witness that an abstract type implements a typeclass
Asked Answered
M

2

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!

Mammet answered 17/10, 2020 at 6:54 Comment(0)
P
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 }
}
Padnag answered 17/10, 2020 at 11:4 Comment(7)
thanks, yes, that's a clever solution that seems to get the best of both worlds.Mammet
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.Mammet
@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.Padnag
@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).Padnag
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.Mammet
@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).Padnag
@Chrisper Multiple constructors of a type class sounds odd.Padnag
C
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.

Continue answered 20/10, 2020 at 10:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.