反应性香蕉:消耗对外部API

时间:2015-10-02 22:26:02

标签: haskell reactive-programming frp reactive-banana

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

2 个答案:

答案 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进行非正式限制。

通过将executereactimate结合起来的技巧,可以以类似的方式为行为编写类似的组合子。请记住,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除外),但output2output1 / {{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助手类型。