
{-# 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 BoolcFork 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