Kity Cartman
Kity Cartman

Reputation: 896

How can I bring ad-hoc polymorphism using type classes or implicit classes into parameterized context at runtime?

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.

Implicit Class Implementation

  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.

Type Class Implementation

  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

Answers (2)

Mateusz Kubuszok
Mateusz Kubuszok

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

Iva Kam
Iva Kam

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 :

  1. functional interface declaration (trait)
  2. object which provides func(smth) alike syntax, companion to trait
  3. finite set of implementations - implicit values
  4. (optional) extension which provides smth.func syntax.

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

Related Questions