Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Haskell Design Patterns
Haskell Design Patterns

Haskell Design Patterns: Take your Haskell and functional programming skills to the next level by exploring new idioms and design patterns

eBook
€15.99 €23.99
Paperback
€29.99
Subscription
Free Trial
Renews at €18.99p/m

What do you get with Print?

Product feature icon Instant access to your digital eBook copy whilst your Print order is Shipped
Product feature icon Paperback book shipped to your preferred address
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Table of content icon View table of contents Preview book icon Preview Book

Haskell Design Patterns

Chapter 1. Functional Patterns – the Building Blocks

Software design patterns were forged at a time when object oriented programming (OOP) reigned. This led to "design patterns" becoming somewhat synonymous with "OOP design patterns". But design patterns are solutions to problems, and "problems" are relative to the strengths and weaknesses of the context in which they occur. A design problem in OOP is not necessarily one in functional programming (FP), and vice versa.

From a Haskell perspective, many (but not all) of the well known "Gang of Four" patterns [Design patterns, Gamma et al.] become so easy to solve that it is not worth going to the trouble of treating them as patterns. However, design patterns remain relevant for Haskell.

 

"After al, as Erich Gamma said, "deja vu is language neutral"

Modularity means more than modules. Our ability to de-compose a problem into parts depends directly on our ability to glue solutions together. To support modular programming, a language must provide good glue."

 
 --Why Functional Programming Matters - John Hughes

In order to have a meaningful conversation about Haskell design patterns, we'll begin our exploration by looking at the three primary kinds of "glue" in Haskell: first-class functions, the Haskell type system, and lazy evaluation. This chapter revisits the Haskell you already know through the lens of design patterns, and looks at:

  • Higher-order functions
  • Currying
  • Recursion
  • Types, pattern matching, polymorphism
  • Lazy Evaluation
  • Monads

Higher-order functions

Functions are our first kind of "glue" in Haskell.

Functions as first-class citizens

Haskell functions are first-class citizens of the language. This means that:

  • We can name a function just as we can name any primitive value:
    square = \x -> x * x
  • We can pass functions to other functions:
    map square [1, 3, 5, 7]

    (Here, map is a higher-order function.)

  • Functions can produce other functions (here, by currying the foldr function):
    sum = foldr (+) 0
  • Functions can form part of other data structures:
    let fs = [(* 2), (* 3), (* 5)]
    zipWith (\f v -> f v) fs [1, 3, 5]

This places Haskell functions on an equal footing with primitive types.

Composing functions

Let's compose these three functions, f, g, and h, in a few different ways:

f, g, h :: String -> String

The most rudimentary way of combining them is through nesting:

z x = f (g (h x))

Function composition gives us a more idiomatic way of combining functions:

z' x = (f . g . h) x

Finally, we can abandon any reference to arguments:

z'' = f . g . h

This leaves us with an expression consisting of only functions. This is the "point-free" form.

Programming with functions in this style, free of arguments, is called tacit programming.

It is hard to argue against the elegance of this style, but in practice, point-free style can be more fun to write than to read: it can become difficult to infer types (and, therefore, meaning). Use this style when ease of reading is not overly compromised.

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)

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) a question arises: why and when would one want to use uncurried functions?

Currying and composability

Consider a function that returns a few pieces of data, which you choose to express as a tuple:

g n = (n^2, n^3)

Then suppose we want to find the maximum value in that tuple:

max (g 11)

This would not work because max value 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, as you will see with this example. Thanks to currying, we can do this:

map (map square) [[1], [2,2], [3,3,3]]

We cannot, however, do this:

let map' = uncurry map
map' (map' square) [[1], [2,2], [3,3,3]]

We need to explicitly curry map' in order to compose it with other functions:

(curry map') (curry map' square) [[1], [2,2], [3,3,3]]

Curried functions are composable, whereas uncurried functions are not.

Decoupling with currying

