Haskell Design Patterns - Sample Chapter
Haskell Design Patterns - Sample Chapter
ee
P U B L I S H I N G
C o m m u n i t y
D i s t i l l e d
E x p e r i e n c e
$ 34.99 US
22.99 UK
Ryan Lemmer
pl
Sa
m
Ryan Lemmer
Preface
This book is not a blow-by-blow translation of the Gang of Four design patterns
(distilled out of the object-oriented programming paradigm). Having said that,
wherever there is an intersection with Gang of Four patterns, we explore it
more deeply.
This book is also not intended as a definitive taxonomy of patterns in functional
programming or Haskell. Instead, this book is the story of modern Haskell,
one pattern at a time, one line of code at a time. By following the historical arc
of development, we can place the elements of modern Haskell in a conceptual
framework more easily.
Preface
Chapter 5, Patterns of Type Abstraction, retraces the evolution of the Haskell type
system, one key language extension at a time. We'll explore RankNtypes, existensial
types, phantom types, GADTs, the type-case pattern, dynamic types, heterogeneous
lists, multiparameter typeclasses, and functional dependencies.
Chapter 6, Patterns of Generic Programming, delves into patterns of generic
programming, with a focus on datatype generic programming. We will taste three
flavors of generic programming: sum of products generic programming, origami
programming, and scrap your boilerplate.
Chapter 7, Patterns of Kind Abstraction, delves into the Haskell kind system and related
language extensions: associated types, type families, kind polymorphism, and type
promotion. We'll get a sense of type-level programming and then conclude by going
to the edge of Haskell: diving into dependently-typed programming.
[1]
Higher-order functions
Currying
Recursion
Lazy Evaluation
Monads
Higher-order functions
Functions are our first kind of "glue" in Haskell.
Functions can produce other functions (here, by currying the foldr function):
sum = foldr (+) 0
[2]
Chapter 1
Composing functions
Let's compose these three functions, f, g, and h, in a few different ways:
f, g, h :: String -> String
Currying functions
Haskell allows for both curried and uncurried functions:
greetCurried :: String -> String -> String
greetCurried title name
= "Greetings " ++ title ++ " " ++ name
greetUncurried :: (String, String) -> String
greetUncurried (title, name)
= "Greetings " ++ title ++ " " ++ name
Let's suppose that we need a function with the first argument fixed:
greetCurried' :: String -> String
greetCurried' = greetCurried "Ms"
greetUncurried' :: String -> String
greetUncurried' name = greetUncurried ("Ms", name)
[3]
In both cases, we have applied one of the arguments and thereby specialized
our original function. For the uncurried function we needed to mention all
parameters in the reshaped function, while for the curried one we could just
ignore subsequent arguments.
Since it is fairly easy to translate a curried function to an uncurried function
(and vice versa) the question arises: why and when would one want to use
uncurried functions?
This would not work because the max function is curried, but we can easily align the
types by uncurrying:
uncurry max (g 11)
Whenever we have a function returning a tuple and we want to consume that tuple
from a curried function, we need to uncurry that function. Alternatively, if we are
writing a function to consume an output tuple from another function, we might
choose to write our function in uncurried (tuple arguments) form so that we don't
have to later uncurry our function or unpack the tuple.
It is idiomatic in Haskell to curry by default. There is a very important reason for
this. Thanks to currying, we can do this:
map (map square) [[1], [2,2], [3,3,3]]
[4]
Chapter 1
Recursion
Recursion is even more fundamental than functions and types, in the sense that we
can have recursive functions and types. Moreover, recursion can refer to syntax
(a function or type referring to itself) or to the execution process.
Non-tail recursion
Recursion can be viewed as a pattern for avoiding mutable state:
sumNonTail [] = 0
sumNonTail (x:xs) = x + (sumNonTail xs)
Without recursion, we would need to iterate through the list and keep adding to an
intermediary sum until the list is exhausted, consider the example:
sumNonTail [2, 3, 5, 7]
This first expands into a nested chain of deferred operations, and when there are
only primitives left in the expression, the computation starts folding back on itself:
----------
2 +
2 +
2 +
2 +
2 +
2 +
2 +
2 +
17
sumNonTail [3, 5, 7]
(3 + sumNonTail [5, 7])
(3 + (5 + sumNonTail [7]))
(3 + (5 + (7 + sumNonTail [])))
(3 + (5 + (7 + 0)))
(3 + (5 + 7))
(3 + 12)
15
[5]
Tail recursion
Tail recursion addresses the exorbitant use of space we have with non-tail-recursive
processes:
sumTail' acc [] = acc
sumTail' acc (x:xs) = sumTail' (acc + x) xs
sumTail xs = sumTail' 0 xs
This form of recursion looks less like mathematical induction than the sumNonTail
function did, and it also requires a helper function sumTail' to get the same ease of
use that we had with sumNonTail. The advantage is clear when we look at the use of
"constant space" in this process:
--------
sumTail [2, 3, 5, 7]
sumTail' 0 [2, 3, 5, 7]
sumTail' 2 [3, 5, 7]
sumTail' 5 [5, 7]
sumTail' 10 [7]
sumTail' 17 []
17
The foldl function expands in exactly the same way as sumTail'. In contrast,
foldrSum expands in the same way as sumNonTail:
foldrSum = foldr (+) 0
One can clearly see the tail recursion in the definition of foldl, whereas in the
definition of foldr, recursion is "trapped" by f:
foldr _ v [] = v
foldr f v (x:xs) = f x (foldr f v xs)
foldl _ v [] = v
foldl f v (x:xs) = foldl f (f v x) xs
[6]
Chapter 1
Type combination is also known as "product of types" and type alternation as "sum
of types". In this way, we can create an "algebra of types", with sum and product as
operators, hence the name Algebraic data types.
By parameterizing algebraic types, we create generic types:
data Maybe' a = Nothing' | Just' a
On the left of the = sign we deconstruct; on the right, we construct. In this sense,
pattern matching is the complement of algebraic data types.
[7]
Recursive types
We can capture the Composite pattern very succinctly with recursive algebraic types,
for example:
data Tree a = Leaf a | Branch (Tree a) (Tree a)
This pattern describes the need to sometimes unify a composite structure with
individual members of that structure. In this case, we're unifying Leaf (a leaf being
a part of a tree) and Tree (the composite structure). Now we can write functions that
act on trees and leaves:
size :: Tree a -> Int
size (Leaf x) = 1
size (Branch t u) = size t + size u + 1
Polymorphism
Polymorphism refers to the phenomenon of something taking many forms. In
Haskell, there are two kinds of polymorphism: parametric and ad-hoc (first
described by Strachey in Fundamental Concepts in Programming Languages, 1967).
Parametric polymorphism
In the following code, we have a function defined on a list of any type. The function
is defined at such a high level of abstraction that the precise input type simply never
comes into play, yet the result is of a particular type:
length' :: [a] -> Int
length' [] = 0
length' (x:xs) = 1 + length xs
[8]
Chapter 1
Ad-hoc polymorphism
"Wadler conceived of type classes in a conversation with Joe Fasel. Fasel had in
mind a different idea, but it was he who had the key insight that overloading should
be reflected in the type of the function. Wadler misunderstood what Fasel had in
mind, and type classes were born!"
- History of Haskell, Hudak et al.
The canonical example of adhoc polymorphism (also known as overloading) is that of
the polymorphic + operator, defined for all types that implement the Num typeclass:
class Num a where
(+) :: a -> a -> a
instance Int Num where
(+) :: Int Int Int
x + y = intPlus x y
instance Float Num where
(+) :: Float Float Float
x + y = floatPlus x y
In fact, the introduction of type classes into Haskell was driven by the need to solve
the problem of overloading numerical operators and equality.
When we call (+) on two numbers, the compiler will dispatch evaluation to the
concrete implementation, based on the types of numbers being added:
let x_int = 1 + 1
let x_float = 1.0 + 2.5
let x = 1 + 3.14
-- dispatch to 'intPlus'
-- dispatch to 'floatPlus'
- dispatch to 'floatPlus'
In the last line, we are adding what looks like an int to a float. In many languages,
we'd have to resort to explicit coercion (of int to float, say) to resolve this type
of "mismatch". In Haskell, this is resolved by treating the value of 1 as a type-class
polymorphic value:
ghci> :t 1 -- Num a => a
[9]
The area function is dispatched over the alternations of the Shape type.
Instead of unifying shapes with an algebraic "sum of types", we created two distinct
shape types and unified them with the Shape type-class. This time the area function
exhibits class-based ad-hoc polymorphism.
[ 10 ]
Chapter 1
Class-based
Different coupling
between function and
type
Distribution of function
definition
A perimeter function
acting on Shape won't be
explicitly related to area
in any way.
Type expressivity
The compiler will dispatch to the appropriate overloaded area function, this can
happen in two ways:
We've referred to this as "dispatching on type" but, strictly speaking, type dispatch
would have to resemble the following invalid Haskell:
f v = case (type v) of
Int -> "Int: " ++ (show v)
Bool -> "Bool" ++ (show v)
So far, we have only seen dispatching on one argument or single dispatch. Let's
explore what double-dispatch might look like:
data CustomerEvent = InvoicePaid Float | InvoiceNonPayment
data Customer = Individual Int | Organisation Int
payment_handler :: CustomerEvent -> Customer -> String
payment_handler (InvoicePaid amt) (Individual custId)
= "SendReceipt for " ++ (show amt)
payment_handler (InvoicePaid amount) (Organisation custId)
= "SendReceipt for " ++ (show amt)
payment_handler InvoiceNonPayment (Individual custId)
= "CancelService for " ++ (show custId)
payment_handler InvoiceNonPayment (Organisation custId)
= "SendWarning for " ++ (show custId)
[ 12 ]
Chapter 1
(==) and (/=) are both given mutually recursive default implementations. An
implementer of the Eq class would have to implement at least one of these functions;
in other words, one function would be specialized (ad-hoc polymorphism), leaving
the other defined at a generic level (parametric polymorphism). This is a remarkable
unification of two very different concepts.
[ 13 ]
Here, we are defining an abstract algorithm by letting the caller pass in functions as
arguments, functions that complete the detail of our algorithm. This corresponds to
the strategy pattern, also concerned with decoupling an algorithm from the parts
that may change.
"Strategy lets the algorithm vary independently from clients that use it."
- Design Patterns, Gamma et al.
[ 14 ]
Chapter 1
We have decoupled flow control from function application, which is akin to the
iterator pattern.
Lazy evaluation
The history of Haskell is deeply entwined with the history of lazy evaluation.
"Laziness was undoubtedly the single theme that united the various groups that
contributed to Haskell's design...
Once we were committed to a lazy language, a pure one was inescapable."
- History of Haskell, Hudak et al
Thanks to lazy evaluation, we can still consume the undoomed part of this list:
doomedList = [2, 3, 5, 7, undefined]
take 0 xs = []
take n (x:xs) = x : (take (n-1) xs)
main = do print (take 4 doomedList)
The take function is lazy because the cons operator (:) is lazy (because all functions
in Haskell are lazy by default).
A lazy cons evaluates only its first argument, while the second argument, the tail, is
only evaluated when it is selected. (For strict lists, both head and tail are evaluated at
the point of construction of the list.)
[ 15 ]
The proxy pattern has several motivations, one of which is to defer evaluation; this
aspect of the proxy pattern is subsumed by lazy evaluation.
Streams
The simple idea of laziness has the profound effect of enabling self-reference:
infinite42s = 42 : infinite42s
Streams (lazy lists) simulate infinity through "the promise of potential infinity" [Why
Functional Programming Matters, Hughes]:
potentialBoom = (take 5 infinite42s)
A stream is always just one element cons'ed to a tail of whatever size. A function
such as take consumes its input stream but is decoupled from the producer of the
stream to such an extent that it doesn't matter whether the stream is finite or infinite
(unbounded). Let's see this in action with a somewhat richer example:
generate :: StdGen -> (Int, StdGen)
generate g = random g :: (Int, StdGen)
-- import System.Random
main = do
gen0 <- getStdGen
let (int1, gen1) = (generate g)
let (int2, gen2) = (generate gen1)
Here we are generating a random int value and returning a new generator, which
we could use to generate a subsequent random int value (passing in the same
generator would yield the same random number).
Carrying along the generator from one call to the next pollutes our code and makes
our intent less clear. Let's instead create a producer of random integers as a stream:
randInts' g = (randInt, g) : (randInts' nextGen)
where (randInt, nextGen) = (generate g)
Next, we suppress the generator part of the stream by simply selecting the first part
of the tuple:
randInts g = map fst (randInts' g)
main = do
g <- getStdGen
print (take 3 (randInts g))
[ 16 ]
Chapter 1
We still pass in the initial generator to the stream, but now we can consume
independently from producing the numbers. We could just as easily now
derive a stream of random numbers between 0 and 100:
randAmounts g = map (\x -> x `mod` 100) (randInts g)
This is why it is said that lazy lists decouple consumers from producers. From
another perspective, we have a decoupling between iteration and termination.
Either way, we have decoupling, which means we have a new way to modularize
and structure our code.
Here we have modeled the bank account as a process, which takes in a stream of
transaction amounts. In practice, amounts are more likely to be an unbounded
stream, which we can easily simulate with our randAmounts stream from earlier:
(take 4 (bankAccount 0 (randAmounts g)))
[ 17 ]
Lazy evil
Streams provide an antidote to mutation, but as with all powerful medicine, streams
create new problems. Because streams pretend to express a complete list while only
incrementally materializing the list, we cannot know exactly when evaluation of a
list element happens. In the presence of side effects, this ignorance of the order of
events becomes a serious problem. We will devote the next chapter to dealing with
mutation in the presence of laziness.
Monads
The Monad typeclass is best understood by looking at it from many perspectives.
That is why this book has no definitive section or chapter on Monad. Instead, we
will successively peel off the layers of this abstraction.
Let's begin by looking at a simple example of interpreting expressions:
data Expr = Lit Int | Div Expr Expr
eval :: Expr -> Int
eval (Lit a) = a
eval (Div a b) = eval a `div` eval b
The eval function interprets expressions written in our Expr data type:
(eval (Lit 42))
(eval (Div (Lit 44) (Lit 11)))
- 42
-- 4
Stripped of real-world concerns, this is very elegant. Now let's add (naive) capability
to deal with errors in our interpreter. Instead of the eval function returning integers,
we'll return a Try data type, which caters for success (Return) and failure (Err):
data Try a = Err String | Return a
The refactored evalTry function is now much more syntactically noisy with
case statements:
evalTry :: Expr -> Try Int
evalTry (Lit a) = Return a
evalTry (Div a b) = case (evalTry a) of
Err e
-> Err e
Return a' -> case (evalTry b) of
Err e -> Err e
Return b' -> divTry a' b'
-- helper function
[ 18 ]
Chapter 1
divTry :: Int -> Int -> Try Int
divTry a b = if b == 0
then Err "Div by Zero"
else Return (a `div` b)
The reason for the noise is that we have to explicitly propagate errors. If (evalTry a)
fails, we return Err and bypass evaluation of the second argument.
We've used the Try data type to make failure more explicit, but it has come at
a cost. This is precisely where monads come into play. Let's make our Try data
type an instance of Monad:
instance Monad Try where
return x
= Return x
fail msg
= Err msg
Err e
>>= _
Return a >>= f
= Err e
= f a
>>= _
= Err e
[ 19 ]
The Try data type helped us make failure more explicit, while making it an instance
of Monad made it easier to work with. In this same way, Monad can be used to make
many other "effects" more explicit.
"Being explicit about effects is extremely useful, and this is something that we
believe may ultimately be seen as one of Haskell's main impacts on mainstream
programming"
- History of Haskell, Hudak et al.
The IO monad is particularly interesting and played an important role in the
development of Haskell. When Haskell was first conceived, there were no monads
and also no clear solution to the "problem of side effects". In 1989, Eugenio Moggi
used monads, from Category theory, to describe programming language features.
Phillip Wadler, a then member of the Haskell Committee, recognized that it was
possible to express Moggi's ideas in Haskell code:
"Although Wadler's development of Moggi's ideas was not directed towards the
question of input/output, he and others at Glasgow soon realised that monads
provided an ideal framework for I/O"
-- History of Haskell, Hudak et al
Because Haskell is a purely functional language, side effects call for special
treatment. We will devote a whole chapter to exploring this topic in Chapter 2,
Patterns for I/O.
[ 20 ]
Chapter 1
Summary
In this chapter, we explored the three primary kinds of "glues" that Haskell
provides: functions, the type system, and lazy evaluation. We did so by focusing
on composability of these building blocks and found that wherever we can compose,
we are able to decompose, decouple, and modularize our code.
We also looked at the two main kinds of polymorphism (parametric and ad hoc) as
they occur in Haskell.
This chapter set the scene for starting our study of design patterns for purely
functional programming in Haskell.
The next chapter will focus on patterns for I/O. Working on I/O in the face of lazy
evaluation is a minefield, and we would do well with some patterns to guide us.
[ 21 ]
www.PacktPub.com
Stay Connected: