6

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

I have a bit different problem now: How can I use the Behaviour output as input for an IO operation and finally display the IO operation's result?

Below is the code from the previous answer changed with a second output:

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

As you can see what I am trying to do it is using the new state of the application -> getAnotherRemoteApiValue -> show. But it doesn't work.

Is actually possible doing that?

UPDATE Based on the Erik Allik and Heinrich Apfelmus below answers I have the current code situation - that works :) :

{-# 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
Community
  • 1
  • 1
Randomize
  • 8,651
  • 18
  • 78
  • 133
  • 1
    just for the record: _"it doesn't work"_ is not an error message — you're not even indicating if it's a compilation or runtime error, or if there's in fact even an error or whether you're just not seeing the wanted behavior. – Erik Kaplun Oct 03 '15 at 07:32
  • You are right sorry. As partial excuse I am having this issue with Haskell Platform and new 'El Capitan' MacOsX that's why I cannot reproduce errors :( : http://stackoverflow.com/questions/32920452/how-to-run-haskell-on-osx-el-capitan – Randomize Oct 03 '15 at 08:46
  • what's wrong with `brew install ghc` or smth? do you have Homebrew? if not, consider giving it a try. you won't get far if you don't have Haskell installed... Also, I recommend using Stackage/`stack` on top of raw Hackage/`cabal`, and therefore Haskell Platform isn't recommended because Stackage/`stack` doesn't recommend it. Just go with Homebrew GHC, or let Stack install it's own local version(s) of GHC, or both. – Erik Kaplun Oct 03 '15 at 08:54
  • yes tried with brew but now cabal doesn't work (some new permissions errors like for /usr/bin/ar, no found versions of cabal self, etc). Quite a mess at the moment – Randomize Oct 03 '15 at 09:29
  • it would make sense to type annotate `mapIO` with a generic type, i.e. `mapIO :: (a -> IO b) -> Event t a -> Moment t (Event t b)`, as in Heinrich's original example, which is better than your current, monomorphic signature. (also, I've fixed an error in your code where you're calling `mapIO` — it seems you still haven't sorted the Haskell+El Capitan issue :) – Erik Kaplun Oct 05 '15 at 09:22
  • LOL yes Haskell+El Capitan is still a work in progress. I used brew to install cabal/stack (and other tools from cabal) + GHC and I have tried to compile reactive-banana-wx with the new GHC-7.10.2. The problem is that cabal-macosx has a 'fgl' version not updated (<5.0) in its cabal file and rbwx uses 0.1.0 that doesn't compile cos of a `catch` error. If you update stack.yaml of rbwx to use version 0.2.3 of cabal-macosx the error is gone, but you have to change c-macosx's cabal file first, to use the right fgl. But it looks like the project cabal-macosx is dead so need local compile of it first – Randomize Oct 05 '15 at 10:02
  • what's this project anyway? learning? school? hobby? I see you've left in all of the dummy code and `putStrLn`s. – Erik Kaplun Oct 08 '15 at 11:53
  • it's a learning/hobby project. Anyway I am just re-adapting the code to something else. – Randomize Oct 11 '15 at 09:30
  • have a public repo of it? – Erik Kaplun Oct 11 '15 at 09:41
  • 1
    Yes there is a public repo with two branches: master with the current old centralized global state and the "temp_backup" branch that is using reactive-banana. Anyway this temp_backup is still a work in progress so it might be no clear at all :). BTW at the moment looks reactive-banana-wx doesn't work with Tool Menu Icons. https://github.com/danfran/hxkcd – Randomize Oct 12 '15 at 09:27
  • Have you also evaluated Sodium, Reflex, Netwrie or Yampa? – Erik Kaplun Oct 12 '15 at 11:21
  • Reactive-Banana is my first FRP with Haskell, but I have started to consider Sodium too (although it looks discontinued for Haskell) and Yampa. I didn't check the other two yet. – Randomize Oct 12 '15 at 12:19
  • @ErikAllik can Sodium be considered the "standard de facto" for FRP in Haskell? – Randomize Oct 12 '15 at 14:14
  • I don't know; I was just curious because there doesn't seem to be too much comparative information. – Erik Kaplun Oct 12 '15 at 15:21
  • 1
    first chapter here (free to download) https://www.manning.com/books/functional-reactive-programming it gives you some info about reactive-banana and sodium and frp in general. – Randomize Oct 12 '15 at 16:11

