Исходная статья «A monad for reactive programming. Part 1»1 написанная Альберто Гомез Корона (Alberto Gómez Corona). Свободный (интерпретированный) перевод выполнен Артуром Файзрахмановым.
Нетерпеливым: вы можете найти кое—что работающее в самом низу.
Вступление
Отзывчивое (реактивное) функциональное программирование не имеет понятия об области видимости событий. При получении сигнала повествовательное (декларативное) отзывчивое функциональное вычисление затрагивается целиком и должно некоторым образом начаться заново, в силу своей повествовательной природы. Монада же может содержать в себе область видимости сигнала,– кое–что уже вычислено заранее и эта часть не меняется, когда текущий компонент встраивает сигнал в вычисление на каком то этапе.
Когда происходит вызов монады с более высокого уровня по сигналу, она может разложить вычисление в цепочку виртуальных обработчиков событий (установленных монадным вычислением), даже в момент своего первого вызова.
Такой подход имеет большую область применения. Представьте себе такое вычисление:
profits = do
quantity ← waitEvent "количество"
liftIO $ do
putStr "количество="
print quantity
getChar
price ← waitEvent "стоимость"
liftIO $ do
putStr "стоимость="
print price
getChar
liftIO $ do
putStr "итог="
print (quantity * price)
getChar
return (quantity * price)Представим, что количество акций и их стоимость меняются в течении суток, таким образом у нас есть два события: первое — когда меняется количество акций, второе — когда изменяется стоимость акций. Представим, что на самом деле программа обновляет данные на информационном табло, вместо того чтобы печатать вывод в консоль, и что в действительности вычисления гораздо более трудоёмкие и затратные, производящие доступ к базам данных и отчётов в реальном времени, сопровождаемые большим количеством других событий. Мы хотим сделать так, чтобы при изменении стоимости монада выполнила только те действия, которые нужны для изменения стоимости и дохода, не затрагивая другие затратные ветки монады, включая не только те вычисления, которые произошли выше, но и ветвления, которые могут произойти в дальнейшем (после события), если мы пожелаем.
Воплощение задумки
Монада, подходящая для настройки отклика на события подобным образом, может выглядить примерно так:
data Transient m x = Transient (m (Maybe x))
instance (MonadState m, StateType m ~ EventF) ⇒ Monad (Transient m) where
return x = Transient (return . Just $ x)
x ↠ f = Transient $ do
setEventCont f
mk ← runTrans x
case mk of
Just k → runTrans (f k)
Nothing → return NothingНазвание Transient, что означает «мимолётный», взято из моей давней идеи
замыканий в монадическом выражении в виде мимолётных чистых состояний, которые
вычисляются заново только тогда, когда происходит изменение выше.
Мы можем остановить остаток вычисления ниже уровнем таким образом:
stop = empty
empty :: Monad m => Transient m a
empty = Transient (return Nothing)Это пустое значение empty будет использоваться в аппликативном образце для
«мимолётной» монады, но мы не будем описывать его прямо сейчас.
Мы используем монаду состояния, так как нам требуется передавать данные события
EventF, которые представляют из себя продолжения вычислений (f) для каждого
вычисления (x).
data EventF = forall m b c. EventF (b → Transient m c)Transient использует возможные значения Maybe, потому что мы хотим иметь
возможность остановливать продвижение вычислительного потока вглубь всегда,
когда в этом нет нужды. Монада не устанавливает обработчики событий, вместо
этого она просто запоминает следующее продолжение вычисления с помощью
setEventCont:
setEventCont f = do
f' ← get
put $ EventF f
return f'waitEvent и есть настроящее вычисление,– текущий (активный) компонент,
который устанавливает обработчик события. Этот обработчик – продолжение
вычисления текущего компонента (установленный монадой) для ожидающего события.
(Заметим, что проще было бы обойтись без состояния, но, как покажет дальнейшее повествование, это на самом деле необходимо.)
В конце концов, примитив waitEvent приостанавливает вычисление, возвращая
Nothing:
waitEvent name = Transient $ do
f ← get
evs ← liftIO $ takeMVar eventHandlers
liftIO . putMVar eventHandlers . M.insert name f $ evs -- !> ("set "++name)
return NothingОн использует карту обработчиков событий:
eventHandlers ∷ MVar (M.Map String EventF)
eventHandlers = unsafePerformIO $ newMVar M.emptyПосле завершения выполнения монадического выражения эта структура по меньшей
мере имеет первый обработчик событий. eventLoop ожидает события и выполняет
соответствующий обработчик:
type EvType = String
data Event = forall a. Event EvType a
eventLoop [] = return()
eventLoop (Event name r : evs) = do
liftIO . print $ "новое событие: " ++ name
ths ← liftIO . readMVar $ eventHandlers
case M.lookup name ths of
Just (EventF f) → runTrans'' $ (unsafeCoerce f) r
Nothing → return ()
eventLoop evsСобытие Event содержит в себе тип (который используется для поиска
обработчиков) и значение.
Вместо использования списка, eventLoop может быть описан как процесс,
считывающий некую очередь [событий].
Обратите внимание на использование небезопасного преобразования unsafeCoerce.
Конструкция диктует то, что это сработает как нужно, так как значение x из
монады будет передано продолжению f, записанному в состоянии, но компилятор
об этом не знает. Сделаем это допущение в целях экспиремента. Я – физик, не
математик и не инженер. Будь я математиком, я бы провёл три года в борьбе с
денотационной семантикой обработки событий, производя заумные работы прежде чем
представить что-то, что можно было бы использовать. Если же я был инженером, я
бы написал простую показательную программу конкретной задачи. Я люблю
математику и инженерное дело, но я с радостью приношу в жертву красоту концепции
и сроки поставки во имя решения общей проблемы.
Вычисление runTrans'' запускает продолжение, принимая трансформер StateT,
который переносит состоние EventF.
runTrans'' ∷ Transient (StateT EventF IO) a → IO ()
runTrans'' tmx = runTrans' tmx >> return ()
runTrans' ∷ Monad m ⇒ Transient (StateT EventF m) x → m (Maybe x)
runTrans' tmx = evalStateT (runTrans tmx) undefinedПродолжение вызываемое runTrans'' может содержать в себе последующие выражения
waitEvent, добавляющие последующие обработчики событий в петление таким же
образом.
Запуск
Давайте скормим нашему чудовищу такой набор событий:
eventList =
[ Event "количество" 10
, Event "стоимость" 2
, Event "стоимость" 3
, Event "количество" 30
, Event "стоимость" 4
]Теперь запускаем (нажимайте Ввод, чтобы продолжать вычисления,
потому что функция доходов profits использует getChar).
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
import Control.Concurrent.MVar
import Control.Monad.State
import Data.Map as M
import System.IO.Unsafe
import Unsafe.Coerce
data EventF = forall m b c. EventF (b -> Transient m c)
data Transient m x = Transient (m (Maybe x))
runTrans :: Transient m x -> m (Maybe x)
runTrans (Transient mx) = mx
setEventCont f = do
f' <- get
put $ EventF f
return f'
empty :: Monad m => Transient m a
empty = Transient $ return Nothing
instance (MonadState EventF m) => Monad (Transient m) where
return x = Transient . return . Just $ x
x >>= f = Transient $ do
setEventCont f
mk <- runTrans x
case mk of
Just k -> runTrans (f k)
Nothing -> return Nothing
instance MonadTrans Transient where
lift mx = Transient $ mx >>= return . Just
instance (MonadState EventF m, MonadIO m) => MonadIO (Transient m) where
liftIO io = let x = liftIO io in x `seq` lift x
instance (MonadState EventF m) => MonadState EventF (Transient m) where
-- type StateType (Transient m) = EventF
get = Transient $ get >>= return . Just
put x = Transient $ put x >> return (Just ())
eventHandlers :: MVar (M.Map String EventF)
eventHandlers = unsafePerformIO $ newMVar M.empty
type EvType = String
data Event = forall a. Event EvType a
waitEvent name = Transient $ do
f <- get
evs <- liftIO $ takeMVar eventHandlers
liftIO . putMVar eventHandlers . M.insert name f $ evs
return Nothing
eventLoop [] = return ()
eventLoop (Event name r : evs) = do
liftIO . putStrLn $ "новое событие: " ++ name
ths <- liftIO . readMVar $ eventHandlers
case M.lookup name ths of
Just (EventF f) -> runTrans'' $ (unsafeCoerce f) r
Nothing -> return ()
eventLoop evs
runTrans' :: Monad m => Transient (StateT EventF m) x -> m (Maybe x)
runTrans' tmx = evalStateT (runTrans tmx) undefined
runTrans'' :: Transient (StateT EventF IO) a -> IO ()
runTrans'' tmx = runTrans' tmx >> return ()
-- show
main :: IO ()
main = do
runTrans'' profits
eventLoop eventList
putStrLn "КОНЕЦ"
eventList :: [Event]
eventList =
[ Event "количество" 10
, Event "стоимость" 2
, Event "стоимость" 3
, Event "количество" 30
, Event "стоимость" 4
]
profits :: Transient (StateT EventF IO) Integer
profits = do
quantity <- waitEvent "количество"
liftIO $ do
putStr "количество="
print quantity
getChar
price <- waitEvent "стоимость"
liftIO $ do
putStr "стоимость="
print price
getChar
liftIO $ do
putStr $ "итог="
print $ quantity * price
getChar
return $ quantity * price
-- /showКак видите, выполняется только тот код, который следует за событием, и используется только тот вышестоящий контекст, который необходим продолжению для каждого вызова по событию.
Вот и всё.
Хотя, нет. Взгляните на этот пример:
main = do
runTrans'' $ do
let threshold = 100
pr <- profits
liftIO $ do
when (pr > threshold) $
putStr "Порог пройден! Отправлено письмо начальнику."
print pr
eventLoop eventList
putStrLn "END"Здесь выполняется то же самое вычисление прибыли profits с теми же событиями,
но в самом конце если доход превышает заданный порог производится дополнительно
сообщение.
Возможно, вы думаете, что последним сообщением программы будет
"Порог пройден…", если заменить главное выражение main в программе на приведённое в
примере, но это не так. Это значит, что в таком виде монада выполняет неполный
набор действий по событию. Необходимо сохранять состояние полного набора всех
вложенных продолжений во всех вложенных вычислениях, затрагиваемых событием, а
не отдельно взятые продолжения.
Кроме того, небезопасное приведение типов unsafeCoerce может привести к ошибке
сегментации, к примеру, если в монадическом вычислении мы заменим
waitEvent "quantity"на
(*) <$> return units <*> waitEvent "quantity".
Но об этом во второй части.
Кстати говоря, там я представлю улучшенную версию этой монады, используемую в
клиентском фреймворке hplayground.