Update (2021-10-19) Correct inaccurate language referring to simple ADTs thanks to e01izuz
Algebraic Data Types are a way for us to define types like the ones that come with Haskell e.g. Bool
and Int
.
One of the simplest datatypes we can construct in Haskell is a type with a single constructor,
data Frame = MkFrame
x = MkFrame
Examining the type in ghci
:> :type MkFrame
MkFrame :: Frame
:> :type x
x :: Frame
In this example, we use the data
keyword to signify a Data Type, Frame
is the name of that type and we refer to it as a Type Constructor. MkFrame
is called a Data Constructor and this is how we create new instances of the type Frame
.
Enums (Enumeration Types), also referred to as Sum Types allow us to design a type that is reminiscent of a Logical OR. Suppose we want to have a type that will represent a day in the week, the day will be either Saturday, or Monday, or Tuesday, …etc.
data Day = Saturday | Sunday | Monday | Tuesday | Wednesday | Thursday | Friday
day = Tuesday
Examining the type in ghci
:> :type Saturday
Saturday :: Day
:> :type day
day :: Day
We can pattern match on our data type like so:
dayNumber :: Day -> Int
dayNumber Saturday = 1
dayNumber Sunday = 2
dayNumber Monday = 3
dayNumber Tuesday = 4
dayNumber Wednesday = 5
dayNumber Thursday = 6
dayNumber Friday = 7
or using a case of
statement:
dayNumberCase :: Day -> Int
dayNumberCase x =
case x of
Saturday -> 0
Sunday -> 2
Monday -> 3
Tuesday -> 4
Wednesday -> 5
Thursday -> 6
Friday -> 7
We can also declare types whose constructor takes one or more arguments, these are commonly referred to as Product Types. Let’s say we want to represent a point in the cartesian coordinate system i.e. a point with x
and y
coordinates,
data Point = P Int Int
mypoint = P 1 2
Examining in ghci
:> :type P
P :: Int -> Int -> Point
:> :type mypoint
mypoint :: Point
:> :type P 1 2
P 1 2 :: Point
It’s a convention in Haskell code to sometimes label the data constructor with the same label as the type constructor, to illustrate this with our Point
example:
data Point = Point Int Int
mypoint = Point 1 2
Let’s say we want to define a User
type that has the name
and age
of a user, we can try to do something like this
data User = User String Int
where name
is of type String
and age
is of type Int
. However, in our application, we will want to get the name and age from an instance of this data type, we can do this like so:
getName :: User -> String
getName (User name _) = name
getAge :: User -> Int
getAge (User _ age) = age
but, as you can imagine, this would get too daunting and verbose if our data constructor takes a large number of arguments, and we’ll have to modify all these accessor functions if we add or remove arguments from the data constructor. Also, we have to keep in mind the position of each argument. Enter records, records allow us to write:
data User = User
{ name :: String
, age :: Int
}
-- Create a User
user1 = User
{ name = "Oliver Heaviside"
, age = 74
}
-- This still works
user2 = User "James Clerk Maxwell" 48
and get accessor functions for free, these will be automatically generated:
name :: User -> String
age :: User -> Int
We also get a nice field update syntax
changeAge user newAge = user { age = newAge}
user1 = User
{ name = "Oliver Heaviside"
, age = 74
}
newuser = changeAge user1 42
Unfortunately records have some issues, refer to this for more details.
Type constructors can be parametrized with types, the standard Prelude
library defines the Maybe
type:
data Maybe a = Nothing | Just a
Here, Maybe
is parametrized with the generic type a
which can take on any type, this is a bit like Generics in other languages. The Maybe
type
allows us to express the nonexistence of a value, for example when trying to take the head of a list, instead of erroneously applying head to an empty
list, we can defend against this:
lhead :: [a] -> Maybe a
lhead [] = Nothing
lhead xs = Just (head xs)
And, playing around in ghci
:> :type lhead []
lhead [] :: Maybe a
:> :type lhead ['a']
lhead ['a'] :: Maybe Char
:> :type lhead [1]
lhead [1] :: Num a => Maybe a
Haskell allows us to define types in terms of themselves i.e. recursive types. Let’s illustrate this by making a type for a Binary Tree:
data BinTree a = EmptyTree
| Node a (BinTree a) (BinTree a)
mytree = Node 1 EmptyTree EmptyTree
Here, we constructed mytree
to be a tree with a single node, with an empty left and right subtrees. We can also construct a tree with 3 nodes:
mytree2 = Node 1 (Node 2 EmptyTree EmptyTree) (Node 3 EmptyTree EmptyTree)