2 Answers2

3

The fundamental problem is a conceptual one: FRP Events and Behaviors can only be combined in a pure way. In principle, it is not possible to have a function of type, say

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

because the order in which the corresponding IO actions are to be executed is undefined.


In practice, it may sometimes be useful to perform IO while combining Events and Behaviors. The execute combinator can do this, as @ErikAllik indicates. Depending on the nature of getAnotherRemoteApiValue, this may be the right thing to do, in particular if this is function is idempotent, or does a quick lookup from location in RAM.

However, if the computation is more involved, then it is probably better to use reactimate to perform the IO computation. Using newEvent to create an AddHandler, we can give an implementation of the mapIO' function:

mapIO' :: (a -> IO b) -> Event a -> MomentIO (Event b)
mapIO' f e1 = do
    (e2, handler) <- newEvent
    reactimate $ (\a -> f a >>= handler) <$> e1
    return e2

The key difference to the pure combinator

fmap :: (a -> b) -> Event a -> Event b

is that the latter guarantees that the input and result events occur simultaneously, while the former gives absolutely no guarantee about when the result event occurs in relation to other events in the network.

Note that execute also guarantees that input and result are have simultaneous occurrences, but places informal restrictions on the IO allowed.


With this trick of combining reactimate with newEvent a similar combinator can be written for Behaviors in a similar fashion. Keep in mind that the toolbox from Reactive.Banana.Frameworks is only appropriate if you are dealing with IO actions whose precise order will necessarily be undefined.


(To keep this answer current, I have used the type signatures from the upcoming reactive-banana 1.0. In version 0.9, the type signature for mapIO' is

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

)