If we can apply one function argument at a time, nothing stops us from doing so at entirely different places in our codebase. For instance, we might "wire in" some authentication-related information into our function at one end of the codebase and use the specialized function in another part of the codebase that has no cognizance of the authentication argument and its related types.

This can be a powerful tool for decoupling, the site of decoupling being the function argument list!

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 a 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, as shown in the following code:

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 in on itself:

-- 2 + sumNonTail [3, 5, 7]
-- 2 + (3 + sumNonTail [5, 7])
-- 2 + (3 + (5 + sumNonTail [7]))
-- 2 + (3 + (5 + (7 + sumNonTail [])))
-- 2 + (3 + (5 + (7 + 0)))
-- 2 + (3 + (5 + 7))
-- 2 + (3 + 12)
-- 2 + 15
-- 17

The sumNonTail function is non-tail-recursive. Because the recursion is "trapped" by the + operator, we need to hold the entire list in memory to perform the sum.

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

Even though sumTail is a recursive function, it expresses an iterative process. sumNonTail is a recursive function that expresses a recursive process.

Folding abstracts recursion

Tail recursion is captured by the foldl function, as shown in the following code:

  foldlSum = foldl (+) 0

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

Types, pattern matching, and polymorphism

Algebraic types give us a very concise way to model composite types, even recursive ones. Pattern matching makes it easy to work with algebraic types. Type classes enable both the fundamental types of polymorphism: parametric and ad-hoc.

Together, these capabilities allow us to easily express many of the Gang of Four patterns.

Algebraic types and pattern matching

Algebraic data types can express a combination of types, for example:

type Name = String
type Age = Int
data Person = P String Int -- combination

They can also express a composite of alternatives:

data MaybeInt = NoInt | JustInt Int

Here, each alternative represents a valid constructor of the algebraic type:

maybeInts = [JustInt 2, JustInt 3, JustInt 5, NoInt]

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

Algebraic data type constructors also serve as "deconstructors" in pattern matching:

