scalatypeclassimplicitsubtyping

Is it possible to use a typeclass to implement a trait?


I have a situation where I'd like to implement a given trait (CanBeString in the example below). I would like to have the option either to implement that trait using a newly created case class (NewImplementation in the example below), or to implement it by adding functionality to some pre-existing type (just Int in the example below), by using a type class. This is probably best illustrated by the below:

package example

// typeclass
trait ConvertsToString[A] {
  def asString(value: A): String
}

// the trait I would like the typeclass to implement
trait CanBeString {
  def asString: String
}

// this implementation approach taken from the scala with cats book
object ConvertsToStringInstances {
  implicit val intConvertsToString: ConvertsToString[Int] = 
    new ConvertsToString[Int] {
      def asString(value: Int): String = s"${value}"
    }
}

object ConvertsToStringSyntax {
  implicit class ConvertsToStringOps[A](value: A) {
    def asString(implicit c: ConvertsToString[A]): String = c.asString(value)
  }
}

object Test {
  import ConvertsToStringInstances._
  import ConvertsToStringSyntax._

  def testAsFunc(c: CanBeString): String = c.asString

  case class NewImplementation (f: Double) extends CanBeString {
    def asString = s"{f}"
  }

  println(testAsFunc(NewImplementation(1.002))) // this works fine!
  println(testAsFunc(1)) // this sadly does not.
}

Is anything like this possible? I'm only recently discovering the topic of typeclasses so I'm aware that what I'm asking for here may be possible but just unwise - if so please chime in and let me know what a better idiom might be.

Thanks in advance, and also afterwards!


Solution

  • For example you can have two overloaded versions of testAsFunc (OOP-style and typeclass-style)

    object Test {
      ...
    
      def testAsFunc(c: CanBeString): String = c.asString
      def testAsFunc[C: ConvertsToString](c: C): String = c.asString
    
      println(testAsFunc(NewImplementation(1.002))) // {f}
      println(testAsFunc(1)) // 1
    }
    

    Or if you prefer to have the only testAsFunc then you can add instances of the type class for subtypes of the trait to be implemented

    object ConvertsToStringInstances {
      implicit val intConvertsToString: ConvertsToString[Int] = ...
    
      implicit def canBeStringSubtypeConvertsToString[A <: CanBeString]: ConvertsToString[A] =
        new ConvertsToString[A] {
          override def asString(value: A): String = value.asString
        }
    }
    
    object Test {
      ...
    
      def testAsFunc[C: ConvertsToString](c: C): String = c.asString
    
      println(testAsFunc(NewImplementation(1.002))) // {f}
      println(testAsFunc(1)) // 1
    }
    

    Please notice that if for a c there are both OOP-ish c.asString and extension-method c.asString then only the first is actually called.