Reactive Banana: how to use values from a remote API and merge them in the event stream

心不动则不痛 提交于 2019-12-21 17:22:33

问题


I am using Reactive-Banana in a WX interface. I need to retrieve a value from an external service API when a button is pressed.

I have a generic Behavior based on the data type AppState that “accums” the transformed changes based on a function transformation (doSomeTransformation). The values that get transformed are transported by the events and they come from a remote API (getRemoteValue) when a button on the interface is pressed. I have written a slim version of the code that represents the essential part:

module Main where

{-# LANGUAGE ScopedTypeVariables #-} -- allows "forall t. Moment t"

import Graphics.UI.WX hiding (Event)
import Reactive.Banana
import Reactive.Banana.WX

{-----------------------------------------------------------------------------
    Main
------------------------------------------------------------------------------}
data AppState = AppState {
    count :: Int
} deriving (Show)

type String = [Char]

main :: IO ()
main = start $ do
    f        <- frame [text := "AppState"]
    myButton <- button f [text := "Go"]
    output   <- staticText f []

    set f [layout := margin 10 $
            column 5 [widget myButton, widget output]]

    let networkDescription :: forall t. Frameworks t => Moment t ()
        networkDescription = do

        ebt   <- event0 myButton command

        remoteValueB <- fromPoll getRemoteApiValue
        myRemoteValue <- changes remoteValueB

        let            
            doSomeTransformation :: AppState -> AppState
            doSomeTransformation ast = ast { count = count ast }

            coreOfTheApp :: Behavior t AppState
            coreOfTheApp = accumB initialState $ (doSomeTransformation to combine with myRemoteValue) <$ ebt

        sink output [text :== show <$> coreOfTheApp]

    network <- compile networkDescription    
    actuate network

getRemoteApiValue :: IO Int
getRemoteApiValue = return 5

and the cabal conf:

name:                brg
version:             0.1.0.0
synopsis:            sample frp gui
-- description:
license:             PublicDomain
license-file:        LICENSE
author:              me
maintainer:          me@gmail.com
-- copyright:
category:            fun
build-type:          Simple
-- extra-source-files:
cabal-version:       >=1.10

executable bgr
  main-is:             Main.hs
  -- other-modules:
  -- other-extensions:
  build-depends:       base >=4.7 && <4.8
                       , text
                       , wx ==0.92.0.0
                       , wxcore ==0.92.0.0
                       , transformers-base
                       , reactive-banana >=0.9 && <0.10
                       , reactive-banana-wx ==0.9.0.2
  hs-source-dirs:      src
  default-language:    Haskell2010
  ghc-options:         -Wall -O2

My problem here is how to compose doSomeTransformation and myRemoteValue in a way that I can use the remote API value as normal event value. changes from banana-reactive has the following signature:

changes :: Frameworks t => Behavior t a -> Moment t (Event t (Future a))

which it will wrap my IO Int from getRemoteApiValue.

So basically how can I go from:

IO Int -> Moment t (Event t (Future AppState)) -> AppState

?

BTW I am not sure if it is cleaner having this different function signature: doSomeTransformation :: Int -> AppState -> AppState, where the Int value is represented by the API returned value. It sounds like two Behaviors and one stream. Maybe a bad way to solve the problem?


回答1:


Short answer: the transform function needs to take one more argument, the value from the API:

transformState v (AppState x) = AppState $ x + v

and you need to use <$> (i.e. apply function) instead of <$ (i.e. overwrite with constant value):

accumB (AppState 0) $ transformState <$> remoteValueB <@ ebt

Long answer:

Note: I've renamed/changed a few things so please read my explanation accordingly

What needs to be changed is the way you fold over the incoming values using accumB. The way accumB works is that it applies a sequence of functions a -> a to a seed value a, to compute a final value of type a. The way you are currently folding over the API values is by always applying the app state count increment function to the initial state, completely throwing away the incoming value (by using <$). Instead you need to map the incoming value not replace it, using <$>. What do you need to map the value to? A function (as per the type of accumB)! And that function is transformValue eventValue :: AppState -> AppState.

A lists and folds based example:

*Frp> data State = State Int deriving Show
*Frp> let transform x (State c) = State $ x + c
*Frp> let xs = [1, 2, 3, 4, 5]                       -- the API values
*Frp> let xsE = transform <$> xs :: [State -> State] -- the event stream
*Frp> let accumB = foldr ($)
*Frp> accumB (State 0) xsE
State 15

(don't forget that a <$> b is the same as fmap a b, or just map a b in the case of lists)

Now consider how you are currently "overwriting" any events from remoteValueB <@ ebt with the (function) constant transformState, which means that all the overwritten events that arrive always hold the same content: the transformState function.

Instead, what you want is to map the incoming values to some actual functions, for example one that takes the old state and combine it to the arrived value and yields a new state value:

remoteValueE :: Event t Int
remoteValueE = remoteValueB <@ ebt

transformsE :: Event t (AppState -> AppState)
transformsE = transformState <$> remoteValueE

coreOfTheApp :: Behavior t AppState
coreOfTheApp = accumB initialState $ transformsE

I've also changed getRemoteApiValue to return a changing value to imitate a real API. So with some modifications to your code, here's something that works:

import System.Random

type RemoteValue = Int

-- generate a random value within [0, 10)
getRemoteApiValue :: IO RemoteValue
getRemoteApiValue = (`mod` 10) <$> randomIO

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 []

    set f [layout := minsize (sz 300 200)
                   $ margin 10
                   $ column 5 [widget myButton, widget output]]

    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] 

    network <- compile networkDescription    
    actuate network


来源:https://stackoverflow.com/questions/32806125/reactive-banana-how-to-use-values-from-a-remote-api-and-merge-them-in-the-event

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!