Reactive Banana:消耗对外部 API 的参数化调用
Reactive Banana: consume parametrized call to an external API
从这里的上一个问题开始:
我现在遇到了一些不同的问题:如何使用 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
如您所见,我正在尝试使用应用程序的新状态 -> getAnotherRemoteApiValue
-> 显示。但是没用。
真的可以这样做吗?
更新
基于 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
TL;DR: 向下滚动到 ANSWER: 部分以获得解决方案和解释。
首先
getAnotherRemoteApiValue state = (`mod` 10) <$> randomIO + count state
因与 FRP 或 reactive-banana 完全无关的原因而无效(即不进行类型检查):您不能将 Int
添加到 IO Int
— 就像您不能应用 mod 10
直接转换为 IO Int
,这就是为什么在回答您原来的问题时,我使用了 <$>
(这是 Functor
中 fmap
的另一个名称) .
我强烈建议您查找并理解 <$>
的 purpose/meaning,以及 <*>
和其他一些 Functor
和 Applicative
类型 class 方法——FRP(至少它在 reactive-banana 中的设计方式)在很大程度上建立在 Functors 和 Applicatives(有时还有 Monads、Arrows 和可能其他一些更新颖的基础)之上,因此如果你不完全理解这些,你永远不会精通 FRP。
其次,我不确定您为什么使用 coreOfTheApp
作为 sink output2 ...
— coreOfTheApp
值与其他 API 值。
第三,另外一个API值应该怎么显示?或者,更具体地说,应该在什么时候显示?单击按钮时会显示您的第一个 API 值,但没有第二个按钮 - 您是否希望同一个按钮触发 API 调用和显示更新?你想要另一个按钮吗?或者您希望它每隔 n
个时间单位轮询一次并在 UI 中自动更新?
最后,reactimate
是用来将Behavior
转换成IO
动作,这不是你想要的,因为你已经有了 show
助手,不需要 setText
或静态标签上的 smth。换句话说,你需要的第二个 API 值和以前一样,除了你需要从应用程序状态中传递一些东西以及对外部 API 的请求,但除此之外,您仍然可以像往常一样使用 show
继续显示(其他)API 值。
答案:
至于如何将getAnotherRemoteApiValue :: AppState -> IO RemoteValue
转换成类似于原来的remoteValueE
的Event t Int
:
我首先尝试通过 IORef
s 并使用 changes
+reactimate'
,但很快就陷入了死胡同(除了丑陋和过于复杂之外):output2
总是更新一个 FRP "cycle" 太晚了,所以它在 UI.
中总是落后一个 "version"
然后我在Oliver Charles (ocharles) 的帮助下在#haskell-game on 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
的解决方案相同——因为同一个按钮会触发一对事件,而这对事件中的一个事件依赖于另一个事件,[= 的内容44=]还差一个版本。
然后我意识到与 output2
相关的事件需要在 在 任何与 output1
相关的事件之后触发。但是,从 Behavior t a -> Event t a
走是不可能的;换句话说,一旦你有一个行为,你就不能(很容易?)从中获得一个事件(除了 changes
,但是 changes
与 reactimate
/reactimate'
, 在这里没用).
我终于注意到我在这一行基本上是 "throwing away" 中级 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)
lambda 由于与 RankN 类型相关的原因无法转换为无点样式。有人告诉我这个问题将在 reactive-banana 1.0 中消失,因为没有 FrameworkMoment
助手类型。
根本问题是概念性问题:FRP 事件和行为只能以一种纯粹的方式组合。原则上不可能有类型的函数,say
mapIO' :: (a -> IO b) -> Event a -> Event b
因为对应的IO action要执行的顺序是undefined.
在实践中,有时在组合事件和行为的同时执行 IO 可能很有用。 execute
组合器可以做到这一点,正如@ErikAllik 所指出的那样。根据 getAnotherRemoteApiValue
的性质,这可能是正确的做法,特别是如果此函数是幂等的,或者从 RAM 中的位置进行快速查找。
不过,如果计算比较复杂,那么用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
还保证输入和结果同时发生,但对允许的 IO 进行了非正式限制。
通过将 reactimate
与 newEvent
组合的技巧,可以以类似的方式为行为编写类似的组合器。请记住,Reactive.Banana.Frameworks
中的工具箱仅适用于处理其精确顺序必然未定义的 IO 操作时。
(为了使这个答案保持最新,我使用了即将推出的 reactive-banana 1.0 的类型签名。在 0.9 版中,mapIO'
的类型签名是
mapIO' :: Frameworks t => (a -> IO b) -> Event t a -> Moment t (Event t b)
)
从这里的上一个问题开始:
我现在遇到了一些不同的问题:如何使用 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
如您所见,我正在尝试使用应用程序的新状态 -> getAnotherRemoteApiValue
-> 显示。但是没用。
真的可以这样做吗?
更新 基于 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
TL;DR: 向下滚动到 ANSWER: 部分以获得解决方案和解释。
首先
getAnotherRemoteApiValue state = (`mod` 10) <$> randomIO + count state
因与 FRP 或 reactive-banana 完全无关的原因而无效(即不进行类型检查):您不能将 Int
添加到 IO Int
— 就像您不能应用 mod 10
直接转换为 IO Int
,这就是为什么在回答您原来的问题时,我使用了 <$>
(这是 Functor
中 fmap
的另一个名称) .
我强烈建议您查找并理解 <$>
的 purpose/meaning,以及 <*>
和其他一些 Functor
和 Applicative
类型 class 方法——FRP(至少它在 reactive-banana 中的设计方式)在很大程度上建立在 Functors 和 Applicatives(有时还有 Monads、Arrows 和可能其他一些更新颖的基础)之上,因此如果你不完全理解这些,你永远不会精通 FRP。
其次,我不确定您为什么使用 coreOfTheApp
作为 sink output2 ...
— coreOfTheApp
值与其他 API 值。
第三,另外一个API值应该怎么显示?或者,更具体地说,应该在什么时候显示?单击按钮时会显示您的第一个 API 值,但没有第二个按钮 - 您是否希望同一个按钮触发 API 调用和显示更新?你想要另一个按钮吗?或者您希望它每隔 n
个时间单位轮询一次并在 UI 中自动更新?
最后,reactimate
是用来将Behavior
转换成IO
动作,这不是你想要的,因为你已经有了 show
助手,不需要 setText
或静态标签上的 smth。换句话说,你需要的第二个 API 值和以前一样,除了你需要从应用程序状态中传递一些东西以及对外部 API 的请求,但除此之外,您仍然可以像往常一样使用 show
继续显示(其他)API 值。
答案:
至于如何将getAnotherRemoteApiValue :: AppState -> IO RemoteValue
转换成类似于原来的remoteValueE
的Event t Int
:
我首先尝试通过 IORef
s 并使用 changes
+reactimate'
,但很快就陷入了死胡同(除了丑陋和过于复杂之外):output2
总是更新一个 FRP "cycle" 太晚了,所以它在 UI.
然后我在Oliver Charles (ocharles) 的帮助下在#haskell-game on 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
的解决方案相同——因为同一个按钮会触发一对事件,而这对事件中的一个事件依赖于另一个事件,[= 的内容44=]还差一个版本。
然后我意识到与 output2
相关的事件需要在 在 任何与 output1
相关的事件之后触发。但是,从 Behavior t a -> Event t a
走是不可能的;换句话说,一旦你有一个行为,你就不能(很容易?)从中获得一个事件(除了 changes
,但是 changes
与 reactimate
/reactimate'
, 在这里没用).
我终于注意到我在这一行基本上是 "throwing away" 中级 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)
lambda 由于与 RankN 类型相关的原因无法转换为无点样式。有人告诉我这个问题将在 reactive-banana 1.0 中消失,因为没有 FrameworkMoment
助手类型。
根本问题是概念性问题:FRP 事件和行为只能以一种纯粹的方式组合。原则上不可能有类型的函数,say
mapIO' :: (a -> IO b) -> Event a -> Event b
因为对应的IO action要执行的顺序是undefined.
在实践中,有时在组合事件和行为的同时执行 IO 可能很有用。 execute
组合器可以做到这一点,正如@ErikAllik 所指出的那样。根据 getAnotherRemoteApiValue
的性质,这可能是正确的做法,特别是如果此函数是幂等的,或者从 RAM 中的位置进行快速查找。
不过,如果计算比较复杂,那么用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
还保证输入和结果同时发生,但对允许的 IO 进行了非正式限制。
通过将 reactimate
与 newEvent
组合的技巧,可以以类似的方式为行为编写类似的组合器。请记住,Reactive.Banana.Frameworks
中的工具箱仅适用于处理其精确顺序必然未定义的 IO 操作时。
(为了使这个答案保持最新,我使用了即将推出的 reactive-banana 1.0 的类型签名。在 0.9 版中,mapIO'
的类型签名是
mapIO' :: Frameworks t => (a -> IO b) -> Event t a -> Moment t (Event t b)
)