Easy to use type classes in Scala

Scala is a pretty good functional programming language, but its type class syntax is pretty ugly to use. Thankfully, there is a pattern you can use to simplify the use of your type classes, making them almost as easy to use as regular inheritance based OOP.

Type classes are a very handy concept first pioneered in Haskell, which allows polymorphism like inheritance, but without requiring the foresight of inheritance. By that I mean that type class relationships between types can be defined for any type after that type has been defined, rather than during that type’s definition.

Below, I have the definition of a type class called depth.

trait Depth[P[_]] {
 def depth(self: P[_]): Int
}

This type class can be implemented for any type constructor like so:

object Depth {
 implicit object ListDepth extends Depth[List] {
   def depth(self: List[_]) = x.size
 }
}

The code above basically tells Scala how Depth is implemented for the type constructor List. This is all well and good, but when we go to use our type class…

object Main extends App {
  List(1,2,3).depth //won't compile, list does not have depth defined

  def fn[T[_]: Depth](t: T[_]) = {
    t.depth //Also doesn't work
    implicitly[Depth[T]].depth(t) //yuck
  }
}

things get very messy very quickly. Thankfully, with just a few tweaks, our typeclass can be as easy to use as regular polymorphism!

trait Depth[P[_]] {
  def depth(self: P[_]): Int

  class Ops[T](self: P[T]) {
    def depth = Depth.this.depth(self)
  }
}

object Depth {
  implicit object ListDepth extends Depth[List] {
    def depth(self: List[_]) = x.size
  }

  //This implicit converts anything that has an implementation of Depth into the Depth.Ops class
  implicit def implicits[T, P[T]](x: P[T])(implicit ev: Depth[P]) = new ev.Ops(x)
}

As you can see, we added a new def to our Depth companion object called implicits and a class named Ops to our Depth trait. We simplify the depth function by making it apply itself to the depth object it belongs to. The implicit def called implicits converts any object X that has an implementation of the Depth type class into a Depth#Ops object. This makes our syntax in main easy to use and much nicer looking:

import Depth
import Depth._

object Main extends App {
  List(1,2,3).depth //now this works just like if List inherited from Depth

  def fn[T[_]: Depth](t: T[_]) = {
    t.depth //So does this!
  }
}

Type classes make certain code patterns much easier to deal with. For example, earlier today I had two different datastructures with similar methods I wanted to benchmark against each other. Problem is, they shared no inheritance, so under normal polymorphism I would have to either implement some kind of shared parentage or implement multiple near copies of the same benchmarking code. With typeclasses, there was a much easier third option. Implement a typeclass with the methods signatures I was going to use, and make both types implement that typeclass. Then I just needed to pass in a datastructure to the benchmark function and it would do the rest. Code gets reused and everyone is happy.

What are the downsides to the pattern above and type classes in general? They are:

  • Type classes are terrible for documentation, since any and all implementations of the type class can be defined anywhere in your code.
  • Method signature collisions between type classes implemented by a type can cause weird “method not defined” compiler errors.
  • Generic syntax along with implicits definitions can be finicky, and sometimes it takes work to figure out why your implicit is not triggering.
  • The implicit def that enhances the typeclass implementations must have a unique name. Thanks to type erasure, Scala can’t differentiate these methods solely on type signature. Collisions in the names of these methods will generate “method not defined” compiler errors.

All in all, typeclasses are a very nice and powerful tool for polymorphism in Scala!

edit: As Nullable has brought up, this can be done with implicit classes and view bounds in a more succint way:

trait Depth[T] extends Any {
  def depth: Int
}

object Depth {
  implicit class ListDepth[T](val self: List[T]) extends AnyVal with Depth[List[T]] {
    def depth = self.size
  }
}

object Main extends App {
  List(1,2,3).depth // 3

  def fn[T <% Depth[T]](t: T) = t.depth //compiles
}

However, Depth is no longer useable as a type class, meaning implicitly goes out the window.
—-

This post was inspired by Scala – The Flying Sandwich Parts by eed3si9n

10 Comments

Filed under Scala

10 responses to “Easy to use type classes in Scala

  1. Why not just make ListDepth an implicit AnyVal class instead of an implicit singleton?

  2. Martin Odersky

    I believe your post confuses a lot of people (see discussion on reddit), because you did not show the obvious implementation: Leave Depth as you defined it, but write Main as follows:

    object Main extends App {
    import Depth._
    List(1,2,3).depth

    def fn[T[_]](t: T[_])(implicit d: Depth[T]) = {
    d.depth(t)
    }
    }

    I guess you wanted to avoid the import and jumped through great hoops to do this, but writing the import explicitly is the correct style, IMO.

    • I was more wanting to avoid using implicitly or have a manually defined implicit field. I really dislike them because they are decently verbose and they make Fn’s signature long compared to what I’d do for two classes with a common parent.
      Is this fine?

      object Main extends App {
        import Depth._
        List(1,2,3).depth
      
        def fn[T[_]: Depth](t: T[_]) = t.depth
      }
      

      Also, while i have you on the phone, one of the commenters on my blog suggested using implicit value classes and view bounds instead of typeclasses with context bounds. On the surface, his approach is simpler, but I’m wary because until now I’ve never seen a view bound used.
      Is there a reason they’ve fallen out of favor compared to context bounds?

  3. Johnk873

    I was suggested this blog by my cousin. I’m not sure whether this post is written by him as no one else know such detailed about my difficulty. You are wonderful! Thanks! ceadfdfbddef

Leave a reply to markehammons Cancel reply