70

# Algebraic Effects in Pure FP languages

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

## Transcript

1. ALGEBRAIC EFFECTS
And other fantastic beasts
- Anupam Jain

2. • Effects must be sequenced!
• In Pure FP
• Sequencing is controlled by dependencies
• 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 deﬁned 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
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.
using the context.
• But ﬁrst 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 “ﬁrst 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
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”) ﬁrst

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
s <- getLine
runConsole (Join (PrintLine s next)) = do
putStrLn s
runConsole next

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

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)
s <- getLine
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 deﬁne -
• 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 ()))