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.
On to the main event…types versus typeclasses.
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
Definition part of the type constructor consists of one or more data constructors. For instance, in this example the data constructor is
data Reader = You
Reader type only has one possible value, and it’s
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
Definitions 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.
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
Age aliases for
type Name = String type Age = Integer data Person = Person Name Age
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.”
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 of[] 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
Num a => portion restricts the types
a could be to types that have an instance of the
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!
[]: “Has an instance of” is Haskell-speak for “implements.”