Reputation: 896
I have a set of ADTs like Person
as mentioned below and a set of implicit class implementations (in my case). It worked all fine until I tried to abstract a common piece code in a method, say, display
in this case. Not sure however, how to achieve this tough. I do understand the following errors but what I would like to know if there is a way to achieve this, if at all possible. Please suggest. TIA.
case class Person(firstName: String, lastName: String)
trait CustomStringifier {
def stringify(): String
}
implicit class PersonCustomStringifier(person: Person) extends CustomStringifier {
def stringify(): String = s"My name is ${person.firstName} ${person.lastName}."
}
def display[T](obj: T): Unit = {
println(obj.stringify())
}
display(Person("John", "Doe"))
This time I rightly get compile time error at line println(obj.stringify)
: Cannot resolve symbol stringify
.
case class Person(firstName: String, lastName: String)
trait CustomStringifier[T] {
def stringify(x: T): String
}
implicit object PersonCustomStringifier extends CustomStringifier[Person] {
def stringify(person: Person): String =
s"My name is ${person.firstName} ${person.lastName}."
}
implicit class Stringifier[T](x: T) {
def stringify(implicit stringifier: CustomStringifier[T]): String =
stringifier.stringify(x)
}
def display[T](obj: T): Unit = {
println(obj.stringify)
}
println(Person("John", "Doe").stringify)
I rightly get compile-time error at line println(obj.stringify)
: NO implicits found for parameter stringifier: CustomStringifier[T]
.
P.S. Let me know if I need to reframe this question to make more sense. Although, I tried to add multiple working pieces of code to indicate what I really need to achieve.
Upvotes: 0
Views: 201
Reputation: 27535
Type classes are resolved at compile so when you see this:
Person("John", "Doe").stringify
compiler produces this:
new Stringifier(Person("John", "Doe")).stringify(PersonCustomStringifier)
because it knows that T=Person
, it needs CustomStringifier[T]
from implicit scope, and that PersonCustomStringifier
is such implicit.
In stringify
method you have:
def stringify(implicit stringifier: CustomStringifier[T]): String =
stringifier.stringify(x)
compiler don't know the type T
BUT it knows that it has some CustomStringifier[T]
that can be found in the implicit scope - because you passed it as an implicit argument. Then the responsibility to learn T
and resolve associated CustomStringfier[T]
is delegated/delayed to the caller (who might delegate it further if it also uses type parameters).
When you write:
def display[T](obj: T): Unit = {
println(obj.stringify)
}
T
is not known. There is no CustomSerializer[T]
in scope, only PersonCustomStringifier
- but we have no proof that T=Person
so we cannot use it. That is why compilation fails.
To fix it you have to take implicit argument, deferring resolution to the caller
def display[T](obj: T)(implicit stringify: CustomStringifier[T]): Unit =
println(obj.stringify)
which could be shortened to
def display[T: CustomStringifier](obj: T): Unit =
println(obj.stringify)
Then when you will call
display(Person("John", "Doe"))
compiler will know that T=Person
and will be able to figure out that PersonCustomStringifier
is the right implicit to pass as implicit argument.
Upvotes: 2
Reputation: 962
First of all, typeclass in a nutshell is a generic function which resolves its implementation from finite number of concrete one via some mechanism (in our case, implicit search mechanism). In scala it consists of 4 components :
So, common usage of implicits looks like this, declaration:
trait YourTypeclass[T] {
def doStuff(t: T): R
}
object YourTypeclass {
def apply[T](t: T)(implicit tInstance: YourTypeclass[T]): R =
tInstance.doStuff(t)
}
implicit YourTypeclassSyntax[T](val t: T) {
def doStuff[T](implicit tInstance: YourTypeclass[T]): R = tInstance.doStuff(t)
}
Implementation:
case class YourClass()
object YourClass {
implicit val yourTypeclassInstance: YourTypeclass[T] = {t => ???: R}
}
Usage:
val x: YourClass = ???
YourTypeclass(x)
x.doStuff
implicitly[YourTypeclass[T]].doStuff(t)
So why it is important to put instance to companion object? Because of implicit search order.Instances defined in companion objects avaliable from every place of project if they are not restricted with private[package]
(Implicit precedence topic on Scala documentation).
You can also control which implicit you want to use by putting implicit instances in trait, then deeper implicit is, then lower priority it will be (usefull for generic ones and overrides, like List[T]
vs List[Int]
).
Second, for your special occasion Show[T]
from cats exists.
Also there's short explanation there, with examples: https://typelevel.org/cats/typeclasses.html
Upvotes: 1