fMaybe f (Just' x) = Just' (f x)
fMaybe f Nothing' = Nothing'

fMaybes = map (fMaybe (* 2)) [Just' 2, Just' 3, Nothing']

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: they are two sides of the same coin.

Recursive types

We capture the "composite pattern" very succinctly by creating 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

Functions over recursive types are typically recursive themselves.

Polymorphism

Polymorphism points at 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

The length object exhibits parametric polymorphism because it acts uniformly on a range of types that share a common structure, in this case, lists:

length' [1,2,3,5,7]
length' ['1','2','3','5','7']

In this sense, length is a generic function. Functions defined on parametric data types tend to be generic.

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 ad-hoc polymorphism (also known as "overloading") is that of the polymorphic + operator, defined for all Num variables:

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        -- dispatch to 'intPlus'
let x_float = 1.0 + 2.5  -- dispatch to 'floatPlus'
let x  = 1 + 3.14        –- dispatch to 'floatPlus'

In the last line, we are adding what looks like an int to a float variable. 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

1 is a generic value (a Num variable); whether 1 is an int variable or a float variable (or a fractional, say) depends on the context in which it will appear.

Alternation-based ad-hoc polymorphism

There are two kinds of ad-hoc polymorphism. We've seen the first type already in this chapter:

data Maybe' a = Nothing' | Just' a
fMaybe f (Just' x) = Just' (f x)
fMaybe f Nothing' = Nothing'

The fMaybe function is polymorphically defined over the alternations of Maybe. In order to directly contrast the two kinds of polymorphism, let's carry this idea over into another example:

data Shape = Circle Float | Rect Float Float

area :: Shape -> Float
area (Circle r) = pi * r^2
area (Rect length width) = length * width

The area function is dispatched over the alternations of the Shape type.

Class-based ad-hoc polymorphism

We could also have achieved a polymorphic area function over shapes in this way:

data Circle = Circle Float
data Rect = Rect Float Float

class Shape a where
  area :: a -> Float

instance Shape Circle where
  area (Circle r) = pi * r^2
instance Shape Rect where
  area (Rect length' width') = length' * width'

Tip

Downloading the example code

You can download the example code files from your account at http://www.packtpub.com for all the Packt Publishing books you have purchased. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you.

Instead of unifying shapes with an algebraic "sum of types", we created two distinct shape types and unified them through a Shape class. This time the area function exhibits class-based polymorphism.

Alternation-based versus class-based

It is tempting to ask "which approach is best?" Instead, let's explore the important ways in which they differ:

 

Alternation-based

Class-based

Different coupling between function and type

The function type refers to the algebraic type Shape and then defines special cases for each alternative.

The function type is only aware of the type it is acting on, not the Shape "super type".

Distribution of function definition

The overloaded functions are defined together in one place for all alternations.

Overloaded functions all appear in their respective class implementations. This means a function can be overloaded in very diverse parts of the codebase if need be.

Adding new types

Adding a new alternative to the algebraic type requires changing all existing functions acting directly on the algebraic "super type"

We can add a new type that implements the type class without changing any code in place (only adding). This is very important since it enables us to extend third-party code.

Adding new functions

A perimeter function acting on Shape won't be explicitly related to area in any way.

A perimeter function could be explicitly related to area by adding it to the Shape class. This is a powerful way of grouping functions together.

Type expressivity

This is useful for expressing simple type hierarchies.

We can have multiple, orthogonal hierarchies, each implementing the type class (For example, we can express multiple-inheritance type relations). This allows for modeling much richer data types.

Polymorphic dispatch and the visitor pattern

While exploring ad-hoc polymorphism, we saw how we can simulate static type dispatch ("static" meaning that the dispatch is resolved at compile time, as opposed to "dynamic dispatch", resolved only at runtime). Let's return to our area function:

area (Circle 10)

The preceding command will dispatch to the overloaded area function by matching:

  • A sub type of the Shape algebraic type (subtype-based)
  • The type class to which Circle belongs that is, Shape (class-based)

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)

Having said that, pattern-based and type-based dispatching are not that far apart:

  data TypeIntBool = Int' Int | Bool' Bool

  f :: TypeIntBool -> String
  f (Int' v) = "Int: " ++ (show v)
  f (Bool' v) = "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)

The payment_handler object defines behavior for all four permutations of CustomerEvent and Customer. In an OOP language, we would have to resort to the visitor pattern to achieve multiple dispatch.

"Visitor lets you define a new operation without changing the classes of the elements on which it operates...

Languages that support double or multiple dispatch lessen the need for the Visitor pattern." - Design Patterns, Gamma et al.

Unifying parametric and ad-hoc polymorphism

On the one hand, we have parametric polymorphism, where a single generic function acts on a variety of types. This is in contrast to ad-hoc polymorphism, where we have an overloaded function that is resolved to a particular function by the compiler. Put another way, parametric polymorphism allows us to lift the level of abstraction, whereas ad-hoc polymorphism gives us a powerful tool for decoupling.

In this sense, parametric polymorphism is considered to be "true polymorphism", while ad hoc is only "apparent" (or "synthetic").

Haskell blurs the distinction between ad hoc (specialized) and parametric (generic) polymorphism. We can see this clearly in the definition of the type class for equality:

class Eq a where
  (==), (/=) :: a -> a -> Bool
  x == y = not (x /= y)
  x /= y = not (x == y)

(==) 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.

Functions, types, and patterns

Functions and types intersect in several ways. Functions have a type, they can act on algebraic types, they can belong to type classes, and they can be specific or generic in type. With these capabilities, we can express several more Gang of Four patterns.

The strategy pattern

Thanks to higher-order functions (HOF), we can easily inject behavior:

strategy fSetup fTeardown
  = do
      setup
      -– fullfil this function's purpose
      teardown

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.

The template pattern

In "OOP speak", the strategy pattern uses delegation to vary an algorithm, while the template pattern uses inheritance to vary parts of an algorithm. In Haskell, we don't have OOP inheritance, but we have something far more powerful: type classes. We might easily abstract an algorithm with this type class that acts as an abstract class:

class TemplateAlgorithm where
  setup :: IO a → a
  teardown :: IO a → a
  doWork :: a → a
  fulfillPurpose
    = do
       setup
       doWork
       teardown
 

"Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure."

 
 --Design Patterns, Gamma et al.

The iterator pattern

 

"Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation"

 
 --Design Patterns, Gamma et al.

The map function takes care of navigating the structure of the list, while the square function only deals with each element of the list:

map square [2, 3, 5, 7]

We have decoupled flow control from function application, which is akin to the iterator pattern.

Decoupling behavior and modularizing code

Whenever we pass one function into another, we are decoupling two parts of code. Besides allowing us to vary the different parts at different rates, we can also put the different parts in different modules, libraries, or whatever we like.

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 object is lazy because the cons operator (:) is lazy, which is 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.)

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 enables 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, 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))

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.

Modeling change with streams

Consider a banking system, where we want to record the current balance of a customer's account. In a non-pure functional language, we would typically model this with a mutable variable for the balance: for each debit and credit in the "real world", we would mutate the balance variable.

"Can we avoid identifying time in the computer with time in the modeled world?"

[Structure and Interpretation of Computer Programs, p. 316], Abelson and Sussman

Yes, we can describe the evolution of a variable as a function of time. Instead of a mutable variable for bank balance, we have a sequence of balance values. In other words, we replace a mutable variable with the entire history of states:

bankAccount openingB (amt:amts)
  = openingB : bankAccount (openingB + amt) amts

(take 4 (bankAccount 0 [-100, 50, 50, 1]))

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)))
 

