Создание поведения для постоянно измеряемого явления
Я хотел бы создать Behavior t a
из IO a
с предполагаемой семантикой, что действие ввода-вывода будет выполняться каждый раз, когда поведение sample
д:
{- language FlexibleContexts #-}
import Reflex.Dom
import Control.Monad.Trans
onDemand :: (MonadWidget t m, MonadIO (PullM t)) => IO a -> m (Behavior t a)
Я надеялся, что смогу сделать это, просто выполнив measurement
в pull
:
onDemand measure = return $ pull (liftIO measure)
Тем не менее, в результате Behavior
никогда не меняется после начального measure
Мент.
Обходной путь, который я мог придумать, заключался в создании манекена Behavior
это меняется "достаточно часто", а затем создает поддельную зависимость от этого:
import Data.Time.Clock as Time
hold_ :: (MonadHold t m, Reflex t) => Event t a -> m (Behavior t ())
hold_ = hold () . (() <$)
onDemand :: (MonadWidget t m, MonadIO (PullM t)) => IO a -> m (Behavior t a)
onDemand measure = do
now <- liftIO Time.getCurrentTime
tick <- hold_ =<< tickLossy (1/1200) now
return $ pull $ do
_ <- sample tick
liftIO measure
Это тогда работает как ожидалось; но с тех пор Behavior
В любом случае, выборка может производиться только по запросу, в этом нет необходимости.
Как правильно создать Behavior
для непрерывного, наблюдаемого в любое время явления?
2 ответа
Делать это в Spider
выглядит невозможно. Internal
рассуждения впереди.
в Spider
реализация Reflex
один из возможных Behavior
s, чтобы получить значение.
data Behavior a
= BehaviorHold !(Hold a)
| BehaviorConst !a
| BehaviorPull !(Pull a)
Pull
ed значение состоит из того, как вычислить значение при необходимости, pullCompute
и кэшированное значение, чтобы избежать ненужных повторных вычислений, pullValue
,
data Pull a
= Pull { pullValue :: !(IORef (Maybe (PullSubscribed a)))
, pullCompute :: !(BehaviorM a)
}
Игнорирование уродливой среды BehaviorM
, liftIO
поднимает IO
вычисления очевидным образом, он запускается, когда BehaviorM
должен быть выбран. в Pull
ваше поведение наблюдается один раз, но не наблюдается повторно, потому что кэшированное значение не становится недействительным.
Кэшированное значение PullSubscribed a
состоит из значения a
список других значений, которые должны быть признаны недействительными, если это значение недействительно, и некоторые скучные вещи по управлению памятью.
data PullSubscribed a
= PullSubscribed { pullSubscribedValue :: !a
, pullSubscribedInvalidators :: !(IORef [Weak Invalidator])
-- ... boring memory stuff
}
Invalidator
это количественно Pull
этого достаточно, чтобы получить ссылку на память для рекурсивного чтения недействительных элементов, чтобы сделать недействительными и записать кэшированное значение в Nothing
,
Чтобы тянуть постоянно, мы хотели бы иметь возможность постоянно аннулировать нашу собственную BehaviorM
, При выполнении среда перешла к BehaviorM
имеет копию своего собственного недействительного, который используется зависимостями BehaviorM
аннулировать его, когда они сами становятся недействительными.
Из внутренней реализации readBehaviorTracked
кажется, нет никакого способа, чтобы собственный инвалидатор поведения (wi
) может когда-либо оказаться в списке подписчиков, которые становятся недействительными при его выборке (invsRef
).
a <- liftIO $ runReaderT (unBehaviorM $ pullCompute p) $ Just (wi, parentsRef)
invsRef <- liftIO . newIORef . maybeToList =<< askInvalidator
-- ...
let subscribed = PullSubscribed
{ pullSubscribedValue = a
, pullSubscribedInvalidators = invsRef
-- ...
}
За пределами внутренних органов, если существует способ постоянно пробовать Behavior
это будет связано с MonadFix (PullM t)
экземпляр или взаимная рекурсия через фиксацию pull
а также sample
:
onDemand :: (Reflex t, MonadIO (PullM t)) => IO a -> Behavior t a
onDemand read = b
where
b = pull go
go = do
sample b
liftIO read
У меня нет Reflex
среда, чтобы попробовать это, но я не думаю, что результаты будут хорошими.
Я экспериментировал с этим некоторое время и нашел обходной путь. Кажется, работает с последней версией рефлекса на сегодняшний день. Хитрость заключается в том, чтобы принудительно аннулировать кэшированное значение каждый раз, когда вы оцениваете данное IO
действие.
import qualified Reflex.Spider.Internal as Spider
onDemand :: IO a -> Behavior t a
onDemand ma = SpiderBehavior . Spider.Behavior
. Spider.BehaviorM . ReaderT $ computeF
where
computeF (Nothing, _) = unsafeInterleaveIO ma
computeF (Just (invW,_), _) = unsafeInterleaveIO $ do
toReconnect <- newIORef []
_ <- Spider.invalidate toReconnect [invW]
ma
Важно использовать unsafeInterleaveIO
запустить аннулирующий как можно позже, чтобы он сделал недействительным существующую вещь.
Есть еще одна проблема с этим кодом: я игнорирую toReconnect
ссылка и результат invalidate
функция. В текущей версии рефлекса последняя всегда пуста, поэтому проблем не должно быть. Но я не уверен в toReconnect
: из кода кажется, что если у него есть подписанные ключи, они могут сломаться, если не будут обработаны должным образом. Хотя я не уверен, может ли такое поведение иметь подписанные ключи или нет.
ОБНОВЛЕНИЕ для тех, кто действительно хочет реализовать это: приведенный выше код может зайти в тупик в некоторых сложных настройках. Моим решением было сделать аннулирование немного позже самого вычисления в отдельном потоке. Вот полный фрагмент кода. Решение по ссылке, похоже, работает правильно (используя его уже почти год в производстве).