[Published in Open Source For You (OSFY) magazine, October 2014 edition.]
Haskell is purely a functional programming language and it enforces strictness with the use of types. In this article, we shall explore type classes and user defined data types.
Consider the elem function that takes an element of a type, a list, and returns ‘true’ if the element is a member of the list; and if not, it returns ‘false’. For example:
ghci> 2 `elem` [1, 2, 3]
True
ghci> 5 `elem` [1, 2, 3]
False
It’s type signature is shown below:
ghci> :t elem
elem :: Eq a => a -> [a] -> Bool
The type signature states that the type variable ‘a’ must be an instance of class ‘Eq’. The class constraint is specified after the ’::’ symbol and before the ‘=>’ symbol in the type signature. The elem function will thus work for all types that are instances of the Eq class.
The word ‘class’ has a different meaning in functional programming. A type class is a parameterised interface that defines functions. A type that is an instance of a type class needs to implement the defined functions of the type class. The Eq class defines functions to assert if two type values are equal or not. Its definition in Haskell is as follows:
class Eq a where
(==), (/=) :: a -> a -> Bool
x /= y = not (x == y)
x == y = not (x /= y)
The keyword class is used to define a type class. This is followed by the name of the class (starting with a capital letter). A type variable (‘a’ here) is written after the class name. Two functions are listed in this class for finding if two values of a type are equal or not. A minimal definition for the two functions is also provided. This code is available in libraries/ghc-prim/GHC/Classes.hs in the GHC (Glasgow Haskell Compiler) source code.
The example works for integers ‘2’ and ‘5’ in the example above, because they are of type Int, which is an instance of Eq. Its corresponding definition is given below:
instance Eq Int where
(==) = eqInt
(/=) = neInt
The keyword instance is used in the definition followed by the name of the class Eq, and a specific type Int. It uses two primitive functions eqInt and neInt for checking if the given integers are equal or not. The detailed definition is available in libraries/ghc-prim/GHC/Classes.hs in the GHC source code.
There are a number of pre-defined type classes available in the Haskell platform.
The Ord type class denotes types that can be compared. The compare function will need to be implemented by types that want to be instances of this class. The resultant values of ‘compare’ are GT, LT, or EQ. For example:
ghci> 'p' > 'q'
False
ghci> 3 > 2
True
Its type class definition is as follows:
class (Eq a) => Ord a where
compare :: a -> a -> Ordering
(<), (<=), (>), (>=) :: a -> a -> Bool
max, min :: a -> a -> a
compare x y = if x == y then EQ
else if x <= y then LT
else GT
x < y = case compare x y of { LT -> True; _ -> False }
x <= y = case compare x y of { GT -> False; _ -> True }
x > y = case compare x y of { GT -> True; _ -> False }
x >= y = case compare x y of { LT -> False; _ -> True }
max x y = if x <= y then y else x
min x y = if x <= y then x else y
The Ord type class needs to be a sub-class of the Eq class because we should be able to test for equality of two values if they need to be compared. This is also defined as a constraint in the class definition. Seven functions are provided and a minimal definition given in the code snippet. The instance definitions for Char and Int types are available from libraries/ghc-prim/GHC/Classes.hs in the GHC source code.
The Enum type class is for types whose values can be listed in an order for which you can find predecessor and successor elements. For example:
ghci> succ 'a'
b
ghci> pred EQ
LT
The class definition for Enum is given below:
class Enum a where
succ :: a -> a
pred :: a -> a
toEnum :: Int -> a
fromEnum :: a -> Int
enumFrom :: a -> [a]
enumFromThen :: a -> a -> [a]
enumFromTo :: a -> a -> [a]
enumFromThenTo :: a -> a -> a -> [a]
succ = toEnum . (+ 1) . fromEnum
pred = toEnum . (subtract 1) . fromEnum
enumFrom x = map toEnum [fromEnum x ..]
enumFromThen x y = map toEnum [fromEnum x, fromEnum y ..]
enumFromTo x y = map toEnum [fromEnum x .. fromEnum y]
enumFromThenTo x1 x2 y = map toEnum [fromEnum x1, fromEnum x2 .. fromEnum y]
The instance for type Ordering for the Enum class is as follows:
instance Enum Ordering where
succ LT = EQ
succ EQ = GT
succ GT = error "Prelude.Enum.Ordering.succ: bad argument"
pred GT = EQ
pred EQ = LT
pred LT = error "Prelude.Enum.Ordering.pred: bad argument"
toEnum n | n == 0 = LT
| n == 1 = EQ
| n == 2 = GT
toEnum _ = error "Prelude.Enum.Ordering.toEnum: bad argument"
fromEnum LT = 0
fromEnum EQ = 1
fromEnum GT = 2
-- Use defaults for the rest
enumFrom = boundedEnumFrom
enumFromThen = boundedEnumFromThen
You can find the definition and instance definitions for Char and Ordering in libraries/base/GHC/Enum.lhs in the GHC source code.
The Show type class lists a show function to display or print data. For example:
ghci> show 3.1415
"3.1415
ghci> show True
"True"
The above code works for both ‘Float’ and ‘Bool’ because there are instance definitions for each in the Show type class.
The read function for the ‘Read’ type class takes as input a ‘String’ and converts it to an appropriate data type, if possible. For example:
ghci> read "1" + 2.0
3.0
ghci> read "False" || True
True
You will find the class definitions and instances for Show and Read in libraries/base/GHC/Show.lhs and libraries/base/GHC/Read.lhs respectively. The .lhs file is a literate Haskell source file in which you can combine both text and code. You can also find the definition for a class, a function or type inside GHCi using ’:i’. For example:
ghci> :i Eq
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
-- Defined in `GHC.Classes'
instance Eq Integer -- Defined in `integer-gmp:GHC.Integer.Type'
instance Eq Ordering -- Defined in `GHC.Classes'
instance Eq Int -- Defined in `GHC.Classes'
instance Eq Float -- Defined in `GHC.Classes'
instance Eq Double -- Defined in `GHC.Classes'
instance Eq Char -- Defined in `GHC.Classes'
instance Eq Bool -- Defined in `GHC.Classes'
...
ghci> :i read
read :: Read a => String -> a -- Defined in `Text.Read'
ghci> :i Int
data Int = GHC.Types.I# GHC.Prim.Int# -- Defined in `GHC.Types'
instance Bounded Int -- Defined in `GHC.Enum'
instance Enum Int -- Defined in `GHC.Enum'
instance Eq Int -- Defined in `GHC.Classes'
instance Integral Int -- Defined in `GHC.Real'
instance Num Int -- Defined in `GHC.Num'
instance Ord Int -- Defined in `GHC.Classes'
instance Read Int -- Defined in `GHC.Read'
instance Real Int -- Defined in `GHC.Real'
instance Show Int -- Defined in `GHC.Show'
Let’s suppose you input the following in a GHCi prompt:
ghci> read "3"
<interactive>:5:1:
No instance for (Read a0) arising from a use of `read'
The type variable `a0' is ambiguous
Possible fix: add a type signature that fixes these type variable(s)
Note: there are several potential instances:
instance Read () -- Defined in `GHC.Read'
instance (Read a, Read b) => Read (a, b) -- Defined in `GHC.Read'
instance (Read a, Read b, Read c) => Read (a, b, c)
-- Defined in `GHC.Read'
...plus 25 others
In the expression: read "3"
In an equation for `it': it = read "3"
The interpreter does not know what type to convert ‘3’ to, and hence you will need to explicitly specify the type:
ghci> read "3" :: Int
3
ghci> read "3" :: Float
3.0
A type synonym is an alias that you can use for a type. ‘String’ in the Haskell platform is an array of characters defined using the type keyword:
type String = [Char]
You can also create a new user data type using the data keyword. Consider a Weekday data type that has the list of days in a week:
data Weekday = Monday
| Tuesday
| Wednesday
| Thursday
| Friday
| Saturday
| Sunday
The data keyword is followed by the name of the data type, starting with a capital letter. After the ‘equal to’ (‘=’) sign, the various value constructors are listed. The different constructors are separated by a pipe (‘|’) symbol.
If you load the above data type in GHCi, you can test the value constructors:
ghci> :t Monday
Monday :: Weekday
Each value constructor can have many type values. The user defined data type can also derive from type classes. Since the primitive data types already derive from the basic type classes, the user defined data types can also be derived. Otherwise, you will need to write instance definitions for the same. The following is an example for a user data type ‘Date’ that derives from the ‘Show’ type class for displaying the date:
data Date = Int String Int deriving (Show)
Loading the above in GHCi, you get:
ghci> Date 3 "September" 2014
Date 3 "September" 2014
The above code will work even if we swap the year and day because the syntax is correct but the semantics are not!
ghci> Date 2014 "September" 3
Date 2014 "September" 3
You can also use the record syntax that can give you helper functions:
data Date = Date { day :: Int
, month :: String
, year :: Int
}
This gives you three helper functions to retrieve the day, month and year from a ‘Date’.
ghci> let d = Date {day = 14, month = "September", year = 2014}
ghci> day d
14
ghci> month d
"September"
You can also make data type definition more explicit with types:
data Date = Date Day Month Year deriving (Show)
type Year = Int
type Day = Int
data Month = January
| February
| March
| April
| May
| June
| July
| August
| September
| October
| November
| December
deriving (Show)
Loading the above in GHCi, you can use:
ghci> Date 3 September 2014
Date 3 September 2014
To support printing the date in a specific format, you can implement an instance for the ‘Show’ type class. You can also add a check to ensure that the day is within a range, and the year and day cannot be swapped:
instance Show Date where
show (Date d m y)
| d > 0 && d <= 31 = (show d ++ " " ++ show m ++ " " ++ show y)
| otherwise = error "Invalid day"
Loading the code in GHCi, and running the following:
ghci> show (Date 3 September 2014)
"3 September 2014"
ghci> show (Date 2014 September 2)
"*** Exception: Invalid day
Suppose, you wish to support different Gregorian date formats, you can define a data type GregorianDate as follows:
data GregorianDate = DMY Day Month Year | YMD Year Month Day
You can also define your own type classes for functions that define their own behaviour. For example, if you wish to dump the output of a date that is separated by dashes, you can write a ‘Dashed’ class with a dash function.
class Dashed a where
dash :: a -> String
instance Dashed Date where
dash (Date d m y) = show d ++ "-" ++ show m ++ "-" ++ show y
Testing the above in GHCi will give the following output:
ghci> dash (Date 14 September 2014)
"14-September-2014"
Haskell allows you to define recursive data types also. A parameterized list is defined as:
data List a = Empty | Cons a (List a) deriving (Show)
Lists for the above definition can be created in GHCi, using the following commands:
ghci> Empty
Empty
ghci> (Cons 3 (Cons 2 (Cons 1 Empty)))
(Cons 3 (Cons 2 (Cons 1 Empty)))