New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move away from a parametric approach to trimming (preferring a monadic approach) #97

Closed
ocharles opened this Issue May 27, 2015 · 12 comments

Comments

Projects
None yet
4 participants
@ocharles
Collaborator

ocharles commented May 27, 2015

This is still for discussion I believe, but here's the discussion I had with @HeinrichApfelmus:


Hi Heinrich,

Finally having another chance to play around more with reactive-banana for putting together web UIs and continuing to really enjoy it. The only pain point is mostly around dynamic event switching and the necessity of trimming. As a recent example, I have a list of strings that changes over time, and I need a set of toggle buttons for each of these strings in the set. Currently, this leads to code such as:

    do futureClassTypeToggles <-
         do never' <- trim never
            execute (fmap (foldlM (\(classTypeSetChanged,html) classType ->
                                     FrameworksMoment
                                       (do c <-
                                             construct (ToggleButton classType) :: Moment t (InstantiatedComponent t ToggleButton)
                                           trimmedHTML <-
                                             trim (observeHTML (render c)) :: Moment t (AnyMoment Behavior 
                                           trimmedUpdate <-
                                             trim (fmap (bool (filter (/= classType)) (classType :))
                                                        (toggled (outputs c)))
                                           return (anyMoment (liftA2 union
                                                                     (now classTypeSetChanged)
                                                                     (now trimmedUpdate))
                                                  ,liftA2 (<>) html trimmedHTML)))
                                  (never',anyMoment (pure mempty)))
                          classTypesChanged)
       pure Instantiation {render =
                             embed (switchB mempty (fmap snd futureClassTypeToggles))
                          ,outputs =
                             ClassTypesSelectorOut
                               (accumB [] (switchE (fmap fst futureClassTypeToggles)))}

Which is rather verbose for what it's doing! Here classTypesChanged :: Event t [String], and I fold this set into both an Event t ([String] -> [String]) (to update the users selection) and also a Behavior t HTML value, which lays out all components into a single piece of HTML (with event handlers wired in appropriately). Essentially I have to do a fair amount of coding to switch between "future" occurances (AnyMoment) and the current point in time - and that seems to require a lot of juggling (I'm yet to be able to fully abstract this away).

I don't have a concrete question about this code - it works fine, but am mostly using it as a talking point. My general question is - what are your plans for the t representation in order to know when to trim? As far as I understand it, you need either this or a monad - the monadic approach being taken by sodium and threepenny-gui. Do you think reactive-banana will continue to use parametricity, or will eventually move to a monad too?

Curious to hear your thoughts,
Ollie


Dear Oliver,

what is your preference?

Currently, I am indeed leaning towards the monad, as used in threepenny-gui or sodium. The thing that irks me the most is that every widget, like your InstantiatedComponent, needs to be tagged with its time of creation, t, forcing you to keep track of it through each and every type signature. Ugh! I have also received questions from people who had a hard time juggling the AnyMoment types and were very confused by various type errors – you seem to have figured it out, at least. :-) The situation would slightly better if GHC would support impredicative types properly, but this is yet another can of worms.

However, I am quite unsure on how to proceed in making the switch. I have already extracted the core functionality into the Reactive.Banana.Prim module, so that it is possible to implement either model – monad or start time parameter – on top of it. I could offer both variants, and make one the default, or switch everything over to the monadic variant, which would totally break backwards compatibility, though. Do you have an opinion on this?

Best regards,
Heinrich


Dear Oliver,

what is your preference?

Hard to say - I haven't used the monadic approach! But my feeling is that I do tend to use dynamic switching more than I might have expected, as most of my FRP work is around UIs (be that GTK or in the browser). Every time I have to switch dynamically, my mood drops a little because I know I have to do more plumbing than appears necessary. Due to the use of rank-n types, it also means that a lot of stuff is very hard to abstract (I have needed to introduce existential types), and common techniques don't even apply any more (point-free programming is hard in rank-n types, often (\x -> foo (bar x)) cannot be rewritten as (foo . bar).

Currently, I am indeed leaning towards the monad, as used in threepenny-gui or sodium. The thing that irks me the most is that every widget, like your InstantiatedComponent, needs to be tagged with its time of creation, t, forcing you to keep track of it through each and every type signature. Ugh! I have also received questions from people who had a hard time juggling the AnyMoment types and were very confused by various type errors – you seem to have figured it out, at least. :-) The situation would slightly better if GHC would support impredicative types properly, but this is yet another can of worms.

I've definitely figured it out, but that's not to say it's become intuitive - this area is usually the only pain-point I encounter with reactive-banana. My general impression is that while the whole notion of parametricity is really nice in theory, in practice it just doesn't play out as nicely. To me, reactive-banana is a beautiful API, and it's a shame that we lose some of this with AnyMoment and friends.

However, I am quite unsure on how to proceed in making the switch. I have already extracted the core functionality into the Reactive.Banana.Prim module, so that it is possible to implement either model – monad or start time parameter – on top of it. I could offer both variants, and make one the default, or switch everything over to the monadic variant, which would totally break backwards compatibility, though. Do you have an opinion on this?

Continuing the above suggestion that reactive-banana is very much do-what-I-mean, I would advise against offering more than you have to. Thus I'd say if the monadic interface is equivalent in power/expressivity as the parametricity approach (or better!) then that's the one to go with.

With respect to the Prim namespace, are you actually using this anywhere? I see that threepenny-gui doesn't actually use this, but instead has its own FRP network.

Hope this helps!

  • Ollie

Dear Oliver,

thanks a lot for your thoughts!

To me, reactive-banana is a beautiful API, and it's a shame that we lose some of this with AnyMoment and friends.

Yeah, I completely agree.

Unfortunately, while the monadic API simplifies switching a lot, it has one big pain point: the type of accumB will become monadic:

    accumB :: a -> Event (a -> a) -> Moment (Behavior a)

Same for accumE. This means that whenever you accumulate state, this becomes visible in the types. This was also the original reason why I came up with current approach: With the phantom type, all functions are pure as long as you don't use switching. I thought this would be useful for beginners, but it seems that the learning curve for switching becomes too high, then.

I'm afraid it's a "pick your poison" situation: To avoid time leaks, one has to pay attention to starting types in the types, be it by phantom type or with a monad.

With respect to the Prim namespace, are you actually using this anywhere? I see that threepenny-gui doesn't actually use this, but instead has its own FRP network.

The intention is for Threepenny to switch to reactive-banana at some point and use the Prim namespace to build its FRP combinators. Apparently, this will still take some time. :-)

