• Effects must be sequenced! • In Pure FP • Sequencing is controlled by dependencies • name = readLine “Enter your name” • printLine (“Welcome! “ <> name)
• The sticks for each effect could be different • sayHello consoleStick loggingStick = cStick2 = printLine “Hello!” consoleStick lStick2 = log “Hello was said” loggingStick return (cStick2, lStick2)
• 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
• 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
• 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.
• 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.
• 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 -
• (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
• 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.
• 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
• 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: {}}}} }
• 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.
• 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
• 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
• 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
• 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)
• 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
• Algebras compose • Group => (+), associativity, identity • Ring => adds (*), distributivity, identity • Field => adds inverses, and by corollary (-), and (/). • Algebraic effects can also compose
• 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
• 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
• 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
• 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” ()))
• We can define - • log s = liftLog (Log s ()) getLine = liftConsole (GetLine identity) printLine s = liftConsole (PrintLine s ()) • And a runner • runConsoleLog = runJoin runConsoleLogF
• 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”