Haskell type classes are a tricky concept for many Haskell beginners to learn.
Most languages cannot express them at all, and they don’t have a concept that comes close.
For many object oriented languages, the Interface
is the closest language construct available.
Ruby module
s fill a similar niche.
However, while these concepts both address name overloading and a kind of polymorphism, they miss some of the power that type classes provide.
This post is intended for people curious about type classes. It doesn’t assume any knowledge of Haskell or functional programming. Familiarity with a statically typed language like Java or C# will help.
If you know what a type class is, feel free to skip to the next header.
To recap, a Haskell type class is defined like this:
-- [1] [2] [3]
class (Eq a) => Ord a where
-- [4]
compare :: a -> a -> Ordering
data Ordering = EQ | GT | LT
Ord
, it must be an instance of Eq
.a
in order to make a
an instance of Ord
.We can read the above code snippet as:
Declare a class
Ord
that is parameterized on some typea
wherea
has anEq
type class instance. To make the typea
an instance ofOrd
, you must define thecompare
function. This function takes two values of the typea
and returns a value with the typeOrdering
.
First, we’ll create a toy datatype ToyOrd
.
data ToyOrd = Smol | Large Int
This data type has two constructors: Smol
, which has no fields, and Large
, which has a single Int
field.
In order to make it an instance of Ord
, we have to make it an instance of Eq
:
-- [1] [2] [3]
instance Eq ToyOrd where
-- [4]
Smol == Smol = True
Large x == Large y = x == y
_ == _ = False
instance
keyword.ToyOrd
is the type that we’re making an Eq
instance for.Eq
class defines a function (==) :: a -> a -> Bool
and (/=) :: a -> a -> Bool
. Since (/=)
has a default implementation, we can only implement (==)
.Now that we’ve defined an Eq
instance for our ToyOrd
data type, we can define Ord
.
instance Ord ToyOrd where
compare Smol Smol = -- [1]
EQ
compare Smol (Large _) = -- [2]
LT
compare (Large x) (Large y) = -- [3]
compare x y
compare (Large _) Smol = -- [4]
GT
Smol
values are equal.Smol
value is always less than a Large
.Large
values are compared by their Int
values.Large
is always greater than a Smol
.Once you have a type class, you can write functions which expect an instance of that type class as an input. Let’s define the ordering operators:
(<=) :: Ord a => a -> a -> Bool
a1 <= a2 =
case compare a1 a2 of
LT -> True
EQ -> True
GT -> False
(>) :: Ord a => a -> a -> Bool
a1 > a2 = not (a1 <= a2)
We specify that this function works for all types a
provided that these types are an instance of the Ord
type class.
Java interfaces allow you to specify a set of methods that an object supports, and, as of Java 8, default implementations for these methods.
So we can write an interface that does essentially the same thing as Ord
.
public interface Eq {
default public bool equalTo(Eq a) {
return ! this.notEqualTo(a);
}
default public bool notEqualTo(Eq a) {
return ! this.equalTo(a);
}
}
This is the Eq
interface in Java.
We’re taking advantage of those default implementations.
If we don’t override one of them, then we’ll loop infinitely if we try to call a method.
public interface Ord extends Eq {
public Ordering compare(Ord other);
}
public enum Ordering {
LT, EQ, GT
}
We can also write generic methods in terms of these interfaces.
class OrdUtil {
bool lessThanOrEqual(Ord a1, Ord a2) {
Ordering result = a1.compare(a2);
return result == LT || result == EQ;
}
}
On the surface, these look similar. However, there are a number of important differences!
The type signature for the Haskell compare
function is very specific about the types of it’s arguments.
compare :: Ord a => a -> a -> Ordering
This type signature says:
The caller of this function can pick any type
a
that is an instance ofOrd
. I will return anOrdering
.
Note that the type signature requires that both parameters to compare
have the same type!
It is illegal to write compare Smol 10
.
The Java version allows any two objects to be passed, provided they implement the Ord
interface.
The Java equivalent looks more like:
public class OrdUtil {
static <A extends Ord> bool lessThanOrEqual(A a1, A a2) {
Ordering result = a1.compare(a2);
return result == LT || result == EQ;
}
}
This method signature introduces a generic type variable A
, and says that A
must extend/implement the Ord
interface.
The method then takes two parameters, both of which have the same generic A
type.
Java classes are defined in one place.
Any interface a class implements must be defined on that class.
Java doesn’t handle sum types very well, so we’ll just do Large
from our ToyOrd
class above.
class Large implements Eq, Ord {
public final int size;
public Large(int size) {
this.size = size;
}
public bool equalTo(Eq other) {
if (other instanceof Large) {
Large other1 = (Large) other;
return other1.size == this.size;
}
return false;
}
public Ordering compare(Ord other) {
if (other instanceof Large) {
Large other1 = (Large) other;
if (other1.size < this.size) {
return Ordering.LT;
}
if (other2.size == this.size) {
return Ordering.EQ;
}
return Ordering.GT;
}
throw new RuntimeException("what does this even mean");
}
}
We’ve defined compare
and equalTo
.
Note that we have to do instanceof
and type casting in order to properly implement these methods.
What does it even mean to try and compare two objects of arbitrary type?
Suppose that we’ve imported Large
from some upstream package, and we’ve defined our own interface.
interface SomeOtherPackage {
public bool doSomeThing(int lol);
}
We are completely incapable of making Large
implement our SomeOtherPackage
interface!
Instead, we must wrap the class with a new class that we control, which implements the interface and otherwise delegates to Large
.
public class MyLarge implements SomeOtherPackage {
public final Large large;
public MyLarge(Large large) {
this.large = large;
}
public bool doSomething(int lol) {
System.out.println("wut");
}
}
Type classes separate the definition of data types and the instances of a class.
So, supposing that I imported the ToyOrd
from another package, I can easily do:
import JokesAreFun (ToyOrd(..))
class MyNewClass a where
doSomething :: a -> Int -> IO ()
instance MyNewClass ToyOrd where
doSomething Smol x = putStrLn "hahahaa yess"
doSomething (Large x) y = putStrLn ("numbers! " ++ show (x + y))
Here’s one of the bigger and more amazing things that type classes allow you to do. We call it return type polymorphism. And it’s kind of obscene.
Let’s define a Haskell type class for actions which can fail.
-- [1]
class CanFail failable where
-- [2] [3]
oops :: failable a
-- [4] [5] [6] [7]
pick :: failable a -> failable a -> failable a
-- [8]
win :: a -> failable a
failable
is the type variable name that we’re using for the class.oops
is a value representing a failed computation.a
to the class variable failable
. So failable
must take a generic type parameters.pick
is a function which looks at the two parameters.a
as a parameter.We can easily make an instance for the Maybe
type:
-- [1] [2]
data Maybe a
-- [3]
= Just a
-- [5]
| Nothing
Maybe
is the name of the type we are declaring here.a
.Just
, which takes a single parameter of the generic type a
.Nothing
does not take any type parameters.-- without annotations,
data Maybe a = Just a | Nothing
notAnInt :: Maybe Int
notAnInt = Nothing
hasAnInt :: Maybe Int
hasAnInt = Just 5
Now, let’s write our CanFail
instance!
instance CanFail Maybe where
oops = Nothing
pick (Just a) _ = Just a
pick Nothing (Just a) = Just a
pick Nothing Nothing = oops
win a = Just a
Now, we can write some functions in terms of CanFail
.
We can write a safe division by zero function:
safeDivision :: CanFail failable => Double -> Double -> failable Double
safeDivision x y =
if y == 0
then oops
else win (x / y)
This function signature is doing something very interesting here! Let’s translate it to plain English:
safeDivision
is a function which accepts two arguments of typeDouble
, and returns a value having the typefailable Double
wherefailable
is a generic type variable that the caller may pick, as long as that type has an instance ofCanFail
.
Woah! The caller gets to pick the type? That means I can write code like:
someMathFunction :: Double -> Double -> Double
someMathFunction x y =
let result = safeDivision x y in
case result of
Just number ->
number * 3
Nothing ->
0
As the caller of safeDivision
in this function, I am able to select the Maybe
type.
What if there are other instances?
data MaybeError a = Error String | Result a
instance CanFail MaybeError where
oops = Error "oops"
pick (Result a) _ = Result a
pick _ (Result a) = Result a
pick ohNooo = ohNooo
win a = Result a
Now I can also select the MaybeError
type!
If I want to, I can also make it an instance of IO
:
-- simplified. Runs an action and catches an exception.
try :: IO a -> IO (MaybeError a)
instance CanFail IO where
oops = throwException "oops"
pick first second = do
eResult <- try first
case eResult of
Result a ->
return a
Error exception ->
second
win a = return a
Now, we can use our safeDivision
function in IO
, just like it were print
or similar!
main :: IO ()
main = do
putStrLn "Woah, look at us!" :: IO ()
x <- safeDivision 3 2 :: IO Double
putStrLn "the result was: "
print x
y <- safeDivision 3 0
print "This never happens because we threw an exception!"
Return type polymorphism is super cool, and definitely one of the best things about type classes. It’s also one of the things that really sets it apart from interfaces or modules in other languages.