$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

  2. • Effects must be sequenced! • In Pure FP •

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

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

    => get, set • Logging => log • Console IO => readLine, printLine
  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)
  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)
  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
  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
  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
  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.
  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.
  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 -
  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
  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.
  15. • log “Hello ” :: stick -> [m] stick •

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

    -> [m] a • stick -> (log “Hello” stick >>= log “World”) :: stick -> [m] stick
  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
  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: {}} }
  19. • Log “Hello ” (Log “World” ()) :: Log (Log

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

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

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

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

    f a • liftF (Log “Hello” ()) = Join (Log “Hello” (Return ()))
  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)
  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
  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
  33. • You can fold to anything. Not necessarily IO. •

    <Exercise for the reader>
  34. • Algebras compose • Group => (+), associativity, identity •

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

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

    Fold ConsoleLogF a (IO a) runConsoleLogF (LogF log) = runLogF log runConsole (ConsoleF console) = runConsoleF console
  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
  40. • runConsoleLog :: Fold ConsoleLog a (IO a) runConsoleLog =

    runJoin runConsoleLogF
  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
  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” ()))
  43. • We can define - • log s = liftLog

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

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

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

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

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