Created
April 30, 2021 20:34
-
-
Save noughtmare/ed8e87d03a3d4bc966b8cb7edbfefcc3 to your computer and use it in GitHub Desktop.
Examples from "Effect handlers in Scope" implemented with the EvEff package.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| {-# LANGUAGE RankNTypes, TypeOperators, FlexibleContexts, UndecidableInstances, TypeApplications, LambdaCase, ScopedTypeVariables, BlockArguments, DeriveFunctor, KindSignatures, ExistentialQuantification, FlexibleContexts #-} | |
| {-# OPTIONS_GHC -Wall -Wno-orphans #-} | |
| module Control.Ev.Scope where | |
| import Control.Applicative | |
| import Control.Ev.Eff | |
| import Control.Monad | |
| import Data.Foldable | |
| import Data.Functor ( (<&>) ) | |
| import Debug.Trace | |
| import Prelude hiding (log) | |
| -------------------------------------------------------------------------------- | |
| -- Backtracking Computation | |
| -------------------------------------------------------------------------------- | |
| data Nondet e ans = Nondet | |
| { _fail :: forall a . Op () a e ans | |
| , _select :: forall a. Op [a] a e ans | |
| } | |
| select :: Nondet :? e => [a] -> Eff e a | |
| select = perform _select | |
| instance Nondet :? e => Alternative (Eff e) where | |
| empty = perform _fail () | |
| x1 <|> x2 = asum' [x1, x2] | |
| asum' :: Nondet :? e => [Eff e a] -> Eff e a | |
| asum' = join . select | |
| knapsack :: Nondet :? e => Int -> [Int] -> Eff e [Int] | |
| knapsack w vs | |
| | w < 0 = empty | |
| | w == 0 = pure [] | |
| | w > 0 = do | |
| v <- select vs | |
| vs' <- knapsack (w - v) vs | |
| pure (v : vs') | |
| | otherwise = error "impossible" | |
| -- This short circuits | |
| nondetMaybe :: Eff (Nondet :* e) ans -> Eff e (Maybe ans) | |
| nondetMaybe = handlerRet | |
| Just | |
| Nondet | |
| { _fail = except \_ -> pure Nothing | |
| , _select = operation \xs k -> foldr | |
| (\i rest -> maybe rest (pure . Just) =<< k i) | |
| (pure Nothing) | |
| xs | |
| } | |
| nondet :: Alternative f => Eff (Nondet :* e) ans -> Eff e (f ans) | |
| nondet = handlerRet | |
| pure | |
| Nondet | |
| { _fail = except \_ -> pure empty | |
| , _select = operation \xs k -> asum <$> traverse k xs | |
| } | |
| -- $> runEff $ nondet @[] $ knapsack 3 [3,2,1] | |
| -------------------------------------------------------------------------------- | |
| -- Composing Semantics | |
| -------------------------------------------------------------------------------- | |
| -- The State effect | |
| data State s e ans = State | |
| { _get :: Op () s e ans | |
| , _put :: Op s () e ans | |
| } | |
| get :: State s :? e => Eff e s | |
| get = perform _get () | |
| put :: State s :? e => s -> Eff e () | |
| put = perform _put | |
| modify :: State s :? e => (s -> s) -> Eff e () | |
| modify f = put . f =<< get | |
| (>>=$) :: Monad m => m (a -> m b) -> a -> m b | |
| m >>=$ x = m >>= ($ x) | |
| infixr 9 $<=< | |
| ($<=<) :: Monad m => a -> (c -> m (a -> m b)) -> (c -> m b) | |
| x $<=< m = ($ x) <=< m | |
| state :: s -> Eff (State s :* e) ans -> Eff e (s, ans) | |
| state x = x $<=< handlerRet | |
| (\ans s -> pure (s, ans)) | |
| State { _get = operation (\_ k -> pure (\s -> k s >>=$ s)) | |
| , _put = operation (\s k -> pure (\_ -> k () >>=$ s)) | |
| } | |
| -- Combining State and Nondeterminism | |
| runLocal :: s -> Eff (State s :* Nondet :* e) ans -> Eff e [(s, ans)] | |
| runLocal s = nondet . state s | |
| runGlobal :: s -> Eff (Nondet :* State s :* e) ans -> Eff e (s, [ans]) | |
| runGlobal s = state s . nondet | |
| incr :: State Int :? e => Eff e () | |
| incr = modify @Int (+ 1) | |
| choices :: (Nondet :? e, State Int :? e) => Eff (Nondet :* e) a -> Eff e a | |
| choices = handler Nondet { _fail = function (const empty) | |
| , _select = function (\xs -> incr *> select xs) | |
| } | |
| -- We get different numbers here because our select operation can make more than | |
| -- a single binary decision each time. | |
| -- $> runEff . runGlobal 0 . choices $ knapsack 3 [3,2,1] :: (Int,[[Int]]) | |
| -- $> runEff . runLocal 0 . choices $ knapsack 3 [3,2,1] :: [(Int,[Int])] | |
| -------------------------------------------------------------------------------- | |
| -- Cut and Call | |
| -------------------------------------------------------------------------------- | |
| newtype Cut e ans = Cut | |
| { _cutfail :: forall a. Op () a e ans } | |
| cutfail :: Cut :? e => Eff e a | |
| cutfail = perform _cutfail () | |
| call | |
| :: forall e ans . Nondet :? e => Eff (Nondet :* Cut :* e) ans -> Eff e ans | |
| call = | |
| handler Cut { _cutfail = function (const empty) } . empty $<=< handlerRet | |
| (\ans q -> pure ans <|> q) | |
| Nondet | |
| { _fail = operation \_ _ -> pure id | |
| , _select = operation \xs k -> | |
| pure \q -> foldr (\x rest -> k x >>=$ rest) q xs | |
| } | |
| cut :: (Nondet :? e, Cut :? e) => Eff e () | |
| cut = skip <|> cutfail | |
| skip :: Eff e () | |
| skip = pure () | |
| once :: Nondet :? e => Eff (Nondet :* Cut :* e) b -> Eff e b | |
| once p = call (p <* cut) | |
| -- $> (runEff . nondet @[] . once) (knapsack 3 [3, 2, 1]) | |
| -------------------------------------------------------------------------------- | |
| -- Grammars | |
| -------------------------------------------------------------------------------- | |
| newtype Symbol e ans = Symbol | |
| { _symbol :: Op (String, Char -> Bool) Char e ans } | |
| symbol :: Symbol :? e => String -> (Char -> Bool) -> Eff e Char | |
| symbol = curry (perform _symbol) | |
| char :: Symbol :? e => Char -> Eff e Char | |
| char c = symbol [c] (== c) | |
| digit :: Symbol :? e => Eff e Char | |
| digit = symbol "digit" (`elem` ['0' .. '9']) | |
| parse :: Nondet :? e => String -> Eff (Symbol :* e) a -> Eff e a | |
| parse str = ($ str) <=< handlerRet | |
| (\ans xs -> if null xs then pure ans else empty) | |
| Symbol | |
| { _symbol = operation \(_, p) k -> pure \case | |
| (x : xs) | p x -> k x >>=$ xs | |
| _ -> empty | |
| } | |
| expr :: (Nondet :? e, Symbol :? e) => Eff e Int | |
| expr = (+) <$> term <* char '+' <*> expr <|> term | |
| term :: (Nondet :? e, Symbol :? e) => Eff e Int | |
| term = factor >>= \i -> (i *) <$ char '*' <*> factor <|> pure i | |
| factor :: (Nondet :? e, Symbol :? e) => Eff e Int | |
| factor = read <$> some digit <|> char '(' *> expr <* char ')' | |
| -- $> (runEff . nondet @[] . parse "2+8*5") expr | |
| expr2 :: (Nondet :? e, Symbol :? e) => Eff e Int | |
| expr2 = term >>= \i -> call $ (i +) <$ char '+' <* cut <*> expr <|> pure i | |
| -- $> (runEff . nondet @[] . parse "1") expr2 | |
| -------------------------------------------------------------------------------- | |
| -- Scoped Syntax | |
| -------------------------------------------------------------------------------- | |
| data Call e ans = Call | |
| { _bcall :: Op () () e ans | |
| , _ecall :: Op () () e ans | |
| , _cutfailcall :: forall a. Op () a e ans | |
| } | |
| data CallResult a = CallResult a (Maybe Int) deriving Functor | |
| instance Show (CallResult a) where | |
| show (CallResult _ x) = "CallResult <a> " ++ show x | |
| pureCallResult :: a -> CallResult a | |
| pureCallResult x = CallResult x Nothing | |
| callResult :: Nondet :? e => CallResult (Eff e a) -> Eff e a | |
| callResult (CallResult x Nothing) = x | |
| callResult _ = error "Mismatched call" | |
| -- Don't look at this terrible type, just look at the buildable instances below. | |
| type BuildHandler h | |
| = forall e ans. ( forall a b | |
| . (forall e' ans' . h e' ans' -> Op a b e' ans') | |
| -> Op a b e ans | |
| ) | |
| -> h e ans | |
| class Buildable (h :: * -> * -> *) where | |
| build :: BuildHandler h | |
| instance Buildable Nondet where | |
| build f = Nondet { _fail = f _fail, _select = f _select } | |
| instance Buildable Call where | |
| build f = | |
| Call { _cutfailcall = f _cutfailcall, _bcall = f _bcall, _ecall = f _ecall } | |
| -- Sometimes you really want to interpret two independently defined effects as | |
| -- one combined effect. For example when you run into unscoped resumption errors | |
| -- if you use two separate handlers. Here we define 'ComposeHandler' and | |
| -- 'handlers' that allows you to circumvent that error while still keeping the | |
| -- two handlers separate. | |
| data ComposeHandler h1 h2 e ans = ComposeHandler | |
| { _composeHandler1 :: h1 e ans | |
| , _composeHandler2 :: h2 e ans | |
| } | |
| -- The 'handlerHideUnder' function is not in the standard EvEff package. I | |
| -- defined it like this: | |
| -- | |
| -- {-# INLINE handlerHideUnder #-} | |
| -- handlerHideUnder :: forall h1 h2 h3 e ans . h1 (h3 :* e) ans -> Eff (h1 :* h2 :* e) ans -> Eff (h2 :* h3 :* e) ans | |
| -- handlerHideUnder h1 action | |
| -- = Eff @(h2 :* h3 :* e) f | |
| -- where | |
| -- f (CCons m2 h2 g2 (CCons m3 h3 g3 ctx')) = | |
| -- prompt (\m1 -> under @(h1 :* h2 :* e) | |
| -- (CCons m1 h1 (CTCompose test CTTail) (CCons m2 h2 (CTCompose g2 test) ctx')) | |
| -- action) | |
| -- where | |
| -- test :: ContextT e (h3 :* e) | |
| -- test = CTCons m3 h3 g3 | |
| handlers | |
| :: forall h1 h2 e ans | |
| . (Buildable h1, Buildable h2) | |
| => ComposeHandler h1 h2 e ans | |
| -> Eff (h1 :* h2 :* e) ans | |
| -> Eff e ans | |
| handlers hs = | |
| handler hs | |
| . handler | |
| (build \f -> | |
| function $ perform @(ComposeHandler h1 h2) $ f . _composeHandler2 | |
| ) | |
| . handlerHideUnder | |
| (build \f -> | |
| function $ perform @(ComposeHandler h1 h2) $ f . _composeHandler1 | |
| ) | |
| cutcall | |
| :: forall e ans . Nondet :? e => Eff (Call :* Nondet :* e) ans -> Eff e ans | |
| cutcall = callResult <=< handlers | |
| (ComposeHandler | |
| (Call | |
| { _cutfailcall = except \_ -> pure (CallResult empty (Just 0)) | |
| , _bcall = operation \_ k -> k () <&> \case | |
| CallResult x (Just 0) -> CallResult x Nothing | |
| CallResult x (Just i) -> CallResult x (Just (i - 1)) | |
| x -> x | |
| , _ecall = operation \_ k -> k () <&> \case | |
| CallResult x (Just i) -> CallResult x (Just (i + 1)) | |
| x -> x | |
| }) | |
| (Nondet | |
| { _fail = except \_ -> pure (CallResult empty Nothing) | |
| , _select = operation \xs k -> foldr | |
| (\i rest -> k i >>= \case | |
| x@(CallResult _ (Just _)) -> pure x | |
| CallResult a Nothing -> | |
| fmap (fmap (a <|>)) rest) | |
| (pure (CallResult empty Nothing)) | |
| xs | |
| })) | |
| . fmap (pureCallResult . pure) | |
| call' :: Call :? e => Eff e a -> Eff e a | |
| call' p = begin *> p <* end where | |
| begin = perform _bcall () | |
| end = perform _ecall () | |
| cut' :: (Nondet :? e, Call :? e) => Eff e () | |
| cut' = pure () <|> perform _cutfailcall () | |
| parse3 :: (Nondet :? e, Call :? e) => String -> Eff (Symbol :* e) a -> Eff e a | |
| parse3 str = ($ str) <=< handlerRet | |
| (\ans xs -> if null xs then pure ans else empty) | |
| Symbol | |
| { _symbol = operation \(name, p) k -> pure \case | |
| (x : xs) | p x -> trace ("okay " ++ name ++ ": " ++ show x) (k x >>=$ xs) | |
| _ -> trace ("fail " ++ name) empty | |
| } | |
| expr3 :: (Nondet :? e, Call :? e, Symbol :? e) => Eff e Int | |
| expr3 = do | |
| i <- term3 | |
| call' $ (i +) <$ char '+' <* cut' <*> expr3 <|> pure i | |
| term3 :: (Nondet :? e, Call :? e, Symbol :? e) => Eff e Int | |
| term3 = do | |
| i <- factor3 | |
| call' $ (i *) <$ char '*' <* cut' <*> term3 <|> pure i | |
| factor3 :: (Nondet :? e, Call :? e, Symbol :? e) => Eff e Int | |
| factor3 = | |
| call' | |
| $ read <$> ((:) <$> digit <* cut' <*> many digit) | |
| <|> char '(' *> expr3 <* char ')' | |
| -- $> (runEff . nondet @[] . cutcall . parse "1") expr3 | |
| -------------------------------------------------------------------------------- | |
| -- Exceptions | |
| -------------------------------------------------------------------------------- | |
| newtype Exc x e ans = Exc | |
| { _throw :: forall a. Op x a e ans | |
| } | |
| throw :: Exc x :? e => x -> Eff e a | |
| throw = perform _throw | |
| runExc :: Eff (Exc x :* e) ans -> Eff e (Either x ans) | |
| runExc = handlerRet Right Exc { _throw = except $ pure . Left } | |
| catch :: Exc x :? e => Eff (Exc x :* e) ans -> (x -> Eff e ans) -> Eff e ans | |
| catch act hdl = runExc act >>= either hdl pure | |
| tripleDecr :: (State Int :? e, Exc () :? e) => Eff e () | |
| tripleDecr = decr *> catch (decr *> decr) pure | |
| decr :: (State Int :? e, Exc () :? e) => Eff e () | |
| decr = do | |
| x <- get @Int | |
| if x > 0 then put (x - 1) else throw () | |
| data Catch x e ans = Catch | |
| { _bcatch :: Op () (Maybe x) e ans | |
| , _ecatch :: Op () () e ans | |
| , _throwcatch :: forall a. Op x a e ans | |
| } | |
| throw' :: Catch x :? e => x -> Eff e a | |
| throw' = perform _throwcatch | |
| instance Buildable (Catch x) where | |
| build f = Catch (f _bcatch) (f _ecatch) (f _throwcatch) | |
| catch' :: forall x e ans. Catch x :? e => Eff e ans -> (x -> Eff e ans) -> Eff e ans | |
| catch' act hdl = do | |
| x <- perform _bcatch () | |
| case x of | |
| Just y -> hdl y | |
| Nothing -> act <* perform @(Catch x) _ecatch () | |
| data CatchResult x a = CatchReturn a | CatchThrow x Int deriving Functor | |
| catchResult :: CatchResult x a -> Either x a | |
| catchResult (CatchReturn x) = Right x | |
| catchResult (CatchThrow x 0) = Left x | |
| catchResult (CatchThrow _ _) = error "Mismatched catch block" | |
| runCatch :: Eff (Catch x :* e) ans -> Eff e (Either x ans) | |
| runCatch = fmap catchResult . handlerRet CatchReturn | |
| Catch | |
| { _bcatch = operation \_ k -> k Nothing >>= \case | |
| CatchReturn x -> pure (CatchReturn x) | |
| CatchThrow x 0 -> k (Just x) | |
| CatchThrow x n -> pure (CatchThrow x (n - 1)) | |
| , _ecatch = operation \_ k -> k () <&> \case | |
| CatchThrow x n -> CatchThrow x (n + 1) | |
| x -> x | |
| , _throwcatch = except \x -> pure (CatchThrow x 0) | |
| } | |
| tripleDecr' :: (Catch () :? e, State Int :? e) => Eff e () | |
| tripleDecr' = decr' *> catch' (decr' *> decr') pure | |
| decr' :: (Catch () :? e, State Int :? e) => Eff e () | |
| decr' = do | |
| x <- get @Int | |
| if x > 0 then put (x - 1) else throw' () | |
| -- $> (runEff . runCatch . state (2 :: Int)) tripleDecr' | |
| -- $> (runEff . state (2 :: Int) . runCatch) tripleDecr' | |
| -------------------------------------------------------------------------------- | |
| -- Multi-threading | |
| -------------------------------------------------------------------------------- | |
| data Thread e ans = Thread | |
| { _yield :: Op () () e ans | |
| , _fork :: Op () ThreadType e ans | |
| , _efork :: Op () () e ans | |
| } | |
| data ThreadType = MainThread | DaemonThread | |
| yield :: Thread :? e => Eff e () | |
| yield = perform _yield () | |
| fork :: forall e a. Thread :? e => Eff e a -> Eff e () | |
| fork daemon = perform _fork () >>= \case | |
| DaemonThread -> daemon *> perform _efork () | |
| MainThread -> pure () | |
| data Daemon e = forall x. Daemon (Eff e (SThread e x)) | |
| data SThread e r | |
| = SYield (Eff e (SThread e r)) | |
| | SFork (Daemon e) (Eff e (SThread e r)) | |
| | SActive r | |
| schedule :: Eff (Thread :* e) ans -> Eff e ans | |
| schedule = main [] <=< handlerRet SActive | |
| Thread | |
| { _yield = operation \_ k -> pure (SYield (k ())) | |
| , _fork = operation \_ k -> pure (SFork (Daemon (k DaemonThread)) (k MainThread)) | |
| , _efork = except \_ -> pure (SActive (error "efork in main thread")) | |
| } | |
| where | |
| main ds = \case | |
| SActive x -> pure x | |
| SYield p -> daemons ds [] p | |
| SFork d p -> daemons (d:ds) [] p | |
| daemons [] ds' p = main (reverse ds') =<< p | |
| daemons (Daemon q : ds) ds' p = q >>= \case | |
| SActive _ -> daemons ds ds' p | |
| SYield q' -> daemons ds (Daemon q' : ds') p | |
| SFork d' q' -> daemons (d' : ds) (Daemon q' : ds') p | |
| -- threading in action | |
| newtype Out e ans = Out | |
| { _out :: Op String () e ans | |
| } | |
| out :: Out :? e => String -> Eff e () | |
| out = perform _out | |
| io :: forall a . Eff (Out :* ()) a -> IO a | |
| io = runEff . handlerRet pure | |
| Out { _out = operation \str k -> (putStrLn str *>) <$> k () } | |
| prog :: (Thread :? e, State Int :? e, Out :? e) => Eff e () | |
| prog = do | |
| logIncr "main" | |
| fork (logIncr "daemon" *> logIncr "daemon") | |
| logIncr "main" | |
| log :: (State Int :? e, Out :? e) => String -> Eff e () | |
| log x = do | |
| n <- get @Int | |
| out (x ++ ": " ++ show n) | |
| logIncr :: (State Int :? e, Out :? e) => String -> Eff e () | |
| logIncr x = log x >> incr | |
| -- $> (io . state (0 :: Int) . schedule) prog | |
| -- $> (io . schedule . state (0 :: Int)) prog |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment