Typeclass in Swift and Scala


functional programming, scala, swift, typeclass

Typeclass in Swift and Scala


Daniel Shin - December 13, 2015

This post will discuss typeclass pattern in swift and scala and how they differ.

To start off, let’s see what typeclass is.

What is Typeclass

According to wikipedia

In computer science, a type class is a type system construct that supports ad hoc polymorphism.

So it seems to be related to this ad-hoc polymorphism. Before discussing what that is, let’s take a detour to what polymorphism is.

In programming language and type theory, polymorphism is the provision of a single interface to entities of different types. A polymorphic type is one whose operations can also be applied to values of some other type, or types.

So polymorphism is a concept of making different types to conform a single interface.

There are three types of polymorphism; the first two probably more familiar than the last.

  • Parameteric Polymorphism
  • Subtype Polymorphism
  • Ad-hoc Polymorphism

Parameteric polymorphism is what’s commonly called generics. We enable polymorphic function/class etc through type parameter.

Subtype Polymorphism is classic object-oriented polymorphism where one class (OOP concept) subclassing another class.

Ad-hoc Polymorphism is more of functional programming oriented polymorphism where a type doesn’t need to declare its relation to another type in its creation. It can be added elsewhere ad-hoc without the type knowing.

Although parameteric polymorphism is quite universal in both OOP and FP languages, subtype polymorphism and ad-hoc polymorphism compare quite nicely in that subtype polymorphism is an answer from OOP languages to deal with hierarchical relationship between types while ad-hoc polymorphism is an answer from FP languages.

Such comparison will become clearer as we look into the typeclass pattern that implements ad-hoc polymorphism (I’m assuming most people are familiar with class inheritance pattern that implements subtype polymorphism in OOP).

Typeclass

As alluded above, typeclass pattern is a way of making one type conform to an interface in ad-hoc way (it doesn’t need to be done upfront at type declaration unlike typical class inheritance). This makes our types more flexible and composable. While subtype polymorphism achieves the goal of sharing code through upfront class inheritance, typeclass achives the same goal through composition.

Inevitably to support typeclass pattern, one language needs to support finding the link between given type A and the block of code that makes it as an instance of typeclass B.

In general, there are two ways that most languages support that searching; first, more popular, through global namespace, and another through local namespace. The first approach, global namespacing, is supported by many languages including Haskell (the language from which typeclass pattern originated), Swift etc. The second approach, local namespacing, is supported by very few languages (AFAIK) which includes Scala.

What I mean by namespacing is the way instance of a given typeclass is declared. Global namespacing make available a type A as an instance of typeclass B globally (which means you cannot override them and can only be declared once) while Local namespacing make available a type A as an instance of typeclass B locally (which means you can override them since their declarations are local by selectively importing different instances for the same typeclass).

This is an important distinction to note when looking at the way typeclass pattern is done in scala and swift.

Let’s first look at scala’s approach to typeclass pattern.

Typeclass in Scala

As noted, scala supports typeclass instances to be locally defined. This is possible thanks to scala’s implicit.

There are generally two ways of using implicit; implicit parameters and implicit conversions. We will only look at implicit parameters since this is what enables typeclass pattern.

In short, implicit parameter forces compiler to search for an implicit value declared in local scope if the given parameter was not passed.

For example,

def getInt(int: Int) = int
getInt // error - missing parameter

This block of code will obviously fail to compile since we failed to pass value of type Int to function getInt(int: Int). But scala allows us to do this.

def getInt(implicit int: Int) = int
implicit val intValue: Int = 1
getInt // 1

When compiler encounters a missing implicit parameter, it searches for its local scope (technically, it searches other scopes as defined by scala’s implicit search rule most notably, the companion object of given implicit type). and look for an implicit value with the same type as given implicit parameter.

Now let’s look at how we can implement typeclass pattern with implicit paramter.

trait Addable[A] {
  def empty: A
  def add(x: A, y: A): A
  def times(x: A, n: Int): A = {
    def go(xs: A, n: Int): A =
      n match {
        case 0 => empty
        case 1 => xs
        case _ => go(add(xs, x), n-1)
      }
    go(x, n)
  }
}

This is our typeclass Addable and types need to implement both empty and add operations to become an instance our typeclass. Note how the conformance is not achieved through is relationship (types don’t need to be Addable) but rather has relationship (types only need to have certain methods to be an instance).

