Supercharged Imperative Programming with Haskell and FP

9d46cbdc3830248a0615f14ae4b7b33d?s=47 Anupam
November 24, 2019

Supercharged Imperative Programming with Haskell and FP

[This talk was presented at TechTriveni 2.0 [https://techtriveni.com/speaker-anupam] on 24 Nov 2019.

"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 atleast 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.

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.

9d46cbdc3830248a0615f14ae4b7b33d?s=128

Anupam

November 24, 2019
Tweet

Transcript

  1. SUPERCHARGED IMPERATIVE PROGRAMMING WITH HASKELL AND FP ANUPAM JAIN

  2. 2 Hello!

  3. 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
  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
  12. 12 (>>=) = \effect -> \handler -> ... A bit

    of syntax Operators
  13. 13 read >>= \canOrderPizza -> ... A bit of syntax

    Infix Usage
  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
  16. 16 handlePizza >>= \_ -> handleMissiles Together

  17. 17 handlePizza >>= \_ -> handleMissiles Together

  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 ()
  19. 19 Effectful Logic Pure Logic Outside World

  20. 20 ❑Can’t mix effectful (imperative) code with pure (functional) code

    ❑All branches must have the same return type Types
  21. 21 Side Effects !!

  22. 22 “Haskell” is the world’s finest imperative programming language. ~Simon

    Peyton Jones (Creator of Haskell)
  23. 23 So How is Haskell The Best Imperative Programming Language?

  24. 24 ❑We don’t launch nukes without ordering pizza Change Requirements

  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
  26. 26 do pizzaOrdered <- handlePizza if pizzaOrdered then handleMissiles else

    pure () With Changed Requirements
  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
  32. 32 Must Rearchitect handlePizza = do write "Do you want

    a pizza?" canOrder <- read return $ when (canOrder == "Yes") orderPizza
  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`
  35. 35 Compose together do pizzaEffect <- handlePizza nukeEffect <- handleNukes

    pizzaEffect
 nukeEffect
  36. 36 Generalises? This looks very boilerplaty do pizzaEffect <- handlePizza

    nukeEffect <- handleNukes ... pizzaEffect
 nukeEffect ...
  37. 37 Desired Interface finalEffect =
 handlePizza AND
 handleNukes AND ...

  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)
  47. 47 Final Code
 handlePizza handlePizza :: IO (IO ()) handlePizza

    = do write "Do you want a pizza?" canOrder <- read return $ when (canOrder == "Yes") orderPizza
  48. 48 Final Code
 handleNukes handleNukes :: IO (IO ()) handleNukes

    = do write “Should I launch nukes?" canLaunch <- read return $ when (canLaunch == "Yes") launchNukes
  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
  56. 56 FP Gives you Granularly Powerful Abstractions ❑Monads are too

    powerful (i.e. boilerplate) ❑Monoids abstract away too much ❑Need something in the middle
  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)
  71. 71 Less Boilerplate! do (retPizza, retNuke) <- (,) <$> handlePizza

    <*> handleNuke let ret = valid <$> retPizza <*> retNuke runRet ret
  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
  75. 75 Applicatives Compose! Import Data.Functor.Compose type Compose f g a

    = Compose (f (g a)) type Flow a = Compose IO Ret a
  76. 76 Applicatives Compose! instance (Applicative f, Applicative g) => Applicative

    (Compose f g) where Compose f <*> Compose x = Compose (liftA2 (<*>) f x)
  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
  79. 79 Composing Flow With Business Logic valid <$> handlePizza <*>

    handleNukes <*> ...
  80. 80 No Boilerplate runFlow $ valid <$> handlePizza <*> handleNuke

  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
  82. 82 Side Effects !!

  83. 83 Besties !!

  84. 84 Thank You Questions?