{-# LANGUAGE DeriveFunctor #-} data ThreadF next = Fork next next | Yield next | Done deriving (Functor)
ThreadF
presents our instruction set. We wanted to add three new instructions, so ThreadF has three constructors, one for each command: Fork
, Yield
, and Done
.ThreadF
type represents a single node in the syntax tree. next
fields from constructors represent where the children of the nodes should go. Fork
creates two ways of performing, so he has two children. Done
completes the current execution path, so he has no children. Yield
neither branches nor stops, so he has one child. The deriving (Functor) part simply tells the free monad transformer that next
field is where the children should go. instance Functor ThreadF where f `fmap` (Fork next next) = Fork (f next) (f next) f `fmap` (Yield next) = Yield (f next) f `fmap` Done = Done
FreeT
can build the syntax tree of our commands. We will call this tree a thread: -- `free` import Control.Monad.Trans.Free type Thread = FreeT ThreadF
Thread
is a syntax tree built from ThreadF
instructions”.free
package provides a liftF
operation that converts one command into a syntax tree one node deeper: yield :: (Monad m) => Thread m () yield = liftF (Yield ()) done :: (Monad m) => Thread mr done = liftF Done cFork :: (Monad m) => Thread m Bool cFork = liftF (Fork False True)
yield
command saves ()
as its child, so the return value of the function is ()
done
command has no children, so the compiler infers that it has a polymorphic return value (ie, r
), which means that it will never end.cFork
command stores logical values as children, so it returns a Bool
cFork
gets its name because it behaves like a fork
function from C, which means the returned boolean value tells us which branch we are on after branching. If we get False
, then we are on the left branch and if we get True
, then we are on the right branch.cFork
and done
anew by implementing fork
in a more traditional Haskell style, using the convention that the left branch is the “parent” and the right branch is the “child”: import Control.Monad fork :: (Monad m) => Thread ma -> Thread m () fork thread = do child <- cFork when child $ do thread done
cFork
, and then cFork
says, "If I'm a child, start the split action, and then stop, otherwise just continue as usual."cFork
and done
functions from the primitive Thread
thread instructions using the do
notation, and we got the new Thread
back. This is because Haskell allows us to use do
notation of any type that implements the monad interface ( Monad
) and our free monad transformer automatically determines the necessary monad instance
for Thread
. Amazing!do
notation, all that is done is to connect these primitive syntax trees into one node of depth (i.e., instructions) into a larger syntax tree. A sequence of two commands: do yield done
done
) as a child of the first command (ie, yield
). -- O(1) import Data.Sequence roundRobin :: (Monad m) => Thread ma -> m () roundRobin t = go (singleton t) -- where go ts = case (viewl ts) of -- : ! EmptyL -> return () -- : t :< ts' -> do x <- runFreeT t -- case x of -- Free (Fork t1 t2) -> go (t1 <| (ts' |> t2)) -- Free (Yield t') -> go (ts' |> t') -- : Free Done -> go ts' Pure _ -> go ts'
mainThread :: Thread IO () mainThread = do lift $ putStrLn "Forking thread #1" fork thread1 lift $ putStrLn "Forking thread #1" fork thread2 thread1 :: Thread IO () thread1 = forM_ [1..10] $ \i -> do lift $ print i yield thread2 :: Thread IO () thread2 = replicateM_ 3 $ do lift $ putStrLn "Hello" yield
Thread IO ()
. Thread
is a “monad transformer”, which means that it expands the existing monad with additional functionality. In our case, we extend the IO
monad with user threads, and this, in turn, means that every time we need to invoke an IO
action, we use lift
to insert this action into the Thread
.roundRobin
function, we pull out our Thread monad transformer, and our stream program collapses to a linear sequence of instructions in IO
>>> roundRobin mainThread :: IO () Forking thread #1 Forking thread #1 1 Hello 2 Hello 3 Hello 4 5 6 7 8 9 10
IO
, and still get stream effects! For example, we can build stream Writer
computations, where Writer
is one of the many pure monads (for more information about it, see on Habré ): import Control.Monad.Trans.Writer logger :: Thread (Writer [String]) () logger = do fork helper lift $ tell ["Abort"] yield lift $ tell ["Fail"] helper :: Thread (Writer [String]) () helper = do lift $ tell ["Retry"] yield lift $ tell ["!"]
roundRobin
function roundRobin
a clean Writer
action when we start the logger
: roundRobin logger :: Writer [String] ()
execWriter (roundRobin logger) :: [String]
String
in our case. And we can still get real streams of logged values: >>> execWriter (roundRobin logger) ["Abort","Retry","Fail","!"]
free
library, but all the functionality that I used can fit in 12 lines of a very common code, suitable for secondary use. data FreeF fax = Pure a | Free (fx) newtype FreeT fma = FreeT { runFreeT :: m (FreeF fa (FreeT fma)) } instance (Functor f, Monad m) => Monad (FreeT fm) where return a = FreeT (return (Pure a)) FreeT m >>= f = FreeT $ m >>= \v -> case v of Pure a -> runFreeT (fa) Free w -> return (Free (fmap (>>= f) w)) instance MonadTrans (FreeT f) where lift = FreeT . liftM Pure liftF :: (Functor f, Monad m) => fr -> FreeT fmr liftF x = FreeT (return (Free (fmap return x)))
Source: https://habr.com/ru/post/195274/
All Articles