Understanding Haskell types

I recently took a break from Perl work to study at the Recurse Center. I’m learning Haskell, and it’s been an interesting adventure so far. I’d heard good things about Haskell’s type system and started with an introductory book Learn You a Haskell for Great Good!. The book is filled with cartoonish humor - “how hard can this be?” I asked myself. The answer was “hard”. I found Haskell’s type system to be counter-intuitive, so this article lays out my understanding of Haskell types. If you’re a programmer with an imperative programming background, you might find this useful.

Your intuition is wrong

For imperative-language programmers, Haskell keywords are likely to mislead. Take the type keyword for instance. It’s not for creating new types per se, but type synonyms, which are like aliases for existing types. I might use it like this:

type FirstName = String
type LastName = String
type Age = Int

One way to declare new types is with the data keyword (naturally!). If I wanted to create a person type, I could use data :

data Person = Person String String Int

This declares a new type called Person with 2 strings and 1 integer as attributes. But I could also use our type synonyms from the earlier example, and clarify my intentions:

data Person = Person FirstName LastName Age

Functions and types

In Haskell function signatures can be restricted by types. I can create a function to tell which of two people is older:

eldest :: Person -> Person -> String
eldest (Person x1 y1 z1) (Person x2 y2 z2)
  | z1 > z2   = x1 ++ " " ++ y1 ++ " is older"
  | z1 < z2   = x2 ++ " " ++ y2 ++ " is older"
  | otherwise = "They're the same age!"

This is a lot of new syntax, so bear with me. The first line declares a function called eldest which takes two persons and returns a string. The second line assigns the attributes of each person to variables. The rest of the function tests which person is older and returns an appropriate message. I’ll save all of this code into a file called “person.hs”, so I can test the function in the Haskell REPL, ghci

ghci> :l person.hs
[1 of 1] Compiling Main             ( person.hs, interpreted )
Ok, modules loaded: Main.
ghci> let a = Person "Bart" "Simpson" 10
ghci> let b = Person "Lisa" "Simpson" 7
ghci> eldest a b
"Bart Simpson is older"

Sometimes we don’t need to access all of the attributes of a type in a function. In these cases Haskell let’s you use _ as a placeholder, that won’t be assigned to a variable. For example to print the initials of a person, I only need to know their first and last names:

initials :: Person -> String
initials (Person x y _) = [head x,'.',head y, '.']

The second line of code assigns a person’s firstname to x and lastname to y. It then takes the first char of each using head and returns a new list of chars with a dot after each char. I can test the function by reloading “person.hs”:

ghci> :l person.hs
[1 of 1] Compiling Main             ( person.hs, interpreted )
Ok, modules loaded: Main.
ghci> let a = Person "Maggie" "Simpson" 1
ghci> initials a
"M.S."

Typeclasses

Typeclasses are similar to traits (roles in Perl-speak) for types. For example, integers are instances of typeclasses like Ord as they are orderable, Num as they are numbers, and so on. Each typeclass defines functions for handling types in specific contexts. The Eq typeclass adds the ability to compare types for equality using operators like ==.

By generalizing the properties of types with typeclasses, Haskell can support generic functions which operate on typeclasses, instead of being restricted to one type. The signature of the quicksort function from Learn You a Haskell is a great example of this:

quicksort :: (Ord a) => [a] -> [a]
quicksort [] = []
quicksort (x:xs) =·
    let smallerSorted = quicksort [a | a <- xs, a <= x]
        biggerSorted = quicksort [a | a <- xs, a > x]
    in  smallerSorted ++ [x] ++ biggerSorted

This declares a new function called quicksort which is restricted to lists of orderable types. Ignore the body and just focus on the first line of code, the function signature. The code (Ord a) defines the typeclass constraint for the function. This function can be used to sort anything orderable, like lists of numbers. Aren’t strings just lists of chars? I guess we can sort them with quicksort too then.

Instance and Class

If you saw the instance keyword in some Haskell code, you might think “aha, a singleton constructor!” but actually instance is used to make types instances of typeclasses. This makes sense when you consider that every type is an instance of a typeclass. Revisiting my Person type from earlier, what if I wanted to make it orderable? Typically in the English-speaking parts of the world, people are sorted by their last name, so I’m going to implement it that way:

data Person = Person FirstName LastName Age deriving (Eq, Show)

instance Ord Person where
  compare (Person _ a _) (Person _ b _) = compare a b

I start by updating the type declaration of Person with deriving (Eq, Show). These operate on the whole type (all of its attributes together). Eq will let Haskell compare Persons for equality and Show just let’s Haskell serialize the type as a string. The second line of code uses instance to make persons orderable. The final line implements a comparison function using the lastname attribute of the Person. I can test the code using the quicksort function declared above.

ghci> :l person.hs
[1 of 1] Compiling Main             ( person.hs, interpreted )
Ok, modules loaded: Main.
ghci> let a = Person "Jason" "Bourne" 37
ghci> let b = Person "James" "Bond" 42
ghci> quicksort [a,b]
[Person "James" "Bond" 43,Person "Jason" "Bourne" 37]

This sorted our list of people by their lastname, and because Person is an instance of Show, Haskell was able to print out the detail to the command line. Not bad!

The final keyword to be aware of is class. By now it shouldn’t surprise you to find out that class is not for declaring classes like in imperative programming, but for creating new typeclasses. You probably won’t use this much when starting out with Haskell, but it’s useful to keep in mind for reducing repetitive code. If you have multiple sets of code doing very similar things for different types, consider creating a new typeclass and merging the functions to operate on the new type class, to keep things DRY.

Code complete

This is the finished code:

--person.hs
type FirstName = String
type LastName  = String
type Age = Int 

data Person = Person FirstName LastName Age deriving (Eq, Show)

eldest :: Person -> Person -> String
eldest (Person x1 y1 z1) (Person x2 y2 z2)
  | z1 > z2   = x1 ++ " " ++ y1 ++ " is older"
  | z1 < z2   = x2 ++ " " ++ y2 ++ " is older"
  | otherwise = "They're the same age!"

initials :: Person -> String
initials (Person x y _) = [head x,'.',head y, '.']

quicksort [] = []
quicksort (x:xs) =·
    let smallerSorted = quicksort [a | a <- xs, a <= x]
        biggerSorted = quicksort [a | a <- xs, a > x]
    in  smallerSorted ++ [x] ++ biggerSorted

instance Ord Person where
  compare (Person _ a _) (Person _ b _) = compare a b

Learn Haskell the Hard Way

Despite its childish demeanor, Learn You a Haskell goes deep into the Haskell type system and can be a bit long-winded at times. My current learning method involves reading the book, and typing out every code example, and studying Penn State’s cis194 course. Both are free. O’Reilly’s Real World Haskell is also available for free online, and emphasizes more immediate practical uses of Haskell. It’s good for when you’re tired of coding binary search trees and sorting algorithms. If you find yourself needing to lookup a Haskell term, DuckDuckGo has the !h bang, which searches Hoogle automatically.

Tags

David Farrell

David is the founder and editor of PerlTricks.com. An organizer of the New York Perl Meetup, he works for ZipRecruiter as a software developer.

Browse their articles