"if we concentrate on the entire time history, we do not emphasize change"

 
 --[Structure and Interpretation of Computer Programs, p. 317], Abelson and Sussman

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 list elements 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 from many perspectives. That is why this book has no definitive section or chapter on monad. Instead, we will successively peel of the layers of this abstraction and make good use of it.

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))      –- 42
(eval (Div (Lit 44) (Lit 11)))  -- 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 (Error):

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
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 a monad:

instance Monad Try where
  return x   = Return x
  fail msg   = Err msg

  Err e    >>= _   = Err e
  Return a >>= f   = f a

Next, we refactor evalTry by using the bind operator (>>=):

evalTry' :: Expr -> Try Int
evalTry' (Lit a)   = Return a
evalTry' (Div a b) = (evalTry' a) >>= \a' ->
                       (evalTry' b) >>= \b' ->
                         divTry a' b'

-– evalTry' (Div (Lit 44) (Lit 0))

The bind operator enables error propagation:

Err e  >>= _   = Err e

Whenever we have an Err function, the subsequent part of the bind chain will be ignored and will thereby propagate the error. While this gets rid of our case statements, it is hardly very friendly. Let's rewrite it using the do notation:

evalTry'' (Lit a) = Return a
evalTry'' (Div a b)
  = do
     a' <- (evalTry' a)
     b' <- (evalTry' b)
     divTry a' b'

The Try data type helped us make failure more explicit, while making it a monad made it easier to work with. In this same way, monads 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 purely functional, side effects call for special treatment. We will devote a whole chapter to exploring this topic in Chapter 2, Patterns for I/O.

Composing monads and structuring programs

As useful as monads are for capturing effects, we also need to compose them, for example, how do we use monads for failure, I/O, and logging together?

In the same way that functional composition allows us to write more focused functions that can be combined together, monad composition allows us to create more focused monads, to be recombined in different ways. In Chapter 3: Patterns for Composition, we will explore monad transformers and how to create "monad stacks" of transformers to achieve monad composition.

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.

Left arrow icon Right arrow icon

