Haskell is the world's finest imperative programming language." The above quote comes from Simon P Jones, the creator of Haskell. To newcomers to Functional programming, this may seem weird and mystifying. After all, Haskell is usually known for its advanced Functional programming features such as purity and strong static typing. While it's likely that Simon said this at least partly in jest, the fact is that those same features also make imperative programming in Haskell much more flexible and powerful than in most other languages!
In this talk, we present a view of Haskell and strongly typed functional programming from the "other side" of the prism. We discuss how you can build and compose powerful imperative abstractions with Haskell using the same FP toolset. We discuss how to write common imperative algorithms in Haskell while exploiting advanced features like first-class computations, laziness, and strong static typing. We will also touch upon interfacing with external imperative systems (such as databases, and network communication). At the end of the talk, participants should have enough groundwork to explore Haskell as a *practical* language, and have a greater appreciation of the powerful features offered by Functional Programming.
4. 4
âť‘Type safety. Eliminates a large class of errors.
âť‘Effectful values are first class
âť‘Higher Order Patterns
âť‘Reduction in Boilerplate
âť‘Zero Cost Code Reuse
Overview
5. 5
âť‘Order of operations matters
âť‘Contrast with functional, where the order of
operations does not matter.
Define “Imperative”
6. 6
write "Do you want a pizza?”
if (read() == "Yes") orderPizza()
write "Should I launch missiles?”
if (read() == "Yes") launchMissiles()
Imperative is simple
7. 7
write "Do you want a pizza?”
if (read() == "Yes") orderPizza()
write "Should I launch missiles?”
if (read() == "Yes") launchMissiles()
Imperative is simple
You REALLY DON’T
want to do these
out of order
8. 8
do
write "Do you want a pizza?"
canOrder <- read
When (canOrder == "Yes") orderPizza
write "Should I launch missiles?"
canLaunch <- read
When (canLaunch == "Yes") launchMissiles
Functional?
9. 9
do
write "Do you want a pizza?"
canOrder <- read
when (canOrder == "Yes") orderPizza
write "Should I launch missiles?"
canLaunch <- read
when (canLaunch == "Yes") launchMissiles
Functional?
Haskell
10. 10
write "Do you want a pizza?" >>= _ ->
read >>= canOrderPizza ->
if (canOrderPizza == "Yes") then
orderPizza
else pure () >>= _ ->
write "Should I launch missiles?" >>= _ -
>
read >>= canLaunchMissiles ->
if (canLaunchMissiles == "Yes") then
launchMissiles
else pure ()
Functional?
11. 11
plusOne = x -> x+1
add = x -> y -> x+y
A bit of syntax
Lambdas
14. 14
write "Do you want a pizza?" >>= _ ->
read >>= canOrderPizza ->
if (canOrderPizza == "Yes") then
orderPizza
else pure ()
One At a Time
15. 15
write "Should I launch missiles?" >>= _ ->
read >>= canLaunchMissiles ->
if (canLaunchMissiles == "Yes") then
launchMissiles
else pure ()
One At a Time
18. 18
handlePizza :: IO ()
handlePizza = do
write "Do you want a pizza?"
canOrderPizza <- read
if (canOrderPizza == "Yes")
then orderPizza
else pure ()
Types This entire block
1. Is Effectful
2. Returns ()
25. 25
handlePizza :: IO Bool
handlePizza = do
write "Do you want a pizza?"
canOrderPizza <- read
if (canOrderPizza == "Yes")
then orderPizza >> pure true
else pure false
Types
27. 27
âť‘Ask the user a bunch of questions
âť‘Then perform a bunch of actions
Reorder?
28. 28
Must Rearchitect
do
write "Do you want a pizza?"
canOrder <- read
write "Should I launch missiles?"
canLaunch <- read
when (canOrder == "Yes") orderPizza
when (canLaunch == "Yes") launchMissiles
29. 29
Must Rearchitect
do
write "Do you want a pizza?"
canOrder <- read
write "Should I launch missiles?"
canLaunch <- read
when (canOrder == "Yes") orderPizza
when (canLaunch == "Yes") launchMissiles
But we have lost
the separation
between
Ordering pizza
and Launching nukes
30. 30
We Need
âť‘Define complex flows with user input and a final
effect to be performed
âť‘To compose these flows without boilerplate
âť‘Be able to run the final effects together at the end
of all user input
31. 31
Desired Abstraction
handlePizza = ...
handleNukes = ...
do
handlePizza
handleNukes
We ask questions in this order,
but the final effect of ordering pizza
and launching nukes should only
happen together at the end
33. 33
Must Rearchitect
handlePizza :: IO (IO ())
handlePizza = do
write "Do you want a pizza?"
canOrder <- read
return $
when (canOrder == "Yes") orderPizza
Return value is a CLOSURE
Captures `canOrder`
34. 34
Must Rearchitect
handleNukes :: IO (IO ())
handleNukes = do
write “Should I launch nukes?"
canLaunch <- read
return $
when (canLaunch == "Yes") launchNukes
Return value is a CLOSURE
Captures `canLaunch`
38. 38
And Allow A Way to
specify “No Effects”
finalEffect = emptyEffects
39. 39
Looks Like a Monoid!
class Monoid M where
empty :: M