Speaking of Threepenny: There is another important difference between its FRP engine and the reactive-banana one when it comes to simultaneous events. In reactive-banana, an Event may have multiple occurrences at the same time. This is due to the

    union :: Event a -> Event a -> Event a

combinator, which has to handle the situation where both arguments have an occurrence at the same time. In contrast, Threepenny disallows multiple simultaneous occurrences and only provides a

unionWith :: (a -> a -> a) -> Event a -> Event a -> Event a

combinator that applies the function when both event arguments have a simultaneous occurrence.

These days, I prefer the second approach, because in the following snippet

     eOut  = accumE a eIn

     eOut' = flip ($) <$> b <@> eIn
     b     = accumB a eIn

the events eOut and eOut' are equal with the latter approach, but not with the former. Taking the union of two events has become more difficult, but I think that it is a good idea to force the programmer to think whenever he wants to take the union of events, because it can be the source of subtle bugs.

What is your opinion on this?

Best,
Heinrich


I'm afraid it's a "pick your poison" situation: To avoid time leaks, one has to pay attention to starting types in the types, be it by phantom type or with a monad.

While this is true, I don't think it's really quite that bad. Almost all use of reactive-banana ends up living inside a monadic do block anyway - with let bindings introduced locally. I don't think I've ever really introduced combinators or logic outside a do block anyway. While this is a poison, it seems to require a lot more of it for it to be harmful :) I think that this actually comes out easier in the long run for beginners - while they might have to switch between pure/monadic binding, they can otherwise follow the types. Switching in particular is something where following the types has a lot less hopes. Right now you're required to provide an AnyMoment, which in turn means you have to trim, and then you need a FrameworksMoment, and then an execute, and then you've forgotten what you were donig! :)

What is your opinion on this?

I don't have much of an opinion on this because I don't think I've ever really had simulatenous firings of events. While you say in other writing that it's easier to introduce them with other combinators (that is, further processing after the GUI event fires), I haven't given it much thought. Certainly it's been quite nice to be able to write things like unions [addItem, removeItem], but I suppose in the new formulation I'm just using unionWith (.) - which is just as clear. It sounds like this would get rid of the experimental Calm API too - an API I've never touched and always wondered why I'd want to use it :) (I see you use it in tidings, but I never fully understood why).

  • Ollie

thanks for your input!

It is pretty much decided, then, that I will move reactive-banana to a monadic API and also remove simultaneous events in favor of unionWith (That's what I did in Threepenny and no one even noticed so far). No promises on how soon, though. :-)

Best,
Heinrich

@jkozlowski

This comment has been minimized.

jkozlowski commented Jun 3, 2015

+1

Right now you're required to provide an AnyMoment, which in turn means you have to trim, and then you need a FrameworksMoment, and then an execute, and then you've forgotten what you were donig! :)