Key benefits

  • • Explore Haskell on a higher level through idioms and patterns
  • • Get an in-depth look into the three strongholds of Haskell: higher-order functions, the Type system, and Lazy evaluation
  • • Expand your understanding of Haskell and functional programming, one line of executable code at a time

Description

Design patterns and idioms can widen our perspective by showing us where to look, what to look at, and ultimately how to see what we are looking at. At their best, patterns are a shorthand method of communicating better ways to code (writing less, more maintainable, and more efficient code) This book starts with Haskell 98 and through the lens of patterns and idioms investigates the key advances and programming styles that together make "modern Haskell". Your journey begins with the three pillars of Haskell. Then you'll experience the problem with Lazy I/O, together with a solution. You'll also trace the hierarchy formed by Functor, Applicative, Arrow, and Monad. Next you'll explore how Fold and Map are generalized by Foldable and Traversable, which in turn is unified in a broader context by functional Lenses. You'll delve more deeply into the Type system, which will prepare you for an overview of Generic programming. In conclusion you go to the edge of Haskell by investigating the Kind system and how this relates to Dependently-typed programming

Who is this book for?

If you’re a Haskell programmer with a firm grasp of the basics and ready to move more deeply into modern idiomatic Haskell programming, then this book is for you.

What you will learn

  • Understand the relationship between the “Gang of Four” OOP Design Patterns and Haskell.
  • Try out three ways of Streaming I/O: imperative, Lazy, and Iteratee based.
  • Explore the pervasive pattern of Composition: from function composition through to high-level composition with Lenses.
  • Synthesize Functor, Applicative, Arrow and Monad in a single conceptual framework.
  • Follow the grand arc of Fold and Map on lists all the way to their culmination in Lenses and Generic Programming.
  • Get a taste of Type-level programming in Haskell and how this relates to dependently-typed programming.
  • Retrace the evolution, one key language extension at a time, of the Haskell Type and Kind systems.
  • Place the elements of modern Haskell in a historical framework.
Estimated delivery fee Deliver to Romania

Premium delivery 7 - 10 business days

€25.95
(Includes tracking information)

Product Details

Country selected
Publication date, Length, Edition, Language, ISBN-13
Publication date : Nov 06, 2015
Length: 166 pages
Edition : 1st
Language : English
ISBN-13 : 9781783988723
Category :
Languages :

What do you get with Print?

Product feature icon Instant access to your digital eBook copy whilst your Print order is Shipped
Product feature icon Paperback book shipped to your preferred address
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Estimated delivery fee Deliver to Romania

Premium delivery 7 - 10 business days

€25.95
(Includes tracking information)

Product Details

Publication date : Nov 06, 2015
Length: 166 pages
Edition : 1st
Language : English
ISBN-13 : 9781783988723
Category :
Languages :

Packt Subscriptions

See our plans and pricing
Modal Close icon
€18.99 billed monthly
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Simple pricing, no contract
€189.99 billed annually
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just €5 each
Feature tick icon Exclusive print discounts
€264.99 billed in 18 months
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just €5 each
Feature tick icon Exclusive print discounts

Frequently bought together


Stars icon
Total 113.97
Haskell Cookbook
€41.99
Haskell Design Patterns
€29.99
Haskell High Performance Programming
€41.99
Total 113.97 Stars icon

Table of Contents

8 Chapters
1. Functional Patterns – the Building Blocks Chevron down icon Chevron up icon
2. Patterns for I/O Chevron down icon Chevron up icon
3. Patterns of Composition Chevron down icon Chevron up icon
4. Patterns of Folding and Traversing Chevron down icon Chevron up icon
5. Patterns of Type Abstraction Chevron down icon Chevron up icon
6. Patterns of Generic Programming Chevron down icon Chevron up icon
7. Patterns of Kind Abstraction Chevron down icon Chevron up icon
Index Chevron down icon Chevron up icon

Customer reviews