Heinrich Apfelmus
  • 11,034
  • 1
  • 39
  • 67
  • When you say "if the computation is more involved" about the `reactimate` usage, are you referring to "time duration"? As it is involved a remote API the response time can be variable (milli seconds, seconds, never). If this is what you meant, the only solution is just using mapIO/reactimate. – Randomize Oct 03 '15 at 21:38
  • 1
    Well, it always depends on what you want to do. Time duration is one criterion: It should be much shorter than the typical time frame in which an external Event occurs. If your computation connects to the internet and waits for results, then this is most likely unsuitable for use with `execute`, because there will be other events coming in in the meantime that the network needs to handle, but can't, because it is stuck in the IO action. – Heinrich Apfelmus Oct 04 '15 at 07:42
  • @HeinrichApfelmus: so based on your explanation, I take it it's not a good idea to use `execute` for possibly expensive calls, so my answer's not really valid in the context of this particular question? – Erik Kaplun Oct 04 '15 at 08:39
  • @ErikAllik Yup. To be fair, the question did not specify this. Your explanation of getting a Behavior from the Event is still useful, though. – Heinrich Apfelmus Oct 04 '15 at 14:28
  • @HeinrichApfelmus: I am using at the moment the version 0.9 of RR, please look again at my question as I have updated it for readability reason. – Randomize Oct 04 '15 at 19:29
  • @Randomize I have added the type signature for 0.9 – Heinrich Apfelmus Oct 05 '15 at 07:55
  • @HeinrichApfelmus: wouldn't it make sense to include `mapIO` in `reactive-banana`? IO seems like a big deal, and the more of the boilerplate for it that has been taken care of by `reactive-banana`, and that is otherwise hard to get right and easy to get wrong, given the tricky relationship between FRP and IO, the easier it is to build real life apps with the library. For example, it's likely I'd have found about everything you said in a trial and error manner, after my FRP app started misbehaving because I was using `execute` with slow IO. – Erik Kaplun Oct 05 '15 at 09:14
  • @HeinrichApfelmus: ok, I think there is a misunderstanding. You are talking about `mapIO` right? So this signature `Control.Event.Handler.mapIO :: (a -> IO b) -> AddHandler a -> AddHandler b`. I was talking about `reactimate` (so I rewrote in the code `mapIO` that should be unnecessary at this point). – Randomize Oct 05 '15 at 09:27
  • 1
    @ErikAllik Can you make an issue on the issue tracker? – Heinrich Apfelmus Oct 06 '15 at 08:25
  • @Randomize Ah, sorry, my code contained a mistake, I forgot to call the `handler` function. I have fixed it in the answer. Also, this is a different `mapIO` function than then one in `Control.Event.Handler`, although they serve such a similar purpose that I have given them the same name. I'll add a tick at the end. – Heinrich Apfelmus Oct 06 '15 at 08:28
  • @HeinrichApfelmus thank you. I didn't tried your code yet but that makes sense. The value from the "remote api" is carried from a new event binding an "IO-handler". Anyway my `MapIO` signature is slightly different than yours. I assume you are keep on referring to the new API 1.0 version. – Randomize Oct 06 '15 at 08:43
  • @HeinrichApfelmus: conceptually, what separates `(a -> IO b) -> Event a -> Event b` from `(a -> IO b) -> Event a -> MomentIO (Event b)` in that latter can exist and the former not? It must be something obvious but your wording of it will probably help. – Erik Kaplun Oct 06 '15 at 10:34
  • @Randomize You didn't need to add the `Frameworks t` constraint, because the type variable `t` was already in scope with this constraint. If you make `mapIO'` a top-level definition , then the constraint would have to be added. – Heinrich Apfelmus Oct 08 '15 at 08:12
  • @ErikAllik Yes. Note that in the latter, the input `Event a` and the output `Event b` will not be simultaneous. Essentially, this is the underlying reason why the former cannot exist. – Heinrich Apfelmus Oct 08 '15 at 08:15
  • So the `MomentIO` wrapping is what's making `Event b` non-simultaneous with `Event a`? Or I guess I should read the source or smth... – Erik Kaplun Oct 08 '15 at 11:42
  • @ErikAllik Not necessarily. The `MomentIO` type is just an indication that something with IO happens, which means that in order to understand what a program does, order of execution has to be taken into account. In this case, the result is a lack of simultaneity. – Heinrich Apfelmus Oct 09 '15 at 08:23
1

TL;DR: scroll down to the ANSWER: section for a solution along with an explanation.


First of all

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

is invalid (i.e. does not typecheck) for reasons completely unrelated to FRP or reactive-banana: you cannot add an Int to an IO Int — just as you can't apply mod 10 to an IO Int directly, which is exactly why, in the answer to your original question, I used <$> (which is another name for fmap from Functor).

I strongly recommend you look up and understand the purpose/meaning of <$>, along with <*> and some other Functor and Applicative type class methods — FRP (at least the way it is designed in reactive-banana) builds heavily upon Functors and Applicatives (and sometimes Monads, Arrows and possibly some other more novel foundation), hence if you don't completely understand those, you won't ever become proficient with FRP.

Secondly, I'm not sure why you're using coreOfTheApp for sink output2 ... — the coreOfTheApp value is related to the other API value.

Thirdly, how should the other API value be displayed? Or, more specifically, when should it be displayed? Your first API value is displayed when the button is clicked but there's no button for the second one — do you want the same button to trigger the API call and display update? Do you want another button? Or do you want it to be polled every n unit of time and simply auto-updated in the UI?

Lastly, reactimate is meant for converting a Behavior into an IO action, which is not what you want, because you already have the show helper and don't need to setText or smth on the static label. In other words, what you need for the second API value is the same as before, except you need to pass something from the app state along with the request to the external API, but aside from that difference, you can still just keep showing the (other) API value using show as normal.


ANSWER:

As to how to convert getAnotherRemoteApiValue :: AppState -> IO RemoteValue into an Event t Int similar to the original remoteValueE:

I first tried to go via IORefs and using changes+reactimate', but that quickly turned out to a dead end (besides being ugly and overly complicated): output2 was always updated one FRP "cycle" too late, so it was always one "version" behind in the UI.

I then, with the help of Oliver Charles (ocharles) on #haskell-game on FreeNode, turned to execute:

execute :: Event t (FrameworksMoment a) -> Moment t (Event t a)

which I still don't fully grasp yet, but it works:

let x = fmap (\s -> FrameworksMoment $ liftIO $ getAnotherRemoteApiValue s)
             (appStateB <@ ebt)
remoteValue2E <- execute x

so the same button would trigger both actions. But the problem with that quickly turned out to be the same as with the IORef based solution — since the same button would trigger a pair of events, and one event inside that pair depended on the other, the contents of output2 was still one version behind.

I then realised the events relatede to output2 need to be triggered after any events related to output1. However, it's impossible to go from Behavior t a -> Event t a; in other words, once you have a behavior, you can't (easily?) obtain an event from that (except with changes, but changes is tied to reactimate/reactimate', which is not useful here).

I finally noticed that I was essentially "throwing away" an intermediate Event at this line:

appStateB = accumB initialState $ transformState <$> remoteValue1E

by replacing it with

appStateE = accumE initialState $ transformState <$> remoteValue1E
appStateB = stepper initialState -- there seems to be no way to eliminate the initialState duplication but that's fine

so I still had the exact same appStateB, which is used as previously, but I could then also rely on appStateE to reliably trigger further events that rely on the AppState:

let x = fmap (\s -> FrameworksMoment $ liftIO $ getAnotherRemoteApiValue s)
             appStateE
remoteValue2E <- execute x

The final sink output2 line looks like:

sink output2 [text :== show <$> remoteValue2B] 

All of the code can be seen at http://lpaste.net/142202, with debug output still enabled.

Note that the (\s -> FrameworkMoment $ liftIO $ getAnotherRemoteApiValue s) lambda cannot be converted to point-free style for reasons related to RankN types. I was told this problem will go away in reactive-banana 1.0 because there will be no FrameworkMoment helper type.

Erik Kaplun
  • 37,128
  • 15
  • 99
  • 111
  • At the moment I cannot compile anything so I am trying to explain what I was trying to do: Once the button is pressed you have the "coreOfTheApp" behaviour that in the first output just show you the current count. In the second output you pipe that behaviour content to another IO remote function in a form of parameter to get another output to display – Randomize Oct 03 '15 at 09:38
  • mapIO/Handler should be what I need for: http://hackage.haskell.org/package/reactive-banana-0.9.0.0/docs/Control-Event-Handler.html#t:AddHandler. Streaming the event in the IO function (instead of the Behavior). – Randomize Oct 03 '15 at 10:01
  • OK, thanks for the explanation — so, effectively, the same button should trigger both API calls; it's just that the new one (obviously) needs to happen the original one because the original one updates the state and the new one relies on the state when doing the API call. I'll also take a look at `mapIO`/`Handler`. – Erik Kaplun Oct 03 '15 at 10:09
  • To be honest, I cannot seem to figure out how to achieve `(Behavior t a, a -> IO b) -> Behavior t b` or `(Event t a, a -> IO b) -> Event t b` or equivalent... at least not with Reactive Banana — Sodium and Reflex seem to have more useful functions to get something like that but not Reactive Banana. – Erik Kaplun Oct 03 '15 at 10:47
  • In other words, Sodium has `executeAsyncIO :: Event r (IO a) -> Event r a` (and a sync version thereof) but Reactive Banana seems to have nothing similar. I'm trying to figure out if I can achieve the same using a combination of `changes` and `reactimate`, or similar. – Erik Kaplun Oct 03 '15 at 10:50