【问题标题】:Reactive Banana: consume parametrized call to an external APIReactive Banana:使用对外部 API 的参数化调用
【发布时间】:2015-12-31 05:29:32
【问题描述】:

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

【问题讨论】:

  • 仅作记录:"it doesn't work" 不是错误消息——您甚至没有指出这是编译错误还是运行时错误,或者是否存在实际上甚至是一个错误,或者您是否只是没有看到想要的行为。
  • 你说得对,对不起。作为部分借口,我在 Haskell 平台和新的“El Capitan”MacOsX 上遇到了这个问题,这就是我无法重现错误的原因:(:stackoverflow.com/questions/32920452/…
  • brew install ghc 有什么问题吗?你有家酿吗?如果没有,请考虑尝试一下。如果你没有安装 Haskell,你不会走得太远......另外,我建议在原始 Hackage/cabal 之上使用 Stackage/stack,因此不建议使用 Haskell 平台,因为 Stackage/@987654332 @不推荐。只需使用 Homebrew GHC,或让 Stack 安装它自己的 GHC 本地版本,或两者兼而有之。
  • 是的,有一个带有两个分支的公共 repo:具有当前旧集中式全局状态的 master 和使用响应式香蕉的“temp_backup”分支。无论如何,此 temp_backup 仍在进行中,因此可能根本不清楚 :)。顺便说一句,目前看起来 reactive-banana-wx 不适用于工具菜单图标。 github.com/danfran/hxkcd
  • 这里的第一章(免费下载)manning.com/books/functional-reactive-programming 它为您提供了一些关于反应性香蕉、钠和玻璃钢的一般信息。

标签: haskell reactive-programming frp reactive-banana


【解决方案1】:

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

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

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


在实践中,有时在组合事件和行为时执行 IO 可能很有用。正如@ErikAllik 所指出的,execute 组合器可以做到这一点。根据 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 操作。


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

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

)

【讨论】:

  • 当您对reactimate 的用法说“如果计算更多”时,您指的是“持续时间”吗?由于涉及远程 API,响应时间可以是可变的(毫秒、秒、从不)。如果这是您的意思,唯一的解决方案就是使用 mapIO/reactimate。
  • 嗯,这总是取决于你想做什么。持续时间是一个标准:它应该比发生外部事件的典型时间范围短得多。如果您的计算连接到互联网并等待结果,那么这很可能不适合与execute 一起使用,因为在此期间会有其他事件进入网络需要处理,但不能,因为它卡在 IO 动作中。
  • @HeinrichApfelmus:所以根据你的解释,我认为使用execute 来处理可能很昂贵的电话不是一个好主意,所以我的回答在这个特定问题的上下文中不是真的有效?
  • @ErikAllik 是的。公平地说,这个问题没有具体说明这一点。不过,您对从事件中获取行为的解释仍然有用。
  • @ErikAllik 你能在问题跟踪器上提出问题吗?
【解决方案2】:

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


首先

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

由于与 FRP 或响应式香蕉完全无关的原因而无效(即不进行类型检查):您不能将 Int 添加到 IO Int — 就像您不能将 mod 10 应用于 IO Int直接,这就是为什么在回答您最初的问题时,我使用了&lt;$&gt;(这是fmap 的另一个名称,来自Functor)。

我强烈建议您查找并理解 &lt;$&gt; 的目的/含义,以及 &lt;*&gt; 和其他一些 FunctorApplicative 类型类方法 — FRP(至少它是在响应式中设计的方式) -banana) 大量建立在 Functors 和 Applicatives(有时还有 Monads、Arrows 和其他更新颖的基础)之上,因此如果你不完全理解这些,你永远不会精通 FRP。

其次,我不知道您为什么将coreOfTheApp 用于sink output2 ...coreOfTheApp 值与其他 API 值相关。

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

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


回答:

至于如何将getAnotherRemoteApiValue :: AppState -&gt; IO RemoteValue转换成类似于原remoteValueEEvent t Int

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

然后,在 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 -&gt; Event t a 出发是不可能的;换句话说,一旦你有一个行为,你就不能(很容易?)从中获取一个事件(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 看到,调试输出仍然启用。

请注意,由于与 RankN 类型相关的原因,(\s -&gt; FrameworkMoment $ liftIO $ getAnotherRemoteApiValue s) lambda 无法转换为无点样式。有人告诉我这个问题会在反应香蕉 1.0 中消失,因为不会有 FrameworkMoment 辅助类型。

【讨论】:

  • 目前我无法编译任何东西,所以我试图解释我想要做的事情:一旦按下按钮,你就会有“coreOfTheApp”行为,在第一个输出中只显示当前数数。在第二个输出中,您将该行为内容以参数的形式传递给另一个 IO 远程函数,以获取另一个输出来显示
  • mapIO/Handler 应该是我需要的:hackage.haskell.org/package/reactive-banana-0.9.0.0/docs/…。在 IO 函数中流式传输事件(而不是 Behavior)。
  • 好的,感谢您的解释——因此,实际上,同一个按钮应该触发两个 API 调用;只是新的(显然)需要发生原始的,因为原始的更新状态,而新的在执行 API 调用时依赖于状态。我也来看看mapIO/Handler
  • 老实说,我似乎无法弄清楚如何实现 (Behavior t a, a -&gt; IO b) -&gt; Behavior t b(Event t a, a -&gt; IO b) -&gt; Event t b 或等效项......至少对于 Reactive Banana 来说没有——Sodium 和 Reflex 似乎有更有用的功能得到类似的东西,但不是 Reactive Banana。
  • 换句话说,Sodium 有 executeAsyncIO :: Event r (IO a) -&gt; Event r a(及其同步版本),但 Reactive Banana 似乎没有类似的东西。我试图弄清楚我是否可以使用changesreactimate 或类似的组合来实现相同的效果。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-11-25
  • 1970-01-01
  • 2018-11-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多