Top Reviews
Rating distribution
Full star icon Full star icon Full star icon Full star icon Half star icon 4.1
(9 Ratings)
5 star 55.6%
4 star 22.2%
3 star 11.1%
2 star 0%
1 star 11.1%
Filter icon Filter
Top Reviews

Filter reviews by




ruben Jan 16, 2016
Full star icon Full star icon Full star icon Full star icon Full star icon 5
This book starts with Haskell 98 and through the lens of patterns and idioms investigates the key advances and programming styles that together make "modern Haskell". Your journey begins with the three pillars of Haskell. Then you'll experience the problem with Lazy I/O, together with a solution. You'll also trace the hierarchy formed by Functor, Applicative, Arrow, and Monad. Next you'll explore how Fold and Map are generalized by Foldable and Traversable, which in turn is unified in a broader context by functional Lenses. You'll delve more deeply into the Type system, which will prepare you for an overview of Generic programming. In conclusion you go to the edge of Haskell by investigating the Kind system and how this relates to Dependently-typed programming.
Amazon Verified review Amazon
Andre M. Van Meulebrouck Sep 15, 2016
Full star icon Full star icon Full star icon Full star icon Full star icon 5
I love this book; it is concise, interesting, insightful, and well written. Lots of food for thought in this book, regardless whether you use Haskell or not.It also provides a behind-the-scenes look at the evolution of Haskell as a language that is very worthwhile, fair, balanced, and objective.Judicious use of quotes is tastefully interjected, which helps to frame the discussion.The book eloquently ends with a quote (from "History of Haskell") which I will summarize in my own words as a conjecture about a day when Haskell will become a distant memory, but when that day comes it will live on in the genes of other technologies which it influenced.Well said!What a gem of a book. I'm glad I didn't miss it.
Amazon Verified review Amazon
SuJo Jan 30, 2016
Full star icon Full star icon Full star icon Full star icon Full star icon 5
I enjoyed reading over this book, Haskell Design Patterns helps approach real world situations that you will experience at one time or another as a programmer. Recursion was covered nicely, my favorite functions are the I/O functions which are covered in Chapter 2.
Amazon Verified review Amazon
Johnny Dec 27, 2018
Full star icon Full star icon Full star icon Full star icon Full star icon 5
I'm blown away by the things I'm learning, and the great way that concepts are explained. Examples: Currying and Tail Recursion. I thought I knew all there was to about currying - I learned it in my CS undergrad after all. But oh, it's totally possible to write functions in an uncurried style by wrapping the parameters in a tuple. That never occurred to me. And then the follow-up with advantages and disadvantages of that with a note about what's idiomatic. Perfect. The book doesn't insult the intelligence of the reader, who's most likely been doing development in other languages.And Tail Recursion --- I didn't quite remember how it works, and sort of had been ignoring this as an 'implementation detail' of programming libraries. But here the explanation and unrolled call stack make it plain of day what's happening, and what the difference is.Lol, two years in to Haskell development, I consider myself to be just an "advanced beginner".
Amazon Verified review Amazon
Perry Nally Jan 31, 2016
Full star icon Full star icon Full star icon Full star icon Full star icon 5
Was helpful learning the design patterns, which is what it's title says it covers. The examples are clear enough to grasp the pattern. If you're new to Haskell, you may want to take it slowly to verify the programs. Functional programming requires a different approach, and this book helped enlighten me.
Amazon Verified review Amazon
Get free access to Packt library with over 7500+ books and video courses for 7 days!
Start Free Trial

FAQs

What is the delivery time and cost of print book? Chevron down icon Chevron up icon

Shipping Details

USA:

'

Economy: Delivery to most addresses in the US within 10-15 business days

Premium: Trackable Delivery to most addresses in the US within 3-8 business days

UK:

Economy: Delivery to most addresses in the U.K. within 7-9 business days.
Shipments are not trackable