And by becoming an instance of typeclass Addable, any type will also get another method times for free.

object Addable {
  implicit val addableInt = new Addable[Int] {
    val empty = 0
    def add(x: Int, y: Int) = x + y
  }
  implicit val addableString = new Addable[String] {
    val empty = ""
    def add(x: String, y: String) = x + y
  }
}

And here are our two typeclass instances - addableInt and addableString. The convention in scala for declaring typeclass instances is to have them in given typeclass’s companion object (scala compiler looks for implicit values in this companion object as a last resort - which means if you have another implicit value of same type in local scope, that will be used)

Note how we could make types that we don’t have control over (Int and String as they are defined in std) an instance of our custom typeclass. This is exactly what ad-hoc polymorphism means and where the power of typeclass pattern lies. What’s even more powerful in scala unlike many other languages is that these typeclass instances are nothing more than a normal value declared implicitly, which means they follow the scoping rules and we can override them as we like.

def timesByFour[A : Addable](a: A) = implicitly[Addable[A]].times(a, 4)

This is how we use typeclass and there are some syntax noises. Let’s tackle them one at a time.

: is called context bound and is used as syntactic sugar for (implicit a: Addable[A]). Without context bound, our function will look like this:

def timesByFour[A](a: A)(implicit addableA: Addable[A]) = addableA.times(a, 4)

implicitly[Addable[A]] is used when we use context bound and we need to reference the instance. It simply lets us to reference the instance directly as in addableA.

Typeclass pattern in scala is a little more verbose than others since, remember, the instances of typeclass are nothing more than a normal value declared implicitly and since they are not globally defined, these instances must be passed to the function either explicitly or implicitly.

This is a trade-off between typeclass patterns implemented by global namespacing and local namespacing. While you gain more flexibility and power, it adds more complexity to our code.

Now, we can use any value that has an instance of typeclass Addable which is passed to our new function implicitly.

timesByFour(5) // 20
timesByFour("hello") // "hellohellohellohello"

This is great. We added a new functionality to pre-defined types Int and String without them even knowing.

So that’s how we use typeclass in Scala. Now let’s look at Swift.

Typeclass in Swift

As mentioned before, typeclass pattern in swift is implemented the same way as Haskell; global namespacing.

This means we can have at most one instance for any type to a given typeclass. While this does impose some limitations, it also cleans up some syntactic noises that we’ve experienced with scala.

Typeclass pattern in swift is achieved through our shiny new protocol extension introduced since swift 2.

protocol Addable {
  typealias A
  static var empty: A { get }
  static func add(x: A, _ y: A) -> A
}

extension Addable {
  static func times(x: A, _ n: Int) -> A {
    func go(xs: A, _ n: Int) -> A {
      switch n {
        case 0: return empty
        case 1: return xs
        case _: return go(add(xs, x), n-1)
      }
      return go(x, n)
    }
  }
}

Setting aside minor syntactic noises, both typeclass declarations are identical. Swift version uses protocol extension to derive times method from empty and add and now any instance which conforms to Addable typeclass will get that method for free! (this was not possible in swift 1.X).

extension Int: Addable {
  static var empty: Int { return 0 }
  static func add(x: Int, y: Int) -> Int { return x + y }
}

extension String: Addable {
  static var empty: String { return "" }
  static func add(x: String, y: String) -> String { return x + y }
}

Swift has done a great job with extension and protocol as shown above by their conciseness and readability.

Now we can use them like this:

func timesByFour<A: Addable where A.A == A>(a: A) -> A { return A.times(a, 4) }

Note how we needed to constrain Addable.A is equal to Addable typeclass itself since the in swift, the type itself becomes the instance of typeclass unlike scala where we explicitly create an instance.

timesByFour(5) // 20
timesByFour("hello") // "hellohellohellohello"

Conclusion

Swift’s way of typeclass pattern is definitely simpler than scala’s. However, by doing so, it takes away some freedom and flexibility since you can extension a given type only once and it’s global. You cannot override them in anyway, which isn’t true in scala.

Such overriding is mostly useful when you use third-party library’s typeclass where the library exposes a set of instances for a given typeclass. With local namespacing, we can selectively import to derive behaviors from different instances. Or we can even create our own instance for the typeclass and use it freely.

But nevertheless, the fact that the two languages are some of the fewer mainstream languages that have support for typeclass pattern do make them very nice to work with.