The Intuition for Type Classes
So last time I sort of had that realization that types(!) really shine when they model data. Recently, I was thinking about functional programming (FP) and wondering why certain constructs are the way they are. In this instance I was trying to glean the intuition behind type classes. Not all statically typed FP languages have them: OCaml, F#, and Elm to name a few. Why are type classes a thing, and why are they not everywhere?
I always like to go from something I know and link it to something I am trying to learn. We will start with object oriented (OO) Javascript, move to FP Javascript, and try to move that to a hypothetical statically typed FP language.
OO Javascript
Boiled down, functional programing is the separation of functions and data. This is in contrast with paradigms like OO that mix functions and data together. In our OO example we will make two classes that have a similarly named function:
class Person {
constructor(name) {
this.name = name
} greet = () => `Hello, my name is ${this.name}!`
}class Dog {
constructor(name, isAGoodDog) {
this.name = name
this.isAGoodDog = true
} greet = () => `Woof`
}const person = new Person('Kevin')
const dog = new Dog('Doggo')
person.greet()
dog.greet()
We defined two data types (or classes). They both contain some instance variables (name and isAGoodDog) as well as a method greet. To actually call greet we first have to make a new instance of each data type and then call that data types method. We are coupling the data type along with the function.
If we wanted to, we could write a greet function outside of a class that delegates to the internal greet method. This might be desireable as we don't need to reach into our class in order to make call the function.
const greet = (obj) => {
if (obj.greet && typeof obj.greet === 'function') {
return obj.greet()
}
}greet(new Person('Kevin'))
greet(new Dog('Doggo', true))
Our new generic greet function checks to see if a greet method is available on the object and calls it if so.
FP Javascript
So far we have been in OO Javascript where functions are mixed with data. How would we do this in an FP fashion? To start, we need to separate our functions from our data.
// Javascript
const makePerson = (name) => ({ name })
const makeDog = (name, isAGoodDog) => ({ name, isAGoodDog: true })// Typescript
type Person = { name: string }
type Dog = { name: string, isAGoodDog: boolean }
const makePerson = (name): Person => ({ name })
const makeDog = (name, isAGoodDog): Dog => ({ name, isAGoodDog: true })
Instead of classes we have functions that return plain objects. In our Typescript version we made a type that describes the shape of each data type. We then specify that each function returns a specific type.
// Javascript
const greetPerson = (person) => {
if (!person.name) return
return `Hello, my name is ${person.name}!`
}
const greetDog = (dog) => {
if (!dog.isAGoodDog) return
return `Woof`
}// Typescript
const greetPerson = (person: Person) => `Hello, my name is ${person.name}!`
const greetDog = (dog: Dog) => `Woof`greetPerson(makePerson('Kevin'))
greetDog(makeDog('Doggo'))
All we have left to do is make our unique greet functions for Person and Dog. Each of these functions takes in a plain object and returns a string. The Javascript version needs additional checks to make sure the object is what we expect; Typescript on the other hand enforces that we pass the correct data to each function.
Wait…
“But… The example isn’t complete!” What is the FP equivalent of a generic greet function that works with anything that has an associated greet instance method? In this FP world of separated functions and data, there are no instance methods. What to do?
Well, we could simply just forgo the unified (overloaded) greet function and stick with greetPerson and greetDog. For any new data type we make that would normally have a greet function we would make new functions: greetCat, greetBird, greetEmptiness. This works, however it isn't ideal. We need to always be diligent to use the right function for every unique data type. This is how languages like Ocaml, Elm, and F# work.
Making overloaded functions in these languages is difficult if not impossible. In these languages, often your basic operators (and… everything else) are functions. For example in our hypothetical language + is an infix version for an add function.
// `add` takes in one Int, then another Int, and returns an Int.
// `add` is also pre-fix, meaning it comes before its arguments
add(1, 2)// `+` is just another name for `add`. The difference is it is
// infix, meaning it is placed _between_ its arguments.
1 + 2
So, what is the problem? Well, we may need Double, Float, and more. Do we have uniq + functions for each data type? Yes, unless the language itself has edge cases for the base types. What about other functions like equality (==) and comparisons (<, >), what about user defined types? We need unique functions for everything.
If we want to keep the ability to write highly generic functions and keep our data separate from our functions, we need some glue that can re-connect them. That glue, in our case, is type classes.
Type Classes
Type classes are similar to interfaces in the sense that they provide a specification that your data type needs to implement. Once it is implemented, the generic function can recognize and deal with its registered data type. Let’s see how this works.
// Our made up language
// Make our data types
type Person = { name: string }
type Dog = { name: string, isAGoodDog: boolean }// ... and then make our constructor functions
const makePerson = (name): Person => ({ name })
const makeDog = (name, isAGoodDog): Dog => ({ name, isAGoodDog: true })
We have our types, now let’s make a type class.
typeclass Greetable<a> {
greet(value: a) => string
}Here we are defining a type class called Greetable. The <a> is the data type that will be implementing the type class. We can then reference the a in the definition of our generic greet function that the type class specifies. We see that the function takes in the type and returns a string.
// Define our specific greet functions
const greetPerson = (person: Person) => `Hello, my name is ${this.name}!`
const greetDog = (dog: Dog) => `Woof`// Now we need to tell the Greetable type class that our
// `Person` and `Dog` conform to its protocol
instance Greetable<Person> {
greet = greetPerson
}
instance Greetable<Dog> {
greet = greetDog
}
That <a> we saw before is was filled with Person and Dog respectively. We then tell the type class where to look for each flavor of greet. Now, we can use it as follows.
// Import the `greet` function from wherever we defined the type class
import { greet } from 'greetable'greet(makePerson('Kevin'))
greet(makeDog('Doggo'))
Tying it all together, we pass an instance of Person to the greet function. The greet function sees that it is fed a Person, it then asks the Greetable type class where to go. The type class gives it back the greetPerson function, and then the function is executed.
And there we have it, we emulated the original greet function in our OO version of Javascript in a type safe manner!
To recap how this all works:
- The type class defines the shape of a function and then exports that generic function
- Data types can make a function that adheres to the specification of generic function of the type class
- The data type then become an “instance” of a type class by informing the type class about its own specific function
- Some code in the wild imports the function from the type class and calls it with some arguments
- The type class checks to make sure the arguments of the function are one of the data types it knows about and then executes the function
Conclusion
How did we do with our initial question: “Why are type classes a thing, and why are they not everywhere?”
- Why are type classes a thing: They let us have overloaded functions, and more!
- Why are they not everywhere: They add overhead and complexity to the type system. Much of this can be avoided with a strong module system at the cost of ad hoc polymorphism.
In reality, all of this just scratches the surface, much of which I am still learning.
So what did we really learn? You could have just read the wikipedia page, and this would have been answered in a single sentence.
Type classes […] were originally conceived as a way of implementing overloaded arithmetic and equality operators
❤️
