Reactive Banana: потребляем параметризованный вызов внешнего API
Начиная с предыдущего вопроса здесь: Reactive Banana: как использовать значения из удаленного API и объединить их в потоке событий
У меня сейчас немного другая проблема: как я могу использовать Behaviour
выводить как ввод для операции ввода-вывода и, наконец, отображать результат операции ввода-вывода?
Ниже код из предыдущего ответа изменен со вторым выводом:
import System.Random
type RemoteValue = Int
-- generate a random value within [0, 10)
getRemoteApiValue :: IO RemoteValue
getRemoteApiValue = (`mod` 10) <$> randomIO
getAnotherRemoteApiValue :: AppState -> IO RemoteValue
getAnotherRemoteApiValue state = (`mod` 10) <$> randomIO + count state
data AppState = AppState { count :: Int } deriving Show
transformState :: RemoteValue -> AppState -> AppState
transformState v (AppState x) = AppState $ x + v
main :: IO ()
main = start $ do
f <- frame [text := "AppState"]
myButton <- button f [text := "Go"]
output <- staticText f []
output2 <- staticText f []
set f [layout := minsize (sz 300 200)
$ margin 10
$ column 5 [widget myButton, widget output, widget output2]]
let networkDescription :: forall t. Frameworks t => Moment t ()
networkDescription = do
ebt <- event0 myButton command
remoteValueB <- fromPoll getRemoteApiValue
myRemoteValue <- changes remoteValueB
let
events = transformState <$> remoteValueB <@ ebt
coreOfTheApp :: Behavior t AppState
coreOfTheApp = accumB (AppState 0) events
sink output [text :== show <$> coreOfTheApp]
sink output2 [text :== show <$> reactimate ( getAnotherRemoteApiValue <@> coreOfTheApp)]
network <- compile networkDescription
actuate network
Как вы можете видеть, что я пытаюсь сделать, это используя новое состояние приложения -> getAnotherRemoteApiValue
-> показать. Но это не работает.
Возможно ли это сделать?
ОБНОВЛЕНИЕ На основе ответов Эрика Аллика и Генриха Апфельма, приведенных ниже, у меня есть текущая ситуация с кодом - это работает:):
{-# LANGUAGE ScopedTypeVariables #-}
module Main where
import System.Random
import Graphics.UI.WX hiding (Event, newEvent)
import Reactive.Banana
import Reactive.Banana.WX
data AppState = AppState { count :: Int } deriving Show
initialState :: AppState
initialState = AppState 0
transformState :: RemoteValue -> AppState -> AppState
transformState v (AppState x) = AppState $ x + v
type RemoteValue = Int
main :: IO ()
main = start $ do
f <- frame [text := "AppState"]
myButton <- button f [text := "Go"]
output1 <- staticText f []
output2 <- staticText f []
set f [layout := minsize (sz 300 200)
$ margin 10
$ column 5 [widget myButton, widget output1, widget output2]]
let networkDescription :: forall t. Frameworks t => Moment t ()
networkDescription = do
ebt <- event0 myButton command
remoteValue1B <- fromPoll getRemoteApiValue
let remoteValue1E = remoteValue1B <@ ebt
appStateE = accumE initialState $ transformState <$> remoteValue1E
appStateB = stepper initialState appStateE
mapIO' :: (a -> IO b) -> Event t a -> Moment t (Event t b)
mapIO' ioFunc e1 = do
(e2, handler) <- newEvent
reactimate $ (\a -> ioFunc a >>= handler) <$> e1
return e2
remoteValue2E <- mapIO' getAnotherRemoteApiValue appStateE
let remoteValue2B = stepper Nothing $ Just <$> remoteValue2E
sink output1 [text :== show <$> appStateB]
sink output2 [text :== show <$> remoteValue2B]
network <- compile networkDescription
actuate network
getRemoteApiValue :: IO RemoteValue
getRemoteApiValue = do
putStrLn "getRemoteApiValue"
(`mod` 10) <$> randomIO
getAnotherRemoteApiValue :: AppState -> IO RemoteValue
getAnotherRemoteApiValue state = do
putStrLn $ "getAnotherRemoteApiValue: state = " ++ show state
return $ count state
2 ответа
Фундаментальная проблема является концептуальной: события FRP и Поведения могут быть объединены только в чистом виде. В принципе, невозможно иметь функцию типа, скажем
mapIO' :: (a -> IO b) -> Event a -> Event b
потому что порядок, в котором должны выполняться соответствующие действия ввода-вывода, не определен.
На практике иногда бывает полезно выполнить ввод-вывод, комбинируя события и поведение. execute
комбинатор может сделать это, как указывает @ErikAllik. В зависимости от характера getAnotherRemoteApiValue
это может быть правильным, в частности, если эта функция идемпотентна или выполняет быстрый поиск из местоположения в ОЗУ.
Однако, если вычисления более сложны, то, вероятно, лучше использовать reactimate
выполнить вычисление IO. С помощью newEvent
создать AddHandler
мы можем дать реализацию mapIO'
функция:
mapIO' :: (a -> IO b) -> Event a -> MomentIO (Event b)
mapIO' f e1 = do
(e2, handler) <- newEvent
reactimate $ (\a -> f a >>= handler) <$> e1
return e2
Ключевое отличие от чистого комбинатора
fmap :: (a -> b) -> Event a -> Event b
в том, что последний гарантирует, что входные и результирующие события происходят одновременно, тогда как первый не дает абсолютно никаких гарантий относительно того, когда результирующее событие происходит по отношению к другим событиям в сети.
Обратите внимание, что execute
также гарантирует, что ввод и результат имеют одновременное возникновение, но накладывает неофициальные ограничения на ввод-вывод.
С этим трюком объединения reactimate
с newEvent
подобный комбинатор может быть написан для поведения подобным образом. Имейте в виду, что набор инструментов от Reactive.Banana.Frameworks
подходит только в том случае, если вы имеете дело с действиями ввода-вывода, точный порядок которых обязательно будет неопределенным.
(Чтобы этот ответ был актуальным, я использовал сигнатуры типов из предстоящего реактивного банана 1.0. В версии 0.9 сигнатура типа для mapIO'
является
mapIO' :: Frameworks t => (a -> IO b) -> Event t a -> Moment t (Event t b)
)
TL;DR: прокрутите вниз до раздела ОТВЕТ: решение, а также объяснение.
Прежде всего
getAnotherRemoteApiValue state = (`mod` 10) <$> randomIO + count state
является недействительным (т.е. не проверяет тип) по причинам, совершенно не связанным с FRP или реактивным-бананом: вы не можете добавить Int
для IO Int
- так же, как вы не можете подать заявку mod 10
для IO Int
непосредственно, именно поэтому, в ответе на ваш оригинальный вопрос, я использовал <$>
(это другое название fmap
от Functor
).
Я настоятельно рекомендую вам посмотреть и понять цель / значение <$>
, вместе с <*>
и некоторые другие Functor
а также Applicative
методы класса типов - FRP (по крайней мере, так, как он разработан в реактивном банане) в значительной степени опирается на Функторы и Аппликативы (а иногда и на Монады, Стрелы и, возможно, на какой-то другой более новый фундамент), следовательно, если вы не до конца понимаете их, вы никогда не станет опытным с FRP.
Во-вторых, я не уверен, почему вы используете coreOfTheApp
за sink output2 ...
- coreOfTheApp
значение связано с другим значением API.
В-третьих, как должно отображаться другое значение API? Или, более конкретно, когда это должно отображаться? Ваше первое значение API отображается при нажатии кнопки, но для второй кнопки нет. Хотите ли вы, чтобы эта же кнопка вызывала вызов API и отображала обновление? Хотите еще одну кнопку? Или вы хотите, чтобы его опрашивали каждый n
единица времени и просто автоматически обновляется в пользовательском интерфейсе?
Наконец, reactimate
предназначен для преобразования Behavior
в IO
действие, которое не то, что вы хотите, потому что у вас уже есть show
помощник и не нужно setText
или что-то на статической этикетке. Другими словами, то, что вам нужно для второго значения API, такое же, как и раньше, за исключением того, что вам нужно передать что-то из состояния приложения вместе с запросом во внешний API, но помимо этого различия вы все равно можете просто продолжать показывать (другое) значение API с использованием show
как обычно.
ОТВЕТ:
Что касается того, как конвертировать getAnotherRemoteApiValue :: AppState -> IO RemoteValue
в Event t Int
похож на оригинал remoteValueE
:
Я сначала попытался пройти через IORef
с и используя changes
+ reactimate'
, но это быстро оказалось в тупик (помимо того, что уродливо и чрезмерно сложно): output2
всегда обновлялся один "цикл" FRP слишком поздно, поэтому в пользовательском интерфейсе всегда оставалась одна "версия".
Затем я с помощью Оливера Чарльза (ocharles) на #haskell-game во FreeNode обратился к execute
:
execute :: Event t (FrameworksMoment a) -> Moment t (Event t a)
что я до сих пор не до конца понял, но это работает:
let x = fmap (\s -> FrameworksMoment $ liftIO $ getAnotherRemoteApiValue s)
(appStateB <@ ebt)
remoteValue2E <- execute x
поэтому одна и та же кнопка запускает оба действия. Но проблема с этим быстро оказалась такой же, как с IORef
решение на основе - поскольку одна и та же кнопка будет вызывать пару событий, и одно событие внутри этой пары зависит от другого, содержимое output2
была еще одна версия позади.
Затем я понял, что события связаны с output2
должны быть запущены после любых событий, связанных с output1
, Однако невозможно перейти от Behavior t a -> Event t a
; другими словами, если у вас есть поведение, вы не можете (легко?) получить событие из этого (кроме как с помощью changes
, но changes
привязан к reactimate
/ reactimate'
что здесь не полезно).
Я наконец заметил, что я по сути "выбрасывал" промежуточный Event
на этой линии:
appStateB = accumB initialState $ transformState <$> remoteValue1E
заменив его
appStateE = accumE initialState $ transformState <$> remoteValue1E
appStateB = stepper initialState -- there seems to be no way to eliminate the initialState duplication but that's fine
так что у меня все еще было то же самое appStateB
, который используется, как и раньше, но я мог бы также рассчитывать на appStateE
надежно инициировать дальнейшие события, которые полагаются на AppState
:
let x = fmap (\s -> FrameworksMoment $ liftIO $ getAnotherRemoteApiValue s)
appStateE
remoteValue2E <- execute x
Финал sink output2
линия выглядит так:
sink output2 [text :== show <$> remoteValue2B]
Весь код можно увидеть по адресу http://lpaste.net/142202, с отладочным выходом по-прежнему включен.
Обратите внимание, что (\s -> FrameworkMoment $ liftIO $ getAnotherRemoteApiValue s)
Лямбда не может быть преобразована в стиль без точек по причинам, связанным с типами RankN. Мне сказали, что эта проблема исчезнет в реактивном банане 1.0, потому что не будет FrameworkMoment
вспомогательный тип.