haskellreactive-programmingfrpreactive-banana

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


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?


Solution

  • 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