问题
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 Behavior
s 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