1

I'm struggling to come up with a good mental model for when is a problem a good fit for the Type Classes pattern?

Recently I was working with a model like

sealed trait FooBar
case class Foo() extends FooBar
case class Bar() extends FooBar

Intuitively I would simply pattern match

def handle(x: FooBar) = x match {
  case f: Foo => println("foo")
  case b: Bar => println("bar")
}

or use subtyping/overloading explicitly like

object Overloading {
  def handle(x: Foo) = println("foo")
  def handle(x: Bar) = println("bar")
}

On the other hand, the type class approach is verbose and I don't see any benefit from using it:

trait FooBarThing[T <: FooBar] {
  def handle(x: T): Unit
}

object TypeClass {

  implicit object HandleFoo extends FooBarThing[Foo] {
    def handle(x: Foo) = println("foo")
  }

  implicit object HandleBar extends FooBarThing[Bar] {
    def handle(x: Bar) = println("bar")
  }

  def process[T <: FooBar](x: T)(implicit ev: FooBarThing[T]): Unit = {
    ev.handle(x)
  }
}

I've found many articles explaining how to write type classes but not much on when?

laurids
  • 931
  • 9
  • 24
  • The thing is, deciding which tool is right for the job is usually a case by case basis thing. The problem you describe is too abstract IMO. What exactly are we handling here? Is it some cross cutting concern like JSON serialization? Or is it just another piece of business logic? – Yuval Itzchakov May 16 '18 at 14:15
  • 1
    Case classes shouldn't have empty argument lists. When pattern matching on case classes, it's typical to extract the argument values instead of matching by type. E.g. `case Foo(x) => // ... Can now use x.` Rather than `case f: Foo => //...`. If you need no arguments, make it a `case object` instead (match is then `case Foo => //...`). See [this article](http://www.casualmiracles.com/2012/05/03/a-small-example-of-the-typeclass-pattern-in-scala/) for some explanation. – Mike Allen May 16 '18 at 14:15
  • 2
    `FooBar` is a sealed trait, there won't be any unknown types `T <: FooBar` to which you would want to attach polymorphic functions in ad-hoc fashion. Creating a typeclass `FooBarThing[T <: FooBar]` is thus meaningless. – Andrey Tyukin May 16 '18 at 14:17

2 Answers2

3

Typeclass pattern provides the possibility to implement ad-hoc polymorphism. That is, if you have some polymorphic function foobar which has to work with many different types T, and then you have some concrete type T1 which does not implement any interface that provides foobar, you can attach foobar to T1 in an ad-hoc fashion as follows:

trait FoobarTypeclass[T] {
  def foobar(t: T): Unit
}

def functionThatRequiresFoobar[T: FoobarTypeclass](t: T): Unit = {
  for (i <- 1 to 10) 
    implicitly[FoobarTypeclass[T]].foobar(t)
}

// note that `functionThatRequiresFoobar` knows nothing about `T1` at this point

class T1
implicit object AdHocFoobarForT1 extends FoobarTypeclass[T1] {
  def foobar(t: T1): Unit  = println("foobar now works on T1, awesome!")
}

functionThatRequiresFoobar(new T1) // but here, it works anyway!

In the above example you can see two things:

  1. Neither FoobarTypeclass nor functionThatRequiresFoobar has to know anything about the existence of the concrete type T1
  2. The type T1 also has to know nothing about FoobarTypeclass or functionThatRequiresFoobar.

That means, T1 and functionThatRequiresFoobar are completely decoupled. But in the last line of the example,

functionThatRequiresFoobar(new T1)

works just nicely anyway, because the AdHocFoobarForT1 typeclass attaches an implementation of foobar to the class T1 in an ad-hoc fashion.

Similarly, you can use this pattern to "implement interfaces in an ad-hoc fashion" on classes that do not declare any relevant interfaces in their inheritance hierarchy. This in turn allows you to glue together completely independent libraries simply by providing a few typeclasses here and there.

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
0

Andrey Tyukin has answered when you could use typeclasses, so I will just add why to prefer pattern matching over overloading when FooBar is a sealed type or you don't need ad-hoc polymorphism.

Generally, overloading creates much hassle for type systems and makes usage of implicits harder. Question on SO discusses further disadvantages of overloading, but among others there are:

  • Hard to lift method to a function
  • Ambiguity in applying the implicit views to arguments of the overloaded function.

I would only use overloading in case of the same functionality provided to unconnected types, to create nicer programmer experience e.g.

object Printer {
    def print(a: Bool): String = ???
    def print(a: Int): String = ???
}

Since you can pattern match over subtypes, I would most certainly use that option.

SzymonPajzert
  • 692
  • 8
  • 18