Ответ 1
Я настоятельно рекомендую вам читать Языковой подход к объединению событий и потоков. В нем рассказывается о том, как вы можете структурировать любую систему concurrency, которую вы хотите, в дополнение к своей подсистеме ввода-вывода, и в своей статье они фактически реализуют ее поверх epoll
.
К сожалению, типы данных и примеры кода в документе невероятно бедны, и потребовалось некоторое время (по крайней мере, для меня), чтобы перепроектировать их код, и есть даже некоторые ошибки в их документе. Однако их подход на самом деле является подмножеством очень мощного и общего подхода, известного как "свободные монады".
Например, их тип данных Trace
- это просто скрытая скрытая монада. Чтобы понять, почему, давайте проконсультироваться с определением свободной монады Хаскелла:
data Free f r = Pure r | Free (f (Free f r))
Свободная монада похожа на "список функторов", где Pure
аналогичен конструктору списка Nil
, а Free
аналогичен конструктору списка Cons
, потому что он добавляет дополнительный функтор в "список". Технически, если бы я был педантичным, нет ничего, что говорит о том, что свободная монада должна быть реализована как вышеупомянутый тип данных типа списка, но все, что вы реализуете, должно быть изоморфно вышеуказанному типу данных.
Хорошая вещь о свободной монаде состоит в том, что, учитывая функтор f
, Free f
автоматически является монадой:
instance (Functor f) => Monad (Free f) where
return = Pure
Pure r >>= f = f r
Free x >>= f = Free (fmap (>>= f) x)
Это означает, что мы можем разложить их тип данных Trace
на две части: базовый функтор f
, а затем свободную монаду, сгенерированную с помощью f
:
-- The base functor
data TraceF x =
SYS_NBIO (IO x)
| SYS_FORK x x
| SYS_YIELD x
| SYS_RET
| SYS_EPOLL_WAIT FD EPOLL_EVENT x
-- You can even skip this definition if you use the GHC
-- "DerivingFunctor" extension
instance Functor TraceF where
fmap f (SYS_NBIO x) = SYS_NBIO (liftM f x)
fmap f (SYS_FORK x) = SYS_FORK (f x) (f x)
fmap f (SYS_YIELD x) = SYS_YIELD (f x)
fmap f SYS_RET = SYS_RET
fmap f (SYS_EPOLL_WAIT FD EPOLL_EVENT x) = SYS_EPOLL_WAIT FD EPOLL_EVEN (f x)
Учитывая этот функтор, вы получаете Trace
монаду "бесплатно":
type Trace a = Free TraceF a
-- or: type Trace = Free TraceF
... хотя это и не потому, что оно называлось "свободной" монадой.
Тогда легче определить все их функции:
liftF = Free . fmap Pure
-- if "Free f" is like a list of "f", then
-- this is sort of like: "liftF x = [x]"
-- it just a convenience function
-- their definitions are written in continuation-passing style,
-- presumably for efficiency, but they are equivalent to these
sys_nbio io = liftF (SYS_NBIO io)
sys_fork t = SYS_FORK t (return ()) -- intentionally didn't use liftF
sys_yield = liftF (SYS_YIELD ())
sys_ret = liftF SYS_RET
sys_epoll_wait fd event = liftF (SYS_EPOLL_WAIT fd event ())
Итак, вы можете использовать эти команды так же, как монада:
myTrace fd event = do
sys_nbio (putStrLn "Hello, world")
fork $ do
sys_nbio (putStrLn "Hey")
sys_expoll_wait fd event
Теперь, вот ключевая концепция. Эта монада, которую я только что написал, создает только тип данных. Это. Он не интерпретирует его вообще. Это точно так же, как вы должны написать абстрактное синтаксическое дерево для выражения. Это полностью зависит от вас, как вы хотите его оценить. В статье они приводят конкретный пример интерпретатора для выражения, но тривиально писать свои собственные.
Важная концепция заключается в том, что этот интерпретатор может работать в любой монаде, которую вы хотите. Поэтому, если вы хотите пролить некоторое состояние через ваш concurrency, вы можете это сделать. Например, здесь интерпретатор игрушек, который использует монаду StateT IO для отслеживания того, сколько раз было вызвано действие IO:
interpret t = case t of
SYS_NBIO io -> do
modify (+1)
t' <- lift io
interpret t'
...
Вы можете даже накладывать монады на действия forkIO'd! Вот какой-то очень старый мой код, который багги и хромой, потому что он был написан обратно, когда я был гораздо менее опытным и понятия не имел, что такое свободные монады, но это демонстрирует это в действии:
module Thread (Thread(..), done, lift, branch, fork, run) where
import Control.Concurrent
import Control.Concurrent.STM
import Control.Monad.Cont
import Data.Sequence
import qualified Data.Foldable as F
data Thread f m =
Done
| Lift (m (Thread f m))
| LiftIO (IO (Thread f m))
| Branch (f (Thread f m))
| Exit
done = cont $ \c -> Done
lift' x = cont $ \c -> Lift $ liftM c x
liftIO' x = cont $ \c -> LiftIO $ liftM c x
branch x = cont $ \c -> Branch $ fmap c x
exit = cont $ \c -> Exit
fork x = join $ branch [return (), x >> done]
run x = do
q <- liftIO $ newTChanIO
enqueue q $ runCont x $ \_ -> Done
loop q
where
loop q = do
t <- liftIO $ atomically $ readTChan q
case t of
Exit -> return ()
Done -> loop q
Branch ft -> mapM_ (enqueue q) ft >> loop q
Lift mt -> (mt >>= enqueue q) >> loop q
LiftIO it -> (liftIO $ forkIO $ it >>= enqueue q) >> loop q
enqueue q = liftIO . atomically . writeTChan q
Точка за свободными монадами состоит в том, что они предоставляют экземпляр monad и NOTHING ELSE. Другими словами, они отступают и дают вам полную свободу, как вы хотите их интерпретировать, поэтому они настолько невероятно полезны.