6

从这里的上一个问题开始: 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

正如您所看到的,我正在尝试使用应用程序的新状态 -> 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
4

2 回答 2

3

基本问题是一个概念问题:FRP 事件和行为只能以纯粹的方式组合。原则上,不可能有类型的功能,比如说

mapIO' :: (a -> IO b) -> Event a -> Event b

因为相应 IO 动作的执行顺序是未定义的。


在实践中,有时在组合事件和行为时执行 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类似组合器组合的技巧,可以以类似的方式为 Behaviors 编写。请记住,Reactive.Banana.Frameworks仅当您正在处理其精确顺序必然未定义的 IO 操作时,工具箱才适用。


(为了使这个答案保持最新,我使用了即将发布的响应式香蕉 1.0 中的类型签名。在 0.9 版中,类型签名mapIO'

mapIO' :: Frameworks t => (a -> IO b) -> Event t a -> Moment t (Event t b)

)

于 2015-10-03T18:57:25.890 回答
1

TL;DR:向下滚动到ANSWER:部分以获取解决方案和解释。


首先

getAnotherRemoteApiValue state = (`mod` 10) <$> randomIO + count state

由于与 FRP 或响应式香蕉完全无关的原因而无效(即不进行类型检查):您不能将 an 添加Int到 an IO Int- 就像您不能直接应用于mod 10an一样IO Int,这正是为什么,在您原来的问题的答案中,我用过<$>(这是fmapfrom的另一个名字Functor)。

我强烈建议您查看并理解<$>, 以及其他<*>一些类型类方法的目的/含义 - FRP(至少它在反应香蕉中的设计方式)在很大程度上建立在 Functors 和 Applicatives (有时还有 Monads、Arrows可能还有其他一些更新颖的基础),因此,如果您不完全理解这些,您将永远不会精通 FRP。FunctorApplicative

其次,我不确定您为什么要使用coreOfTheAppfor sink output2 ...— 该coreOfTheApp值与其他 API 值相关。

第三,其他API值应该如何显示?或者,更具体地说,应该何时显示?单击按钮时会显示您的第一个 API 值,但没有第二个按钮 - 您是否希望同一个按钮触发 API 调用并显示更新?你想要另一个按钮吗?或者您是否希望每个n时间单位都对其进行轮询并在 UI 中简单地自动更新?

最后reactimate用于将 aBehavior转换为IO动作,这不是您想要的,因为您已经有了show帮助程序,并且不需要setText在静态标签上进行操作。换句话说,您需要的第二个 API 值与以前相同,只是您需要将应用程序状态中的某些内容与请求一起传递给外部 API,但除此之外,您仍然可以继续显示(其他)API 值show正常使用。


回答:

至于如何转换getAnotherRemoteApiValue :: AppState -> IO RemoteValueEvent t Int类似于原来的remoteValueE

我首先尝试通过IORefs 并使用changes+ reactimate',但这很快就变成了死胡同(除了丑陋和过于复杂之外):output2总是更新一个 FRP“周期”太晚了,所以它总是落后一个“版本”用户界面。

然后,在 FreeNode 上 #haskell-game 的 Oliver Charles (ocharles) 的帮助下,我转向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; 换句话说,一旦你有一个行为,你就不能(很容易?)从中获得一个事件(除了 with changes,但changesreactimate/相关联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 类型相关的原因,无法将 lambda 转换为无点样式。有人告诉我这个问题将在反应香蕉 1.0 中消失,因为不会有FrameworkMoment辅助类型。

于 2015-10-03T08:28:50.923 回答