What is a typeclass and why should you care
Type systems are great because they protect you from lots of errors at compile time. However, sometimes it feels like it gets in your way and can make your code feel less flexible when you are trying to extend certain types that you may or may not have defined.
In dynamically typed languages this is less of a problem because of duck-typing. However, you get no safety here; if you pass in something that doesn’t quack you will get a runtime error.
Duck typing doesn't fix everything though. A human class might be able to quack with a "say" function; you would need to give your program some help in these instances
If you wish to write a generic function that operates on a variety of types, you may be tempted into writing an interface and ensuring all the types you care about implement them.
But this requires changing potentially lots of code and adding in a commonality that only you care about to solve a particular problem. Ask yourself, is this polymorphism truly useful for other people? Or are you polluting the namespace for your convenience (hint: the latter).
The GoF have a solution to this too, the adapter pattern. But every time someone wants to use your library they will have to take their type and wrap them up in an adapter. This feels clunky to me; and of course there is a better way.
Type classes to the rescue!
A type class T is an interface that defines a set of behaviours that a type Foo must implement in order to be of type T. Great, so what?
If you wish to have a polymorphic function, you can require evidence of a type class for the type which is being passed in. This can be any type because a developer can provide this evidence wherever they like; i.e not within the type itself.
This is a great strength of type classes because it addresses the flaws of the other approaches:
- Any arbitrary type can use your library if they implement your typeclass; no need for access to the source code.
- No need to pollute namespace of types only for your needs. Clear separation of concerns and still polymorphic
- No wrappers needed. Scala's implicit means you can pass in your type to the library function without noisy adapters; makes your code easier to follow.
- Type safety.
Example
// Pretend this is your awesome library code that you want others to use freely
object MyAmazingSerialisationLib{
trait Serialiser[A]{
def toJson(x: A): String
}
def writeJsonToDisk[A](item: A)(implicit serialiser: Serialiser[A]){
def writeToDisk(x: String) {println(s"I saved [$x] to disk, honest!")}
writeToDisk(serialiser.toJson(item))
}
}
object TypeClasses extends App {
import MyAmazingSerialisationLib._
case class Dog(name: String)
case class ComputerMachine(id: Int, complexity: Int)
implicit val dogSerialiser = new Serialiser[Dog] {
def toJson(dog: Dog) = s"""{"name":"${dog.name}"}"""
}
// compiles because we have evidence of serialiser
writeJsonToDisk(Dog("Spot"))
// doesn't compile because we dont have a serialiser in scope
writeJsonToDisk(ComputerMachine(2, 20))
}
Scala provides sugar to make it so your function's type can reflect it's needs, rather than having it as a parameter. You can replace the function from above with the code below and it will continue to work.
// [A: Serialiser] means the compiler expects an implicit Serialiser
// needs to be available for A
def writeJsonToDisk[A: Serialiser](item: A){
def writeToDisk(x:String){println(s"I saved [$x] to disk, honest!")}
// Access the parameter with implicity
writeToDisk(implicitly[Serialiser[A]].toJson(item))
}
This syntax can provide more flexibility in terms of not having to pass implicit parameters to other sub-functions; your function just requires "evidence" of a type-class.
However, people in your team may be more familiar with an implicit parameter so be sure to explain things whenever you introduce exotic symbols into your code-base.
Conclusion
- Type classes are easy to use and an interesting functional design pattern for your tool-belt.
- Enables “ad-hoc” polymorphism but with type safety.
- Allows users of your useful functions to use them with any type without having to change their own types.
- Easier to separate concerns without making lots of types/adapters.
- Essential for making your library code re-usable and extensible; a very common approach in the standard Scala libraries and popular open source projects.
Other links
I originally posted this at spiking the solution