Types vs. datatypes vs. typeclasses in Haskell


When I joined The Recurse Center, I planned to work on all sorts of different things, but the world of functional programming wasn’t on my radar. Despite that, within a week of starting at RC someone introduced me to Haskell, and since then I’ve spent most of my time there learning it. My main learning resource is Haskell Programming from First Principles (HPFP), which is easily the most engaging technical book I’ve read. I’m still working through it, and so far it’s very thorough without being boring or confusing.

There are three consecutive chapters in HPFP named Datatypes, Types, and Typeclasses. Those all sound very similar―in this article, I attempt to explain the differences between them.them.

Types vs. Typeclasses

On to the main event…types versus typeclasses.

Types

To define a new type in Haskell:

data TypeName = Definition

This construct is referred to as a type constructor. Once written, it allows you to define values of type TypeName.

The Definition part of the type constructor consists of one or more data constructors. For instance, in this example the data constructor is You:

data Reader = You

The Reader type only has one possible value, and it’s You.

Type constructors can take arguments (note that FName is the data constructor here):

data FirstName s = FName s

These arguments can be restricted to specific types or typeclasses, but more on that in the Typeclasses section.

Two common kinds of Definition\s are sum and product types. The value of a sum type can be one of a number of values, and the value of a product type is a combination of multiple values.

-- ThisOrThat is a sum type, because it can be one of a list of values
-- This and That are both data constructors
data ThisOrThat = This | That

-- FullName is a product type, because it's composed of multiple values
data FullName a a = FirstLastName a a

Types in Haskell do the same thing as types doing in common typed languages―plus much more. They constrain the values a variable can have. But the difference between types in Haskell and in (most) typed imperative languages is that in Haskell, types can be as polymorphic or as concrete as you want. If you want a function to take in a value of any type, that’s allowed!

-- polymorphicFun accepts any two values of the same type, regardless
-- of what that type is, and returns another value of that type.
polymorphicFun :: a -> a -> a
polymorphicFun x y = ...


-- concreteFun requires that you pass it a String, and no other type.
-- It returns an Integer.
concreteFun :: String -> Integer
concreteFun str = ...

There’s a tradeoff to polymorphism, though―the wider the range of values you allow, the fewer operations you can perform on those values. With the type signature a -> a -> a, there’s nothing you can do but return one of the inputs, because you can’t know which operations the inputs support. If you try to assume those values are numbers and add them together, Haskell will catch your logical error and fail to compile.

-- A fully polymorphic function type signature.
-- Takes in two values of type `a`, and returns some value of type `a`.
superPoly :: a -> a -> a

-- This function definition won't compile...we don't know if `a`
-- is a numeric type!
superPoly x y = x + y

This sounds like a pain, but it allows Haskell to catch most errors at compile-time instead of runtime.

Confusing caveats

The type keyword

The type keyword does not define a new type. type actually defines an alias. You can use type to map an existing type to a new name, which is useful when you want to make the name of an existing type more specific for your particular use case.

Say we want to create a new type Person, that stores a person’s name and age. Names are strings and ages are integers, but to make it clearer what those strings and integers represent, we can create Name and Age aliases for String and Integer:

type Name = String
type Age  = Integer

data Person = Person Name Age

Datatypes? Types?

One major source of confusion for me: what’s the difference between datatypes and types?

Nothing. There’s no difference. HPFP says as much in the first paragraph of Chapter 4 (Datatypes):

Types, also called datatypes, …

I must have read that phrase when I started Chapter 4, but by the time I got to the next chapter (Types) I’d forgotten all about it. To avoid passing on my confusion to you, my dear reader, I’ve stuck to “types.”

Typeclasses

Here’s how you create a typeclass:

class Typeclass a where
  fn1 :: a -> [a] -> [a]
  fn2 :: a -> a -> a
  fn3 :: Integer -> a -> Integer

Typeclasses are a whole different animal. If you’re coming from an object-oriented paradigm, typeclasses are similar to interfaces in that any type that has an instance of1 a specific typeclass must implement every function defined by that typeclass.

-- The type Type has an instance of Typeclass
data Type = Definition
instance Typeclass Type where
  fn1 :: ...  -- implementation of fn1 as it applies to Type
  fn2 :: ...  -- implementation of fn2 as it applies to Type
  fn3 :: ...  -- implementation of fn3 as it applies to Type

Typeclasses are useful in that they constrain the range of valid inputs to a function. If you want to write a function that adds one to whatever value it’s given, well, you need to make sure that value is a number. If it’s not a number, adding one to it is meaningless. You can add that numeric constraint like this:

-- Num is a built-in typeclass that defines functions that numeric
-- types must implement
myIncrement :: Num a => a -> a
myIncrement x = x + 1

The Num a => portion restricts the types a could be to types that have an instance of the Num typeclass.

Typeclasses are useful in your everyday life! Here’s a common problem: you usually determine how much you trust someone/something based on the length of their nose, but people, dogs, and elephants all have different kinds of noses. You need to make sure you can use one function to get the length of any nose regardless of the creature you’re dealing with. So you create a typeclass!

class NoseHaver a where
  noseLength :: a -> Integer


data Person = Nose Integer
instance NoseHaver Person where
  noseLength (Nose n) = n

data Dog = Snout Integer
instance NoseHaver Dog where
  noseLength (Snout s) = s

data Elephant = Trunk Integer
instance NoseHaver Elephant where
  noseLength (Trunk t) = t

Now, you can get the nose length of people and dogs and elephants, all using the same function! That will come in handy when you finalize your nose length to trustworthiness function.

You can also specify multiple typeclass constraints on a single value, like so:

-- `a` must have an instance of both the Num and Eq typeclasses
incrementIfTwo :: (Num a, Eq a) => a -> a
incrementIfTwo x = if x == 2 then x + 1 else x

I hope that I’ve helped clarify the differences between types and typeclasses. They’re foundational concepts in Haskell, and they took me a while to fully wrap my head around. I’m no expert, so if you see anything wrong with this, please let me know!


Footnotes

1 “Has an instance of” is Haskell-speak for “implements.”