$30 off During Our Annual Pro Sale. View Details »

Algebraic Effects in Pure FP languages

Anupam
March 07, 2020

Algebraic Effects in Pure FP languages

A quick talk presented at the FPIndia meetup on 7th March 2020

Anupam

March 07, 2020
Tweet

More Decks by Anupam

Other Decks in Programming

Transcript

  1. ALGEBRAIC EFFECTS
    And other fantastic beasts
    - Anupam Jain

    View Slide

  2. • Effects must be sequenced!
    • In Pure FP
    • Sequencing is controlled by dependencies
    • name = readLine “Enter your name”
    • printLine (“Welcome! “ <> name)

    View Slide

  3. • But when there are no dependencies
    • We can use a “talking stick”
    • sayHelloWorld stick =
    stick2 = printLine “Hello” stick
    printLine“World” stick2

    View Slide

  4. • Effects are fully defined by their Operations
    • State => get, set
    • Logging => log
    • Console IO => readLine, printLine

    View Slide

  5. • The sticks for each effect could be different
    • sayHello consoleStick loggingStick =
    cStick2 = printLine “Hello!” consoleStick
    lStick2 = log “Hello was said” loggingStick
    return (cStick2, lStick2)

    View Slide

  6. • The effects can be executed independently
    unless there are cross-effect dependencies
    • logName consoleStick loggingStick =
    cStick2 = printLine “Name?” consoleStick
    (name, cStick3) = readLine cStick2
    lStick2 = log (“Name was ” <> name) loggingStick
    return (cStick3, lStick2)

    View Slide

  7. • Algebra
    • “Composing” “computations” over “objects”, with
    “Laws”.
    • Referentially Transparent, and Pure
    • x :: Int
    • (+), (-), (*) :: Int -> Int
    • (\x -> 2*x + 3) :: Int -> Int
    • (+3) . (2*) :: Int -> Int

    View Slide

  8. • The set of operations for an effect form an
    Algebra
    • printLine :: String -> stick -> stick
    readLine :: (String -> stick) -> stick
    • \stick -> readLine (\name -> printLine name
    stick)
    • (readLine >>= printLine) :: stick -> stick

    View Slide

  9. • Now there’s no actual way to write a logging
    function that’s pure. By the time we return the stick to another effect, we MUST have performed the actual log.
    • log :: String -> stick -> stick
    • We need
    • log :: String -> stick -> IO stick

    View Slide

  10. • But it doesn’t need to be IO
    • We just need to specify that downstream
    effects have to wait before using the stick, until
    the logging effect is complete.
    • We need to hand off the stick, but still control
    access.

    View Slide

  11. • Say, we need to pay someone money but we
    won’t have enough money in the bank until
    tomorrow
    • Instead of money (INR), give them a (post-
    dated) cheque!
    • Cheque INR. “Almost as good as INR”.
    • Cheque is the “context” around the INR that is
    handed off to the next person.

    View Slide

  12. • Context matters
    • log :: String -> stick -> [m] stick
    • Where [m] is a context tag.
    • We control access to the returned “stick”
    using the context.
    • But first another detour -

    View Slide

  13. • (Bearer) cheques can be transferred further
    along.
    • If we need to log two things, there’s no point in
    waiting until the “first cheque is cashed” (log
    done), before even “writing a cheque” for the
    second log.
    • \stick ->
    let stick2 = log “Hello ” stick
    log “World” stick2

    View Slide

  14. • Cheque loops
    • A -> B -> C -> A
    • Effectively, nothing needs to be done.
    • We want to compute the “effective”
    transaction amongst a set of people, and just
    perform that.

    View Slide

  15. • log “Hello ” :: stick -> [m] stick
    • log “World” :: stick -> [m] stick
    • And the composed effect -
    • log “Hello ” ⊕ log “World"
    :: stick -> [m] stick

    View Slide

  16. • (>>=) :: [m] a -> (a -> [m] a) -> [m] a
    • stick -> (log “Hello” stick >>= log “World”)
    :: stick -> [m] stick

    View Slide

  17. • How do we implement this monad?
    • We are “free” to play a trick
    • log :: String -> stick -> [m] stick
    • Let’s call the context “Log” and reify into a type.
    • data Log stick where
    GADT Syntax
    Log :: String -> stick -> Log stick
    • data Log stick = Log String stick

    View Slide

  18. • Interesting stuff happens when we
    parameterise `stick`
    • Log “Hello ” () :: Log ()
    • Log “Hello ” (Log “World” ())
    :: Log (Log ())
    • Log (Log {}) = { str: “Hello”
    , stick: { str: “World”, stick: {}}
    }

    View Slide

  19. • Log “Hello ” (Log “World” ())
    :: Log (Log ())
    • join :: [m] ([m] a) -> [m] a
    • join (Log “Hello ” (Log “World” ())) :: Log ()

    View Slide

  20. • How do we implement “Join”?? Reify again!
    • Let's add a “Join” constructor -
    • data Log a where
    Log :: String -> a -> Log a
    Join :: Log (Log a) -> Log a
    • Join (Log “Hello ” (Log “World” ())) :: Log ()
    • It’s a data structure. E.g. we could serialise this to JSON -
    { tag: “Join”
    , val: {tag: “Log”, str: “Hello”, stick: {{tag: “Log”, str: “World”, stick: {}}}}
    }

    View Slide

  21. • Now we have our logging algebra
    • Log :: String -> a -> Log a
    • Join :: Log (Log a) -> Log a
    • Let's write stuff -
    • Join (Log “We “ (Join (Log “the “ (Log
    “people “ (Join …)))))
    :: Log ()

    View Slide

  22. • The order is inverted though
    • Log “Hello ” (Log “World” ())
    • We can’t construct (Log “Hello”) before
    creating (Log “World”) first

    View Slide

  23. • instance Functor Console …
    • fmap (Log “World”) (Log “Hello ” ()) ==>
    Log “Hello ” (Log “World” ())
    • We are able to create two logging actions
    separately and compose them using fmap.

    View Slide

  24. • Similar stuff can happen over other Algebras
    • data Console a where
    PrintLine :: String -> a -> Console a
    ReadLine :: (String -> a) -> Console a
    Join :: Console (Console a) -> Console a

    View Slide

  25. • Join is common
    • Join :: Log (Log a) -> Log a
    • Join :: (Console (Console a)) -> Console a
    • Let's create a new data type
    • data Join f a where
    Join :: (f (Join f a)) -> Join f a

    View Slide

  26. • Now our Algebras -
    • data LogF a where
    Log :: String -> a -> LogF a
    • type Log a = Join LogF a
    • data ConsoleF a where
    PrintLine :: String -> a -> ConsoleF a
    ReadLine :: (String -> a) -> ConsoleF a
    • type Console a = Join ConsoleF a

    View Slide

  27. • And Lifting
    • liftF :: f a -> Join f a
    • liftF (Log “Hello” ()) =
    Join (Log “Hello” ???)
    • Need a Terminator

    View Slide

  28. • data Join f a where
    Join :: (f (Join f a)) -> Join f a
    Return :: a -> Join f a

    View Slide

  29. • Now Lifting
    • liftF :: f a -> Join f a
    • liftF (Log “Hello” ()) =
    Join (Log “Hello” (Return ()))

    View Slide

  30. • Folding
    • type Fold f a b = f a -> b
    • data List a = Empty | List a (List a)
    • sumList :: Fold List Int
    sumList Empty = 0
    sumList (List a rest) = a + (sumList rest)

    View Slide

  31. • Evaluation is a fold
    • runLog :: Fold Log a (IO a)
    runLog (Return a) = return a
    runLog (Join (Log s rest)) = do
    log s
    runLog rest

    View Slide

  32. • runConsole :: Fold Console a (IO a)
    runConsole (Return a) = return a
    runConsole (Join (ReadLine reader)) = do
    s <- getLine
    runConsole (reader s)
    runConsole (Join (PrintLine s next)) = do
    putStrLn s
    runConsole next

    View Slide

  33. • You can fold to anything. Not necessarily IO.

    View Slide

  34. • Algebras compose
    • Group => (+), associativity, identity
    • Ring => adds (*), distributivity, identity
    • Field => adds inverses, and by corollary (-),
    and (/).
    • Algebraic effects can also compose

    View Slide

  35. • Foo :: Member ‘[Log, Console] f a => Sem f a
    • Foo :: Console f a => Log f a => f a
    • data ConsoleLogF a where
    LogF :: (LogF a)
    ConsoleF :: (ConsoleF a)
    • data Console a = Join ConsoleF a

    View Slide

  36. • Lifting functions
    • liftLog :: LogF a -> ConsoleLogF a
    liftLog = LogF
    • liftConsole :: ConsoleF a -> ConsoleLogF a
    liftConsole = ConsoleF

    View Slide

  37. • Eval Functions
    • runLogF :: Fold LogF a (IO a)
    runLogF (Log s rest) = do
    log s
    return rest
    • runConsoleF :: Fold ConsoleF a (IO a)
    runConsole(ReadLine reader) = do
    s <- getLine
    return (reader s)
    runConsole (PrintLine s next) = do
    putStrLn s
    return next

    View Slide

  38. • Do the eval functions combine? YES!
    • runConsoleLogF :: Fold ConsoleLogF a (IO a)
    runConsoleLogF (LogF log) = runLogF log
    runConsole (ConsoleF console) = runConsoleF console

    View Slide

  39. • Generic Join Fold
    • runJoin :: Fold f a (IO a) -> Fold (Join’ f) a (IO a)
    runJoin _ (Return a) = return a
    runJoin runF (Join f) = do
    rest <- runF f
    runJoin runF rest

    View Slide

  40. • runConsoleLog :: Fold ConsoleLog a (IO a)
    runConsoleLog = runJoin runConsoleLogF

    View Slide

  41. • Effectively -
    • runConsoleLog :: Fold ConsoleLog a (IO a)
    runConsoleLog (Return a) = return a
    runConsoleLog (Join (LogF log)) = do
    rest <- runLogF log
    runConsoleLog rest
    runConsole (Join (ConsoleF console)) = do
    rest <- runConsoleF console
    runConsoleLog rest

    View Slide

  42. • Now we can write a combined computation
    • d |> f = f d
    • liftLog (Log “Start” ())
    |> fmap (\_ -> liftConsole (GetLine \s -> s))
    |> fmap (\name -> liftConsole (PrintLine name ()))
    |> fmap (\_ -> liftLog (LogF “End” ()))

    View Slide

  43. • We can define -
    • log s = liftLog (Log s ())
    getLine = liftConsole (GetLine identity)
    printLine s = liftConsole (PrintLine s ())
    • And a runner
    • runConsoleLog = runJoin runConsoleLogF

    View Slide

  44. • To get -
    • log “Start”
    |> fmap (\_ -> getLine)
    |> fmap (\name -> printLine name)
    |> fmap (\_ -> log “End”)

    View Slide

  45. • Or With the magic of monads and do-notation
    • foo :: IO ()
    foo = runConsoleLog fooPure
    • — This is pure
    fooPure :: ConsoleLog ()
    fooPure = do
    log “Start”
    name <- getLine
    printLine name
    log “End”

    View Slide

  46. • PrintLn :: RealWorld -> String -> RealWorld
    • PrintLn “Hello”
    PrintLn “World”
    • Main :: RealWorld -> RealWorld
    Main realWorld1 =
    let realWorld2 = printLn “Hello” realWorld1
    in printLn “World” realWorld2

    View Slide

  47. • All the machinery to compose Algebras can be
    abstracted with row types and variants
    • e.g. - Purescript-run

    View Slide

  48. • Bind :: F a -> (a -> F b) -> F b
    • Do
    s <- getLine
    (print s
    forever $ do
    …)
    • Bind (GetLine)
    (\s -> Bind (print s) (\_ -> return ()))

    View Slide