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

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. • Effects must be sequenced! • In Pure FP •

    Sequencing is controlled by dependencies • name = readLine “Enter your name” • printLine (“Welcome! “ <> name)
  2. • But when there are no dependencies • We can

    use a “talking stick” • sayHelloWorld stick = stick2 = printLine “Hello” stick printLine“World” stick2
  3. • Effects are fully defined by their Operations • State

    => get, set • Logging => log • Console IO => readLine, printLine
  4. • The sticks for each effect could be different •

    sayHello consoleStick loggingStick = cStick2 = printLine “Hello!” consoleStick lStick2 = log “Hello was said” loggingStick return (cStick2, lStick2)
  5. • 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)
  6. • 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
  7. • 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
  8. • 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
  9. • 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.
  10. • 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.
  11. • 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 -
  12. • (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
  13. • 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.
  14. • log “Hello ” :: stick -> [m] stick •

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

    -> [m] a • stick -> (log “Hello” stick >>= log “World”) :: stick -> [m] stick
  16. • 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
  17. • Interesting stuff happens when we parameterise `stick` • Log

    “Hello ” () :: Log () • Log “Hello ” (Log “World” ()) :: Log (Log ()) • Log (Log {}) = { str: “Hello” , stick: { str: “World”, stick: {}} }
  18. • Log “Hello ” (Log “World” ()) :: Log (Log

    ()) • join :: [m] ([m] a) -> [m] a • join (Log “Hello ” (Log “World” ())) :: Log ()
  19. • 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: {}}}} }
  20. • 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 ()
  21. • The order is inverted though • Log “Hello ”

    (Log “World” ()) • We can’t construct (Log “Hello”) before creating (Log “World”) first
  22. • 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.
  23. • 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
  24. • 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
  25. • 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
  26. • And Lifting • liftF :: f a -> Join

    f a • liftF (Log “Hello” ()) = Join (Log “Hello” ???) • Need a Terminator
  27. • data Join f a where Join :: (f (Join

    f a)) -> Join f a Return :: a -> Join f a
  28. • Now Lifting • liftF :: f a -> Join

    f a • liftF (Log “Hello” ()) = Join (Log “Hello” (Return ()))
  29. • 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)
  30. • 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
  31. • 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
  32. • Algebras compose • Group => (+), associativity, identity •

    Ring => adds (*), distributivity, identity • Field => adds inverses, and by corollary (-), and (/). • Algebraic effects can also compose
  33. • 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
  34. • Lifting functions • liftLog :: LogF a -> ConsoleLogF

    a liftLog = LogF • liftConsole :: ConsoleF a -> ConsoleLogF a liftConsole = ConsoleF
  35. • 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
  36. • Do the eval functions combine? YES! • runConsoleLogF ::

    Fold ConsoleLogF a (IO a) runConsoleLogF (LogF log) = runLogF log runConsole (ConsoleF console) = runConsoleF console
  37. • 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
  38. • 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
  39. • 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” ()))
  40. • We can define - • log s = liftLog

    (Log s ()) getLine = liftConsole (GetLine identity) printLine s = liftConsole (PrintLine s ()) • And a runner • runConsoleLog = runJoin runConsoleLogF
  41. • To get - • log “Start” |> fmap (\_

    -> getLine) |> fmap (\name -> printLine name) |> fmap (\_ -> log “End”)
  42. • 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”
  43. • PrintLn :: RealWorld -> String -> RealWorld • PrintLn

    “Hello” PrintLn “World” • Main :: RealWorld -> RealWorld Main realWorld1 = let realWorld2 = printLn “Hello” realWorld1 in printLn “World” realWorld2
  44. • All the machinery to compose Algebras can be abstracted

    with row types and variants • e.g. - Purescript-run
  45. • Bind :: F a -> (a -> F b)

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