Premium: Trackable delivery to most addresses in the U.K. within 3-4 business days!
Add one extra business day for deliveries to Northern Ireland and Scottish Highlands and islands

EU:

Premium: Trackable delivery to most EU destinations within 4-9 business days.

Australia:

Economy: Can deliver to P. O. Boxes and private residences.
Trackable service with delivery to addresses in Australia only.
Delivery time ranges from 7-9 business days for VIC and 8-10 business days for Interstate metro
Delivery time is up to 15 business days for remote areas of WA, NT & QLD.

Premium: Delivery to addresses in Australia only
Trackable delivery to most P. O. Boxes and private residences in Australia within 4-5 days based on the distance to a destination following dispatch.

India:

Premium: Delivery to most Indian addresses within 5-6 business days

Rest of the World:

Premium: Countries in the American continent: Trackable delivery to most countries within 4-7 business days

Asia:

Premium: Delivery to most Asian addresses within 5-9 business days

Disclaimer:
All orders received before 5 PM U.K time would start printing from the next business day. So the estimated delivery times start from the next day as well. Orders received after 5 PM U.K time (in our internal systems) on a business day or anytime on the weekend will begin printing the second to next business day. For example, an order placed at 11 AM today will begin printing tomorrow, whereas an order placed at 9 PM tonight will begin printing the day after tomorrow.


Unfortunately, due to several restrictions, we are unable to ship to the following countries:

  1. Afghanistan
  2. American Samoa
  3. Belarus
  4. Brunei Darussalam
  5. Central African Republic
  6. The Democratic Republic of Congo
  7. Eritrea
  8. Guinea-bissau
  9. Iran
  10. Lebanon
  11. Libiya Arab Jamahriya
  12. Somalia
  13. Sudan
  14. Russian Federation
  15. Syrian Arab Republic
  16. Ukraine
  17. Venezuela
What is custom duty/charge? Chevron down icon Chevron up icon

Customs duty are charges levied on goods when they cross international borders. It is a tax that is imposed on imported goods. These duties are charged by special authorities and bodies created by local governments and are meant to protect local industries, economies, and businesses.

Do I have to pay customs charges for the print book order? Chevron down icon Chevron up icon

The orders shipped to the countries that are listed under EU27 will not bear custom charges. They are paid by Packt as part of the order.

List of EU27 countries: www.gov.uk/eu-eea:

A custom duty or localized taxes may be applicable on the shipment and would be charged by the recipient country outside of the EU27 which should be paid by the customer and these duties are not included in the shipping charges been charged on the order.

How do I know my custom duty charges? Chevron down icon Chevron up icon

The amount of duty payable varies greatly depending on the imported goods, the country of origin and several other factors like the total invoice amount or dimensions like weight, and other such criteria applicable in your country.

For example:

  • If you live in Mexico, and the declared value of your ordered items is over $ 50, for you to receive a package, you will have to pay additional import tax of 19% which will be $ 9.50 to the courier service.
  • Whereas if you live in Turkey, and the declared value of your ordered items is over € 22, for you to receive a package, you will have to pay additional import tax of 18% which will be € 3.96 to the courier service.
How can I cancel my order? Chevron down icon Chevron up icon

Cancellation Policy for Published Printed Books:

You can cancel any order within 1 hour of placing the order. Simply contact customercare@packt.com with your order details or payment transaction id. If your order has already started the shipment process, we will do our best to stop it. However, if it is already on the way to you then when you receive it, you can contact us at customercare@packt.com using the returns and refund process.

Please understand that Packt Publishing cannot provide refunds or cancel any order except for the cases described in our Return Policy (i.e. Packt Publishing agrees to replace your printed book because it arrives damaged or material defect in book), Packt Publishing will not accept returns.

What is your returns and refunds policy? Chevron down icon Chevron up icon

Return Policy:

