Trait Objects, for better use of type classes

One of the classic disadvantages of using typeclasses in Scala is that it can break down when dealing with multiple values who have different typeclass instances. For example, maybe you would like to create a List of things that can be shown (via Show):

trait Show[-A] {
  def show(a: A): String
}
object Show {
  implicit val stringShow: Show[String] = identity[String]
  implicit val intShow: Show[Int] = _.toString
}

val myShows: List[(things that have a show instance)] = List(1, "hello", 3)

val strings: List[String] = myShows.map(/** instance.show(_) */)

There’s not really a nice way of doing this in Scala out of the box, yet it is how we program using OOP polymorphism all the time, so why not allow this using typeclasses?

I had wondered this a while ago, and forgot about it. But then once I started looking at Rust, I saw that they have this feature, and they call it “trait Objects”

Here’s an example of that and here is the official docs if you’re interested

As for an implementation in Scala one can do something like this:

trait TraitObject[T[_]] {
  type Data
  def data: Data
  def _trait: T[Data]
}

object TraitObject {
  def apply[D, T[_]](_data: D)(implicit __trait: T[D]): TraitObject[T] { type Data = D} =
    new TraitObject[T] {
      type Data = D
      val data: Data = _data
      val _trait: T[Data] = __trait
    }
  object Implicits {
    object Converters {
      implicit class DataToTraitObject[D](data: D) {
        def traitObject[T[_]](implicit _trait: T[D]): TraitObject[T] { type Data = D} = apply(data)
      }
    }
    object Conversions {
      implicit def dataToTraitObject[T[_], D: T](data: D): TraitObject[T] { type Data = D} = apply(data)
      implicit def traitObjectToTypeclass[T[_]](traitObject: TraitObject[T]): T[traitObject.Data] = traitObject._trait
    }
  }
}

and accomplish the same thing pretty easily, and can expose the following syntax

(with implicit conversions)

val myShows: List[TraitObject[Show]] = List(1, "hello", 2, "world")
val strings: List[String] = myShows.map(to => to._trait.show(to.data)))

(or only converters)

    val myShows: List[TraitObject[Show]] = List(
            1.traitObject, 
            "hello".traitObject, 
            2.traitObject, 
            "world".traitObject
        )
    val strings: List[String] = myShows.map(to => to._trait.show(to.data)))

(or no implicits)

    val shows1: List[TraitObject[Show]] = List(TraitObject(1), TraitObject("hello"))
    // some helpful type aliases
    val shows2: List[TObj[Show]] = List(TObj(1), TObj("hello"))

I was also pointed towards this polymorphic library to see prior work (which includes Instance which accomplishes this as well).

Does it seem like a good or bad idea to promote typeclass based polymorphism in this way, maybe either in the std lib or in the platform?

Link to full example source code

Thanks a bunch!

Edit:
For further context, here is the enumeration of existing concepts in other libraries / languages. Will keep this up to date if I find more.

TraitObject[T[_]] from Rust
Instance[T[_]] from Polymorphic
Pack[T[_]] from Haskell

3 Likes

Note that the usage in functional languages seems to be to use an ad-hoc ADT, so that instead of List(1, "hello", 3) you write List(AnInt(1), AString("hello"), AnInt(3)). Now, of course this has two problems:

  • it only works if we have a finite number of cases that we know of beforehand (eg: we’re only going to put integers and strings there);

  • it is very cumbersome having to define ADTs every time such a thing is needed.

It seems to me that Dotty’s union types could be used to alleviate both of these limitations. You would define a Show instance for union types with a Show instance on both sides, and then write:

val myShows: List[Int|String] = List(1, "hello", 2, "world")
val strings: List[String] = myShows.map(_.show)

The implicit resolution does not currently work to enable this in Dotty (we get a diverging search), but perhaps it would be worth making it work. The code is like:

class Show[A](f: A => String) { def apply(a: A) = f(a) }
object Show extends ShowLowPrio {
  implicit object ShowStr extends Show[String](identity)
  implicit object ShowInt extends Show[Int](_.toString)
}
class ShowLowPrio {
  // ClassTag is necessary to allow pat-mat:
  implicit def showOr[A:Show:ClassTag,B:Show:ClassTag]: Show[A|B] = new Show({
      case a: A => implicitly[Show[A]].apply(a)
      case b: B => implicitly[Show[B]].apply(b)
    })
}
object Main {
  def main(args: Array[String]): Unit = {
    // println(implicitly[Show[Int|String]]) // error: diverging implicit search
    val so = Show.showOr[Int,String]
    println(List[Int|String](1,"ok").map(so.apply)) // works: List(1, ok)
  }
}
2 Likes

