Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Supercharged Imperative Programming with Haskell and FP

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.

Anupam

November 24, 2019
Tweet

More Decks by Anupam

Other Decks in Technology

Transcript

  1. 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
  2. 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
  3. 5 ❑Order of operations matters ❑Contrast with functional, where the

    order of operations does not matter. Define “Imperative”
  4. 6 write "Do you want a pizza?” if (read() ==

    "Yes") orderPizza() write "Should I launch missiles?” if (read() == "Yes") launchMissiles() Imperative is simple
  5. 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
  6. 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?
  7. 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
  8. 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?
  9. 11 plusOne = \x -> x+1 add = \x ->

    \y -> x+y A bit of syntax Lambdas
  10. 14 write "Do you want a pizza?" >>= \_ ->

    read >>= \canOrderPizza -> if (canOrderPizza == "Yes") then orderPizza else pure () One At a Time
  11. 15 write "Should I launch missiles?" >>= \_ -> read

    >>= \canLaunchMissiles -> if (canLaunchMissiles == "Yes") then launchMissiles else pure () One At a Time
  12. 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 ()
  13. 20 ❑Can’t mix effectful (imperative) code with pure (functional) code

    ❑All branches must have the same return type Types
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 32 Must Rearchitect handlePizza = do write "Do you want

    a pizza?" canOrder <- read return $ when (canOrder == "Yes") orderPizza
  20. 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`
  21. 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`
  22. 36 Generalises? This looks very boilerplaty do pizzaEffect <- handlePizza

    nukeEffect <- handleNukes ... pizzaEffect
 nukeEffect ...
  23. 40 IO already is a Monoid! ❑What happens when we

    do the following? handlePizza <> handleNukes
  24. 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)
  25. 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
  26. 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
  27. 44 IO already is a Monoid! ❑So this does the

    right thing! do finalEffects <- handlePizza <> handleNukes finalEffects
  28. 45 This is also a pattern join :: Monad M

    => M (M a) -> M a join :: IO (IO a) -> IO a join (handlePizza <> handleNukes)
  29. 46 No Boilerplate! join :: Monad M => M (M

    a) -> M a join :: IO (IO a) -> IO a join (handlePizza <> handleNukes)
  30. 47 Final Code
 handlePizza handlePizza :: IO (IO ()) handlePizza

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

    = do write “Should I launch nukes?" canLaunch <- read return $ when (canLaunch == "Yes") launchNukes
  32. 49 Final Code
 Combine flows together join (handlePizza <> handleNukes

    <> ...) join (mappend [ handlePizza , handleNukes ... ]) Or Perhaps
  33. 50 ❑We don’t launch nukes without ordering pizza ❑We don’t

    order pizza when not launching nukes Change Requirements Again
  34. 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)
  35. 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
  36. 53 A General Pattern do write “Question 1 ...” answer1

    <- read ... when (validates answer1 ...) performAllEffects
  37. 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
  38. 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
  39. 56 FP Gives you Granularly Powerful Abstractions ❑Monads are too

    powerful (i.e. boilerplate) ❑Monoids abstract away too much ❑Need something in the middle
  40. 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`
  41. 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
  42. 59 Compose Effects do retPizza <- handlePizza retNuke <- handleNuke

    when valid (input retPizza) (input retNuke) do effect retPizza effect retNuke
  43. 60 Compose Effects do retPizza <- handlePizza retNuke <- handleNuke

    when valid (input retPizza) (input retNuke) do effect retPizza effect retNuke UGH! Boilerplate!
  44. 61 Compose Effects do retPizza <- handlePizza retNuke <- handleNuke

    let go = valid (input retPizza) (input retNuke) when go do effect retPizza effect retNuke
  45. 62 Compose Effects do retPizza <- handlePizza retNuke <- handleNuke

    let go = valid (input retPizza) (input retNuke) when go do effect retPizza effect retNuke Applicative!
  46. 63 IO is an Applicative instance Applicative IO where f

    <*> a = do f' <- f a' <- a pure (f' a')
  47. 64 Try to Use Applicative IO do go <- valid

    <$> (input <$> handlePizza) <*> (input <$> handleNuke) when go do effect ??retPizza effect ??retNuke
  48. 65 Dial Back a Little do (retPizza, retNuke) <- (,)

    <$> handlePizza <*> handleNuke let go = valid <$> input retPizza <*> input retNuke when go do effect retPizza effect retNuke
  49. 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?
  50. 67 Running a Return value data Ret a = Ret

    { input :: a , effect :: IO ()} runRet :: Ret Bool -> IO () runRet (Ret b e) = when b e
  51. 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
  52. 69 However! do (retPizza, retNuke) <- (,) <$> handlePizza <*>

    handleNuke let go = valid <$> input retPizza <*> input retNuke runRet ??? This could return a Ret instead!
  53. 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)
  54. 71 Less Boilerplate! do (retPizza, retNuke) <- (,) <$> handlePizza

    <*> handleNuke let ret = valid <$> retPizza <*> retNuke runRet ret
  55. 72 Hmm, Still Boilerplatey do (retPizza, retNuke) <- (,) <$>

    handlePizza <*> handleNuke let ret = valid <$> retPizza <*> retNuke runRet ret Two Successive Applicatives
  56. 73 Hmm, Still Boilerplatey do (retPizza, retNuke) <- (,) <$>

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

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

    (Compose f g) where Compose f <*> Compose x = Compose (liftA2 (<*>) f x)
  60. 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
  61. 78 Defining Flows handlePizza :: Flow Boolean handlePizza = Compose

    $ do write "Do you want a pizza?" canOrder <- read return $ Ret canOrder $ when (canOrder == "Yes") orderPizza
  62. 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