We want you to be happy with your purchase from Packtpub.com. We will not hassle you with returning print books to us. If the print book you receive from us is incorrect, damaged, doesn't work or is unacceptably late, please contact Customer Relations Team on customercare@packt.com with the order number and issue details as explained below:

  1. If you ordered (eBook, Video or Print Book) incorrectly or accidentally, please contact Customer Relations Team on customercare@packt.com within one hour of placing the order and we will replace/refund you the item cost.
  2. Sadly, if your eBook or Video file is faulty or a fault occurs during the eBook or Video being made available to you, i.e. during download then you should contact Customer Relations Team within 14 days of purchase on customercare@packt.com who will be able to resolve this issue for you.
  3. You will have a choice of replacement or refund of the problem items.(damaged, defective or incorrect)
  4. Once Customer Care Team confirms that you will be refunded, you should receive the refund within 10 to 12 working days.
  5. If you are only requesting a refund of one book from a multiple order, then we will refund you the appropriate single item.
  6. Where the items were shipped under a free shipping offer, there will be no shipping costs to refund.

On the off chance your printed book arrives damaged, with book material defect, contact our Customer Relation Team on customercare@packt.com within 14 days of receipt of the book with appropriate evidence of damage and we will work with you to secure a replacement copy, if necessary. Please note that each printed book you order from us is individually made by Packt's professional book-printing partner which is on a print-on-demand basis.

What tax is charged? Chevron down icon Chevron up icon

Currently, no tax is charged on the purchase of any print book (subject to change based on the laws and regulations). A localized VAT fee is charged only to our European and UK customers on eBooks, Video and subscriptions that they buy. GST is charged to Indian customers for eBooks and video purchases.

What payment methods can I use? Chevron down icon Chevron up icon

You can pay with the following card types:

  1. Visa Debit
  2. Visa Credit
  3. MasterCard
  4. PayPal
What is the delivery time and cost of print books? Chevron down icon Chevron up icon

Shipping Details

USA:

'

Economy: Delivery to most addresses in the US within 10-15 business days

Premium: Trackable Delivery to most addresses in the US within 3-8 business days

UK:

Economy: Delivery to most addresses in the U.K. within 7-9 business days.
Shipments are not trackable

Premium: Trackable delivery to most addresses in the U.K. within 3-4 business days!
Add one extra business day for deliveries to Northern Ireland and Scottish Highlands and islands

EU:

Premium: Trackable delivery to most EU destinations within 4-9 business days.

Australia:

Economy: Can deliver to P. O. Boxes and private residences.
Trackable service with delivery to addresses in Australia only.
Delivery time ranges from 7-9 business days for VIC and 8-10 business days for Interstate metro
Delivery time is up to 15 business days for remote areas of WA, NT & QLD.

Premium: Delivery to addresses in Australia only
Trackable delivery to most P. O. Boxes and private residences in Australia within 4-5 days based on the distance to a destination following dispatch.

India:

Premium: Delivery to most Indian addresses within 5-6 business days

Rest of the World:

Premium: Countries in the American continent: Trackable delivery to most countries within 4-7 business days

Asia:

Premium: Delivery to most Asian addresses within 5-9 business days

Disclaimer:
All orders received before 5 PM U.K time would start printing from the next business day. So the estimated delivery times start from the next day as well. Orders received after 5 PM U.K time (in our internal systems) on a business day or anytime on the weekend will begin printing the second to next business day. For example, an order placed at 11 AM today will begin printing tomorrow, whereas an order placed at 9 PM tonight will begin printing the day after tomorrow.


Unfortunately, due to several restrictions, we are unable to ship to the following countries:

  1. Afghanistan
  2. American Samoa
  3. Belarus
  4. Brunei Darussalam
  5. Central African Republic
  6. The Democratic Republic of Congo
  7. Eritrea
  8. Guinea-bissau
  9. Iran
  10. Lebanon
  11. Libiya Arab Jamahriya
  12. Somalia
  13. Sudan
  14. Russian Federation
  15. Syrian Arab Republic
  16. Ukraine
  17. Venezuela