SUPERCHARGED IMPERATIVE
PROGRAMMING WITH HASKELL
AND FP
ANUPAM JAIN
Slide 2
Slide 2 text
2
Hello!
Slide 3
Slide 3 text
3
❑ HOME PAGE
https://fpncr.github.io
❑ GETTOGETHER COMMUNITY
https://gettogether.community/fpncr/
❑ MEETUP
https://www.meetup.com/DelhiNCR-Haskell-And-
Functional-Programming-Languages-Group
❑ TELEGRAM:
https://t.me/fpncr
Functional Programming NCR
Slide 4
Slide 4 text
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
Slide 5
Slide 5 text
5
❑Order of operations matters
❑Contrast with functional, where the order of
operations does not matter.
Define “Imperative”
Slide 6
Slide 6 text
6
write "Do you want a pizza?”
if (read() == "Yes") orderPizza()
write "Should I launch missiles?”
if (read() == "Yes") launchMissiles()
Imperative is simple
Slide 7
Slide 7 text
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
Slide 8
Slide 8 text
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?
Slide 9
Slide 9 text
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
Slide 10
Slide 10 text
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?
Slide 11
Slide 11 text
11
plusOne = \x -> x+1
add = \x -> \y -> x+y
A bit of syntax
Lambdas
Slide 12
Slide 12 text
12
(>>=) = \effect -> \handler -> ...
A bit of syntax
Operators
Slide 13
Slide 13 text
13
read >>= \canOrderPizza -> ...
A bit of syntax
Infix Usage
Slide 14
Slide 14 text
14
write "Do you want a pizza?" >>= \_ ->
read >>= \canOrderPizza ->
if (canOrderPizza == "Yes") then
orderPizza
else pure ()
One At a Time
Slide 15
Slide 15 text
15
write "Should I launch missiles?" >>= \_ ->
read >>= \canLaunchMissiles ->
if (canLaunchMissiles == "Yes") then
launchMissiles
else pure ()
One At a Time
Slide 16
Slide 16 text
16
handlePizza >>= \_ ->
handleMissiles
Together
Slide 17
Slide 17 text
17
handlePizza >>= \_ ->
handleMissiles
Together
Slide 18
Slide 18 text
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 ()
Slide 19
Slide 19 text
19
Effectful Logic
Pure Logic
Outside World
Slide 20
Slide 20 text
20
❑Can’t mix effectful (imperative) code with pure
(functional) code
❑All branches must have the same return type
Types
Slide 21
Slide 21 text
21
Side Effects !!
Slide 22
Slide 22 text
22
“Haskell” is the world’s finest
imperative programming
language.
~Simon Peyton Jones
(Creator of Haskell)
Slide 23
Slide 23 text
23
So How is Haskell
The Best Imperative
Programming
Language?
Slide 24
Slide 24 text
24
❑We don’t launch nukes without ordering pizza
Change Requirements
Slide 25
Slide 25 text
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
Slide 26
Slide 26 text
26
do
pizzaOrdered <- handlePizza
if pizzaOrdered
then handleMissiles
else pure ()
With Changed
Requirements
Slide 27
Slide 27 text
27
❑Ask the user a bunch of questions
❑Then perform a bunch of actions
Reorder?
Slide 28
Slide 28 text
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
Slide 29
Slide 29 text
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
Slide 30
Slide 30 text
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
Slide 31
Slide 31 text
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
Slide 32
Slide 32 text
32
Must Rearchitect
handlePizza = do
write "Do you want a pizza?"
canOrder <- read
return $
when (canOrder == "Yes") orderPizza
Slide 33
Slide 33 text
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`
Slide 34
Slide 34 text
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`
Slide 35
Slide 35 text
35
Compose together
do
pizzaEffect <- handlePizza
nukeEffect <- handleNukes
pizzaEffect
nukeEffect
Slide 36
Slide 36 text
36
Generalises?
This looks very boilerplaty
do
pizzaEffect <- handlePizza
nukeEffect <- handleNukes
...
pizzaEffect
nukeEffect
...
Slide 37
Slide 37 text
37
Desired Interface
finalEffect =
handlePizza AND
handleNukes AND
...
Slide 38
Slide 38 text
38
And Allow A Way to
specify “No Effects”
finalEffect = emptyEffects
Slide 39
Slide 39 text
39
Looks Like a Monoid!
class Monoid M where
empty :: M
(<>) :: M -> M -> M
Slide 40
Slide 40 text
40
IO already is a
Monoid!
❑What happens when we do the following?
handlePizza <> handleNukes
Slide 41
Slide 41 text
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)
Slide 42
Slide 42 text
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
Slide 43
Slide 43 text
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
Slide 44
Slide 44 text
44
IO already is a
Monoid!
❑So this does the right thing!
do
finalEffects <- handlePizza <> handleNukes
finalEffects
Slide 45
Slide 45 text
45
This is also a pattern
join :: Monad M => M (M a) -> M a
join :: IO (IO a) -> IO a
join (handlePizza <> handleNukes)
Slide 46
Slide 46 text
46
No Boilerplate!
join :: Monad M => M (M a) -> M a
join :: IO (IO a) -> IO a
join (handlePizza <> handleNukes)
Slide 47
Slide 47 text
47
Final Code
handlePizza
handlePizza :: IO (IO ())
handlePizza = do
write "Do you want a pizza?"
canOrder <- read
return $
when (canOrder == "Yes") orderPizza
Slide 48
Slide 48 text
48
Final Code
handleNukes
handleNukes :: IO (IO ())
handleNukes = do
write “Should I launch nukes?"
canLaunch <- read
return $
when (canLaunch == "Yes") launchNukes
Slide 49
Slide 49 text
49
Final Code
Combine flows together
join (handlePizza <> handleNukes <> ...)
join (mappend [ handlePizza
, handleNukes
...
])
Or Perhaps
Slide 50
Slide 50 text
50
❑We don’t launch nukes without ordering pizza
❑We don’t order pizza when not launching nukes
Change Requirements
Again
Slide 51
Slide 51 text
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)
Slide 52
Slide 52 text
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
Slide 53
Slide 53 text
53
A General Pattern
do
write “Question 1 ...”
answer1 <- read
...
when (validates answer1 ...)
performAllEffects
Slide 54
Slide 54 text
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
Slide 55
Slide 55 text
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
Slide 56
Slide 56 text
56
FP Gives you
Granularly
Powerful
Abstractions
❑Monads are too powerful (i.e. boilerplate)
❑Monoids abstract away too much
❑Need something in the middle
Slide 57
Slide 57 text
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`
Slide 58
Slide 58 text
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
Slide 59
Slide 59 text
59
Compose Effects
do
retPizza <- handlePizza
retNuke <- handleNuke
when valid (input retPizza) (input
retNuke) do
effect retPizza
effect retNuke
Slide 60
Slide 60 text
60
Compose Effects
do
retPizza <- handlePizza
retNuke <- handleNuke
when valid (input retPizza) (input
retNuke) do
effect retPizza
effect retNuke
UGH! Boilerplate!
Slide 61
Slide 61 text
61
Compose Effects
do
retPizza <- handlePizza
retNuke <- handleNuke
let go = valid (input retPizza) (input
retNuke)
when go do
effect retPizza
effect retNuke
Slide 62
Slide 62 text
62
Compose Effects
do
retPizza <- handlePizza
retNuke <- handleNuke
let go = valid (input retPizza) (input
retNuke)
when go do
effect retPizza
effect retNuke
Applicative!
Slide 63
Slide 63 text
63
IO is an Applicative
instance Applicative IO where
f <*> a = do
f' <- f
a' <- a
pure (f' a')
Slide 64
Slide 64 text
64
Try to Use Applicative IO
do
go <- valid
<$> (input <$> handlePizza)
<*> (input <$> handleNuke)
when go do
effect ??retPizza
effect ??retNuke
Slide 65
Slide 65 text
65
Dial Back a Little
do
(retPizza, retNuke) <- (,)
<$> handlePizza
<*> handleNuke
let go = valid
<$> input retPizza
<*> input retNuke
when go do
effect retPizza
effect retNuke
Slide 66
Slide 66 text
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?
Slide 67
Slide 67 text
67
Running a Return value
data Ret a = Ret
{ input :: a
, effect :: IO ()}
runRet :: Ret Bool -> IO ()
runRet (Ret b e) = when b e
Slide 68
Slide 68 text
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
Slide 69
Slide 69 text
69
However!
do
(retPizza, retNuke) <- (,)
<$> handlePizza
<*> handleNuke
let go = valid
<$> input retPizza
<*> input retNuke
runRet ???
This could return a
Ret instead!
Slide 70
Slide 70 text
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)
Slide 71
Slide 71 text
71
Less Boilerplate!
do
(retPizza, retNuke) <- (,)
<$> handlePizza
<*> handleNuke
let ret = valid
<$> retPizza
<*> retNuke
runRet ret
Slide 72
Slide 72 text
72
Hmm, Still Boilerplatey
do
(retPizza, retNuke) <- (,)
<$> handlePizza
<*> handleNuke
let ret = valid
<$> retPizza
<*> retNuke
runRet ret
Two Successive
Applicatives
Slide 73
Slide 73 text
73
Hmm, Still Boilerplatey
do
(retPizza, retNuke) <- (,)
<$> handlePizza
<*> handleNuke
let ret = valid
<$> retPizza
<*> retNuke
runRet ret
Combine Effectful
IO
Combine Effectful
Ret
Slide 74
Slide 74 text
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
Slide 75
Slide 75 text
75
Applicatives Compose!
Import Data.Functor.Compose
type Compose f g a = Compose (f (g a))
type Flow a = Compose IO Ret a
Slide 76
Slide 76 text
76
Applicatives Compose!
instance (Applicative f, Applicative g)
=> Applicative (Compose f g) where
Compose f <*> Compose x =
Compose (liftA2 (<*>) f x)
Slide 77
Slide 77 text
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
Slide 78
Slide 78 text
78
Defining Flows
handlePizza :: Flow Boolean
handlePizza = Compose $ do
write "Do you want a pizza?"
canOrder <- read
return $ Ret canOrder $
when (canOrder == "Yes") orderPizza
Slide 79
Slide 79 text
79
Composing Flow With
Business Logic
valid
<$> handlePizza
<*> handleNukes
<*> ...
Slide 80
Slide 80 text
80
No Boilerplate
runFlow $ valid
<$> handlePizza
<*> handleNuke
Slide 81
Slide 81 text
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