(<>) :: M -> M -> M
40. 40
IO already is a
Monoid!
âť‘What happens when we do the following?
handlePizza <> handleNukes
41. 41
IO already is a
Monoid!
instance Monoid a => Monoid (IO a) where
empty = pure empty
f <> g = do
a <- f
b <- g
pure (a <> b)
42. 42
IO already is a
Monoid!
instance Monoid a => Monoid (IO a) where
empty = pure empty
f <> g = do
a <- f
b <- g
pure (a <> b)
First perform individual effects
43. 43
IO already is a
Monoid!
instance Monoid a => Monoid (IO a) where
empty = pure empty
f <> g = do
a <- f
b <- g
pure (a <> b) Then Join the results
As Monoids
44. 44
IO already is a
Monoid!
âť‘So this does the right thing!
do
finalEffects <- handlePizza <> handleNukes
finalEffects
45. 45
This is also a pattern
join :: Monad M => M (M a) -> M a
join :: IO (IO a) -> IO a
join (handlePizza <> handleNukes)
46. 46
No Boilerplate!
join :: Monad M => M (M a) -> M a
join :: IO (IO a) -> IO a
join (handlePizza <> handleNukes)
49. 49
Final Code

Combine flows together
join (handlePizza <> handleNukes <> ...)
join (mappend [ handlePizza
, handleNukes
...
])
Or Perhaps
50. 50
❑We don’t launch nukes without ordering pizza
❑We don’t order pizza when not launching nukes
Change Requirements
Again
51. 51
Must Rearchitect
do
write "Do you want a pizza?"
canOrder <- read
write "Should I launch missiles?"
canLaunch <- read
when (canOrder == “Yes" && canLaunch ==
"Yes") (orderPizza >> launchMissiles)
52. 52
Must Rearchitect
do
write "Do you want a pizza?"
canOrder <- read
write "Should I launch missiles?"
canLaunch <- read
when (canOrder == “Yes" && canLaunch ==
"Yes") (orderPizza >> launchMissiles)
Business Logic
53. 53
A General Pattern
do
write “Question 1 ...”
answer1 <- read
...
when (validates answer1 ...)
performAllEffects
54. 54
We Need
âť‘Define complex flows with user input and a final
effect to be performed
âť‘To compose these flows without boilerplate
âť‘Call a function on all the user input to determine if
we should perform the final effects.
âť‘Be able to run the final effects together at the end
of all user input
55. 55
Can we do this with
Monoids?
do
finalEffects <- handlePizza <> handleNukes
finalEffects
âť‘We abstracted away the captured variables
âť‘Now all we can do is run the final composed effect
We can’t access `canOrder` or `canLaunch` here
57. 57
Let's work through this
data Ret a = Ret
{ input :: a
, effect :: IO ()
}
âť‘Return the final effect, AND the user input
âť‘Parameterise User Input as `a`
58. 58
Let's work through this
handlePizza :: IO (Ret Boolean)
handlePizza = do
write "Do you want a pizza?"
canOrder <- read
return $ Ret canOrder $
when (canOrder == "Yes") orderPizza
59. 59
Compose Effects
do
retPizza <- handlePizza
retNuke <- handleNuke
when valid (input retPizza) (input
retNuke) do
effect retPizza
effect retNuke
60. 60
Compose Effects
do
retPizza <- handlePizza
retNuke <- handleNuke
when valid (input retPizza) (input
retNuke) do
effect retPizza
effect retNuke
UGH! Boilerplate!
61. 61
Compose Effects
do
retPizza <- handlePizza
retNuke <- handleNuke
let go = valid (input retPizza) (input
retNuke)
when go do
effect retPizza
effect retNuke
62. 62
Compose Effects
do
retPizza <- handlePizza
retNuke <- handleNuke
let go = valid (input retPizza) (input
retNuke)
when go do
effect retPizza
effect retNuke
Applicative!
63. 63
IO is an Applicative
instance Applicative IO where
f <*> a = do
f' <- f
a' <- a
pure (f' a')
64. 64
Try to Use Applicative IO
do
go <- valid
<$> (input <$> handlePizza)
<*> (input <$> handleNuke)
when go do
effect ??retPizza
effect ??retNuke
65. 65
Dial Back a Little
do
(retPizza, retNuke) <- (,)
<$> handlePizza
<*> handleNuke
let go = valid
<$> input retPizza
<*> input retNuke
when go do
effect retPizza
effect retNuke
66. 66
Perhaps a try a
different abstraction
do
(retPizza, retNuke) <- (,)
<$> handlePizza
<*> handleNuke
let go = valid
<$> input retPizza
<*> input retNuke
when go do
effect retPizza
effect retNuke
This is a common pattern
Can we abstract this?
67. 67
Running a Return value
data Ret a = Ret
{ input :: a
, effect :: IO ()}
runRet :: Ret Bool -> IO ()
runRet (Ret b e) = when b e
68. 68
More trouble than its
worth?
do
(retPizza, retNuke) <- (,)
<$> handlePizza
<*> handleNuke
let go = valid
<$> input retPizza
<*> input retNuke
runRet ??? We need to Compose a Ret
To be able to run it
69. 69
However!
do
(retPizza, retNuke) <- (,)
<$> handlePizza
<*> handleNuke
let go = valid
<$> input retPizza
<*> input retNuke
runRet ???
This could return a
Ret instead!
70. 70
Combining Return values
data Ret a = Ret
{ input :: a
, effect :: IO ()}
instance Functor Ret where
fmap f (Ret a e) = Ret (f a) e
instance Applicative Ret where
Ret f e1 <*> Ret a e2 =
Ret (f a) (e1 <> e2)
72. 72
Hmm, Still Boilerplatey
do
(retPizza, retNuke) <- (,)
<$> handlePizza
<*> handleNuke
let ret = valid
<$> retPizza
<*> retNuke
runRet ret
Two Successive
Applicatives
73. 73
Hmm, Still Boilerplatey
do
(retPizza, retNuke) <- (,)
<$> handlePizza
<*> handleNuke
let ret = valid
<$> retPizza
<*> retNuke
runRet ret
Combine Effectful

IO
Combine Effectful

Ret
74. 74
Compose Applicatives?
data IO a = ...
data Ret a = Ret
{ input :: a
, effect :: IO ()}
type Flow a = IO (Ret a)
We need an Applicative instance for Flow
77. 77
Running Compose
runRet :: Ret Bool -> IO ()
runRet (Ret b e) = when b e
runFlow :: Compose IO Ret Bool -> IO ()
runFlow (Compose e) = e >>= runRet
78. 78
Defining Flows
handlePizza :: Flow Boolean
handlePizza = Compose $ do
write "Do you want a pizza?"
canOrder <- read
return $ Ret canOrder $
when (canOrder == "Yes") orderPizza
81. 81
âť‘Type safety. Eliminates a large class of errors.
âť‘Effectful values are first class
âť‘Higher Order Patterns
âť‘Reduction in Boilerplate
âť‘Zero Cost Code Reuse
Takeaways