Hey thanks for the reply! I was also considering Union Types in Dotty. They have clear advantage in that they don’t require boxing or require implicit conversions. However they are restricted by both type erasure, and don’t handle coproducts of the same type ( A | A ) very well. This would be handled with TObj by

import TraitObject.Implicits.Conversions._
val listA = {
  import Instances.intShowA
  List[TObj[Show]](1, 2, 3)
}
val listB = {
  import Instances.intShowB
  List[TObj[Show]](4, 5, 6)
}
val listC = listA ++ listB

Well, this is because A|B is specifically not a coproduct :slight_smile:

If you want different instances of Show[Int] to coexist, I don’t think existentials is the most idiomatic way. Wouldn’t it be simpler to define a new (opaque) type (which can be done without boxing)?

I guess I need to see convincing use cases where the more idiomatic ways would not work.

Maybe something like this rough sketch will illustrate

object WritesExample extends App {

  trait Writes[-T] {
    def write(elem: T, message: String): Unit
    def close(elem: T): Unit
  }

  val textFileWrites: Writes[OutputStream] = new Writes[OutputStream] {
    override def write(elem: OutputStream, message: String): Unit = elem.write(s"$message\n".getBytes)
    override def close(elem: OutputStream): Unit = elem.close()
  }

  val jsonLinesFileWrites: Writes[OutputStream] = new Writes[OutputStream] {
    def asJson(str: String): String = s"""{"msg" : "$str"}"""
    override def write(elem: OutputStream, message: String): Unit = elem.write(s"${asJson(message)}\n".getBytes)
    override def close(elem: OutputStream): Unit = elem.close()
  }

  implicit val listBufferWrites: Writes[ListBuffer[String]] = new Writes[ListBuffer[String]] {
    override def write(elem: ListBuffer[String], message: String): Unit = elem.append(s"$message\n")
    override def close(elem: ListBuffer[String]): Unit = ()
  }

  implicit val printStreamWrites: Writes[PrintStream] = new Writes[PrintStream] {
    override def write(elem: PrintStream, message: String): Unit = elem.append(s"$message\n")
    override def close(elem: PrintStream): Unit = elem.close()
  }


  val inMemoryLog: ListBuffer[String] = ListBuffer[String]()

  val textFile: OutputStream = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))

  val jsonFile: OutputStream = new BufferedOutputStream(new FileOutputStream(new File("out.jsonl")))

  val stdout: PrintStream = System.out

  val stdErr: PrintStream = System.err


  class CompositeLogger(loggers: TObj[Writes]*) {
    def info(message: String): Unit = loggers.foreach(_.withTrait(w => w.write(_, s"[info] $message")))
    def close(): Unit = loggers.foreach(_.withTrait(_.close))
  }


  import TObj.Implicits.Conversions._

  val compositeLogger = new CompositeLogger(
    inMemoryLog,
    // had to add `from` factory, the original implementation's type inference didn't quite work out
    TObj.from(textFile, textFileWrites),
    TObj.from(jsonFile, jsonLinesFileWrites),
    stdout,
    stdErr
  )

  compositeLogger.info("hello")
  compositeLogger.info("world!")
  compositeLogger.close()
}

I’ve also added this code to the gist for a view of full source

EDIT: Also, have added a def withTrait[S](f: T[Data] => Data => S): S = f(_trait)(data) helper method on TObj, since this kind of pattern would be common with these objects:

val string: String = (??? : TObj[Show]).withTrait(_.show)
// equivalent to
val string0: String = { val to: TObj[Show] = ???; to._trait.show(to.data) }

It’s just a rough idea, but shows how easy it is to make these non-intrusive.

1 Like

Could you even go as far as making def withTrait into def apply?

I can’t quickly test right now, but it seems that would allow you to do val string: String = (??? : TObj[Show])(_.show) instead of val string: String = (??? : TObj[Show]).withTrait(_.show)

or

def showables: List[TObj[Show]] = ???
showables.map(to => to(_.show))
1 Like

Yeah good suggestion, I like it.

Interesting coincidence! Over at

https://github.com/lampepfl/dotty/pull/4085

we are playing with new ways to encode type classes for Dotty. But the premise is a little different: I believe that the current way to do type classes is already too cumbersome. So I am l looking for alternatives that hopefully would keep “trait objects” as plain traits. That is, a type class trait should be usable as a trait object without further wrapping.

4 Likes