从上一个问题开始: Reactive Banana: how to use values from a remote API and merge them in the event stream
我现在有一个不同的问题:如何使用Behaviour
输出作为IO操作的输入并最终显示IO操作的结果?
以下是使用第二个输出更改上一个答案的代码:
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
正如您所看到的,我正在尝试使用应用程序的新状态 - &gt; getAnotherRemoteApiValue
- &gt;节目。但它没有用。
实际上有可能这样做吗?
更新 基于Erik Allik和Heinrich Apfelmus以下答案,我有当前的代码情况 - 有效:):
{-# 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
答案 0 :(得分:3)
根本问题是概念性问题:FRP事件和行为只能以纯粹的方式组合。原则上,不可能具有类型的功能,比如
mapIO' :: (a -> IO b) -> Event a -> Event b
因为执行相应IO操作的顺序是未定义。
实际上,在组合事件和行为时执行IO有时可能很有用。正如@ErikAllik所指出的那样,execute
组合器可以做到这一点。根据{{1}}的性质,这可能是正确的做法,特别是如果这是功能是幂等的,或者从RAM中的位置快速查找。
但是,如果计算更复杂,那么最好使用getAnotherRemoteApiValue
来执行IO计算。使用reactimate
创建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
还可以保证输入和结果同时发生,但会对允许的IO进行非正式限制。
通过将execute
与reactimate
结合起来的技巧,可以以类似的方式为行为编写类似的组合子。请记住,newEvent
中的工具箱仅适用于处理IO操作,其精确顺序必定未定义。
(为了使这个答案保持最新状态,我使用了即将发布的reactive-banana 1.0中的类型签名。在0.9版中,Reactive.Banana.Frameworks
的类型签名是
mapIO'
)
答案 1 :(得分:1)
TL; DR:向下滚动到 ANSWER:部分以获取解决方案以及解释。
首先
getAnotherRemoteApiValue state = (`mod` 10) <$> randomIO + count state
由于与FRP或反应性香蕉完全无关的原因,无效(即不进行类型检查):您无法向Int
添加IO Int
- 就像您无法应用{{1}一样直接转到mod 10
,这正是为什么,在原始问题的答案中,我使用了IO Int
(这是来自<$>
的{{1}}的另一个名称)。
我强烈建议您查看并了解fmap
的目的/含义,以及Functor
和其他一些<$>
和<*>
类型类方法 - FRP(at至少它在反应性香蕉中的设计方式很大程度上建立在Functors和Applicatives(有时是Monads,Arrows和可能还有一些其他更新颖的基础)上,所以如果你不完全理解那些,你将永远不会精通FRP。
其次,我不确定您为Functor
使用Applicative
的原因 - coreOfTheApp
值与其他API值相关。< / p>
第三,应该如何显示其他API值?或者,更具体地说,什么时候应该显示?单击按钮时会显示您的第一个API值,但第二个按钮没有按钮 - 您是否希望使用相同的按钮来触发API调用并显示更新?你想要另一个按钮吗?或者您希望每sink output2 ...
个单位时间对其进行一次轮询,并在UI中自动更新?
最后,coreOfTheApp
用于将n
转换为reactimate
操作,这不是您想要的,因为您已经拥有Behavior
1}}帮助,不需要IO
或静态标签上的smth。换句话说,您需要的第二个API值与之前相同,除了您需要将应用程序状态中的内容与请求一起传递给外部API,但除了这些差异之外,您仍然可以继续显示(其他)正常使用show
的API值。
<强>解答:强>
关于如何将setText
转换为与原始show
类似的getAnotherRemoteApiValue :: AppState -> IO RemoteValue
:
我首先尝试通过Event t Int
并使用remoteValueE
+ IORef
,但很快就变成了死胡同(除了丑陋和过于复杂):{{1总是更新一个FRP“周期”太晚了,所以它总是在UI中落后一个“版本”。
然后,在Oliver Charles(ocharles)的帮助下,在FreeNode的#haskell-game上,转向changes
:
reactimate'
我还没有完全掌握,但它确实有效:
output2
所以相同的按钮会触发两个动作。但问题很快就与基于execute
的解决方案相同 - 因为相同的按钮会触发一对事件,并且该对内的一个事件依赖于另一个事件,{{{{} 1}}仍然是一个版本。
然后我意识到与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
/ {{1 },这在这里没用。)
我终于注意到我在这一行基本上“扔掉了”中间人Behavior t a -> Event t a
:
changes
将其替换为
changes
所以我仍然使用完全相同的reactimate
,这是先前使用的,但我还可以依靠reactimate'
可靠地触发依赖于Event
的其他事件:< / p>
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
所有代码都可以在http://lpaste.net/142202看到,调试输出仍然启用。
请注意,由于与RankN类型相关的原因,appStateE
lambda无法转换为无点样式。有人告诉我,这个问题会在反应性香蕉1.0中消失,因为没有AppState
助手类型。