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,这就是为什么在回答您原来的问题时,我使用了 <$>(这是 Functorfmap 的另一个名称) .

我强烈建议您查找并理解 <$> 的 purpose/meaning,以及 <*> 和其他一些 FunctorApplicative 类型 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转换成类似于原来的remoteValueEEvent t Int

我首先尝试通过 IORefs 并使用 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,但是 changesreactimate/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 进行了非正式限制。


通过将 reactimatenewEvent 组合的技巧,可以以类似的方式为行为编写类似的组合器。请记住,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)

)