I have recently tried to master this (while following https://github.com/ocharles/Francium todo-mvc) and I would also vote for a simpler approach.

@miguel-negrao

This comment has been minimized.

miguel-negrao commented Jun 5, 2015

For those not using dynamic event switching, accumB becoming monadic might actually be more annoying then dealing with the type issues, since these don't appear that much, while making accumB monadic means abstracting parts of the event graph into functions requires writing functions working on monadic values, which is makes things more complicated.

With the phantom type, all functions are pure as long as you don't use switching.

I feel it would be a real pity to remove this feature... although I can understand why it might be needed.

@ocharles

This comment has been minimized.

Collaborator

ocharles commented Jun 5, 2015

I get that, but in reality I'm yet to write many applications that don't
later end up requiring dynamic switching. I can't speak for others though :)
On 5 Jun 2015 6:22 pm, "Miguel Negrão" notifications@github.com wrote:

For those not using dynamic event switching, accumB becoming monadic might
actually be more annoying then dealing with the type issues, since these
don't appear that much, while making accumB monadic means abstracting parts
of the event graph into functions requires writing functions working on
monadic values, which is makes things more complicated.

With the phantom type, all functions are pure as long as you don't use
switching.

I feel it would be a real pity to remove this feature...


Reply to this email directly or view it on GitHub
#97 (comment)
.

@ocharles

This comment has been minimized.

Collaborator

ocharles commented Aug 11, 2015

One more argument in favour of this - even with GeneralizedNewtypeDeriving, GHC can't automatically infer the type classes for the following:

newtype Francium t a =
  Francium {runFrancium :: Frameworks t => ReaderT (FranciumEnv t) (WriterT (Behavior t [MarkedNode]) (Moment t)) a}
  deriving (Functor,Applicative,Monad,MonadFix,MonadReader (FranciumEnv t),MonadWriter (Behavior [MarkedNode]))

So the author has to manually create all the instances. That's a fair bit of faff for someone who just wants to abstract over reactive-banana! (Not to mention the abstract leak with t).

@HeinrichApfelmus

This comment has been minimized.

Owner

HeinrichApfelmus commented Sep 3, 2015

The latest commit 4313823 in the monadic-api branch implements the API as envisioned. It's ready to be tried out!

@ocharles

This comment has been minimized.

Collaborator

ocharles commented Sep 4, 2015

This is looking really good! One quick question - is there anyway to switch into a stepper? The problem I see is that stepper is now in forall m. MonadMoment m, but switch can only switch into a Behavior or Event. I can see that I can execute to get rid of that, but the type doesn't quite communicate that (execute wants a MomentIO, but I only need to eliminate Moment).

@HeinrichApfelmus

This comment has been minimized.

Owner

HeinrichApfelmus commented Sep 4, 2015

I can see that I can execute to get rid of that, but the type doesn't quite communicate that (execute wants a MomentIO, but I only need to eliminate Moment).

Observe: observeE!

The duplication is unfortunate, but I think it's important to distinguish a pure and an impure variant.

@ocharles

This comment has been minimized.

Collaborator

ocharles commented Sep 4, 2015

observeE certainly matches when it comes to type, but it seems to only sample Moment computation once. For example, I have:

let clockOnKeyPress = observeE (fmap (const (integrate stepPhysics 0 1)) keyPressed)
in ...

-- later

integrate dt initial x =
  trace "integrate" $ accumB initial ((.+^) <$> ((^*) <$> x <@> (dt :: Event Double)))

Yet despite pressing the key multiple times, I always get the same integration (when I would expect the Behavior in clockOnKeyPress to restart at 0 on every press). Maybe this is better discussed in another issue?

@HeinrichApfelmus

This comment has been minimized.

Owner

HeinrichApfelmus commented Sep 4, 2015

Yes, please open another issue, this could be a bug with accumB. Note that the type of clockOnKeyPress is Event (Behavior X), i.e. you're not evaluating the clock just yet.

@HeinrichApfelmus

This comment has been minimized.

Owner

HeinrichApfelmus commented Sep 4, 2015

Actually, no need to open another issue, it's definitely a bug. 😄 I've added a test case.

@HeinrichApfelmus

This comment has been minimized.

Owner

HeinrichApfelmus commented Sep 12, 2015

The new API has been merged into master and the test suite succeeds as of commit ff0b914 . Everything is ready to be tried out, this is the code that will likely make up the 1.0 release.

@HeinrichApfelmus

This comment has been minimized.

Owner

HeinrichApfelmus commented Sep 23, 2015

Thanks a lot for your input everyone! I think this issue can be closed now that the monadic API has been implemented.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment