Hacker News new | past | comments | ask | show | jobs | submit login
More fun with reactive-banana-automation (joeyh.name)
138 points by edward on May 6, 2018 | hide | past | favorite | 17 comments



What I love about Monad and Applicative interfaces is that I can read that code and almost immediately fully grasp what’s going on without having a clue about the underlying library.

There are a bunch of (well named) functions and control flow. No boilerplate.


> no boilerplate

Well, compare to

    PowerChange inverterPowerChange(Sensors *sensors)
    {
        if (sensors->lowpower)
            return PowerOff;
        if (sensors->wifiusers > 0)
            return PowerOn;
        return sensors->fridgePowerChange;
    }
in a more traditional language. Nobody would make a fuss about that code.


You've written the react function:

    react lowpower fridgepowerchange wifiusers
            | lowpower = Just PowerOff
            | wifiusers > 0 = Just PowerOn
            | otherwise = fridgepowerchange
The big deal with inverterPowerChange is this line:

    return $ react <$> lowpower <*> fridgepowerchange <*> wifiusers
It binds values emitted by the lowpower, fridgepowerchange, and wifiusers Behaviors, and wraps around it, returning a Behavior that itself emits values.


It doesn't do that. The one in the post composes a bunch of other Behaviors, see the lowPower one further down in the article for an example, it's not polling a bunch of variables. Then it is all wired up to react to events.


I think that, while the lack of type annotations on every variable is great since hiding boilerplate is great, it could be confusing to the uninitiated.

To elaborate, lowpower etc. on this line are Behaviors, which emit values by time:

    react <$> lowpower <*> fridgepowerchange <*> wifiusers
while lowpower, fridgepowerchange, wifiusers in the react function bind to the values that are emitted by the Behaviors. (Just by looking at the code we can infer that their types are Bool, (Num a, Ord a) => a, and PowerChange respectively.)

    react lowpower fridgepowerchange wifiusers
            | lowpower = Just PowerOff
            | wifiusers > 0 = Just PowerOn
            | otherwise = fridgepowerchange
None of these are simply state variables!

Although this might have confused GP, I think this is nice since we can associate the name 'lowpower' for its semantics (The low power signal) and not have to worry about "this lowpower is a Behavior while that lowpower is a Bool".


So is it confusing or obvious? Pick one


The code behavior is obvious once you know Haskell. The code simplicity confuses people that don't know the language and think it is something much less powerful than it actually is.


In that case, why not just write everything in perl?


We actually did for a long time. It wasn't so bad, I guess? Certainly not worse than Python, to me.

But the value of writing this kind of code in Haskell is that Haskell makes code that couples very abstract interfaces VERY easily. This is really hard to explain without some examples, so I'm going to give you an example.

Here, let me show you from real code. I'm going to tweak some endpoint names, but this is real code I'm going to ship next week: https://gist.github.com/KirinDave/9b4380376f9f748bcf0f8440de...

What's cool about this code is that I'm describing a "generic" radAPI service with a bunch of endpoints stripped out. Everything is parameterized around 3 generic ideas: "p", the Storage service. "Patchable", a strategy for applying patches to a storage service (exposing runPatch), and "CanValidate", a strategy for authenticating requests based on some sort of stored string, auth headers, and body contents.

This is even better than mocking, because instead of deeply coupling the structure of my code to my tests, I only couple the capabilities.


The joy of Perl!


The reactive-banana code does more things; it also does the equivalent (in Observer pattern terms) of adding observers to the corresponding observables.


I have a small quibble about the names though—or perhaps I didn't fully understand the example.

Since "inverterPowerChange" returns a Behaviour http://hackage.haskell.org/package/reactive-banana-1.2.0.0/d... which is supposed to be "inspectable" at any point in time, shouldn't PowerChange, PowerOff and PowerOn actually be PoweredState, PoweredOff and PoweredOn? The former sound more like event names. Or perhaps should "inverterPowerChange" return an Event instead? http://hackage.haskell.org/package/reactive-banana-1.2.0.0/d...


Another way to look at this is that while the Behavior PowerChange is continuous, once it gets wired up to a switch or something, the reaction to the Behavior at any point may change the state of the switch (particularly if something else might have affected the state; including eg some human pressing a physical button). So a Behavior PowerChange is the potential changes that might occur.

Still, I will think about renaming PowerChange, perhaps it could be clearer.


Yes, I think your alternate names make more sense when you think about `Behavior`s only. That said, from what I understand of FRP you don't want to think of the data type as "type T is for Behaviors" or "type S is only for Events", because it's so easy to compose them. If I have even an `Event ()` (an event source with no data beyond the fact that it fired), I can use (<@) to view the `Behavior T`, and now I have an `Event T`.

If the function in OP was returning an Event, it would be a sequence of instructions to the freezer: "turn on now", "turn off now" and so on. As a Behavior, it's more like a set of times when the fridge should be on or off.


Ah I see. Events form a Monoid, so I guess I could have a "something happens" event aggregating all the sensor updates, and sample the "inverterPowerChange" Behaviour at the occurrences of the Event.

The API for reactive-banana doesn't seem to have a "value changed" combinator that creates an Event from a Behaviour alone; one must sample the Behaviour with another Event.

(Other FRP libraries somewhat similar in philosophy like reflex—not the JS Reflex—do seem to have Behaviour-like things that can fire events on change: http://hackage.haskell.org/package/reflex-0.4.0.1/docs/Refle... Although reactive-banana seems simpler to understand.)


reactive-banana does have a function from Behavior a -> Event (Future a) but that Future gets in the way and removing it is only for wizards.

On the other hand, I think that's necessary for mutually recursive definitions of behaviors and events to reach a fixed point, which is quite a neat feature although not one I've needed yet.


A piece of folk wisdom I picked up along the way was you often end up needing a "Behavior a" and an "Event a" that fires when the Behavior changes. The reason I switched from dabbling-with-reactive-banana to dabbling-with-reflex is exactly that I needed "Dynamic"s.The semantics for Behavior require that it doesn't appear to change value until the frame has finished updating, so you either need to keep an Event around that fires the "new value" in the current frame or use a more powerful library.

Remark: Event is a Monoid or a Semigroup only when the contained value is either a Semigroup. The most useful combining ability comes from Behavior's Applicative instance.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: