-
Notifications
You must be signed in to change notification settings - Fork 4
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
Behaviour like a Subject in RxJS #1
Comments
README mentions it:
|
@gaearon In Rx all are push based collection. |
About const counter = behavior(
0, // seed value
[ increment$ , (prev, ev) => prev + 1 ], // case 1
[ decrement$ , (prev, ev) => prev - 1 ] // case 2
) This is just a join pattern. For example you can reproduce it with Bacon: const increment$ = new Bacon.Bus();
const decrement$ = new Bacon.Bus();
const counter = Bacon.update(0,
increment$ , (prev, ev) => prev + 1, // case 1
decrement$ , (prev, ev) => prev - 1 // case 2
);
counter.onValue(...);
increment$.next()
// => log 1
decrement$.next() Also you can emulate it in Rx pretty easy https://github.com/xgrommx/react-rx-flux/blob/master/src/rx-extensions.js#L37-L48 . By the way several approaches also similar to https://github.com/paldepind/flyd library. Probably really FRP here https://github.com/atzeus/FRPNow. Also you can take a look on https://github.com/briancavalier/catalyst and https://github.com/briancavalier/most-behavior |
There is a fundamental diff between discrete event streams and behaviors. A behavior has always a value. You may emulate some semantics of behaviors with scan/when but behaviors are more expressive when it comes to state. Esp. When dealing with dynamic behavior switching (not impl. Actually in this repo) Also the implicit lifting (see computed) is far more expressive than combineLatest esp. With dynamic dependencies |
The closest concept to behaviors is actually bacon Properties. But AFAIK bacon doesnt support dynamic dependencies (useful to build computations based on a behavior of an array of behaviors) |
@yelouafi I found it similar because |
@yelouafi Lifting is just a transformation for monads (in haskell) Because we need lift context of monad to another context of monad for normal computation unboxed values. Lift is composition of applicative functors. For example: liftA1 = map
liftA2 = map + ap
liftA3 = map + ap + ap
liftA4 = map + ap + ap + ap
... Also all should be to use curry approach and we need HOF. For example: cons liftA3 = fn => s1 => s2 => s3 => s1.map(fn).ap(s2).ap(s3) For monad we need Also take a look on this repos |
I think lifting is more related to the applicative structure. And both Events and Behaviors are Functors, Applicatives & Monads. But each concept has different semantics for the those structures. the utility of Applicatives is that you can use them to emulate a restricted subset of monadic streams/behaviors if you dont want to deal with the famous drawbacks of dynamic switching: something also known as Applicative FRP (vs pure/impure Monadic FRP which defines dynamic switching in terms of monads). This is for example the case of (ex) Elm signals There is a small mention in Conal's paper about implicit lifting. When you write code like |
@yelouafi But in js you can emulate it with const f1 = x => y => x * y;
const res = b1.map(f1).ap(b2);
// or
const res of(f1).ap(b1).ap(b2); |
Cf the example of allDone in todoList (see readme). How can you express that behavior using just map an ap? |
@yelouafi I like a composition of |
See also: https://github.com/mobxjs/mobx/wiki/Mobx-vs-Reactive-Stream-Libraries-(RxJS,-Bacon,-etc) The dynamic behavior of transparantly tracking derivations only if actively used in an expression is quite unique for TFRP. It is not that there are things you cannot express with stream libraries, but it is hard and it adds basically a second GPL on top of javascript. At mendix for example we have validation rules and components from external sources, and we have upfront no single clue about which observables they will use. Nonetheless with TFRP you can trigger these validation rules at exactly the right and minimally required moments, because you can basically say Or in other words if your map expression is |
@mweststrate Interesting but roughly |
@mweststrate I think what you call TFRP is the perfect expression of implicit (or call it dynamic) lifting. The main purpose of lifting is to promote behaviors as first class values in our program. Where we had previously a mutable variable, we replace it with an immutable value (the behavior 'container') which encodes all the possible mutations over time of a value. To promote those behaviors to first class values we need to be able to compose them with functions just as we do with 'normal' values in the mutable world. If I can write a relation involving arbitrarily complex expressions using arbitrarily complex behaviors (including behaviors of behaviors) and have the lib/runtime maintain this relation transparently and continuously over time, performing the necessary lifting and monadic joining under the hood, then I think w've got pretty closer to the purpose of behaviors and CFRP @xgrommx As I said the Behavior Monad (state) hasent the same semantics as the Event Monad. And both havent the same semantics as the IO Monad (which is used for Side Effects in Haskell). i.e. Monad and Side Effect are not the same, Monads are a way (among others) to represent Side Effects in pure FP, but Monads can be used to represent other 'nested' structures (Arrays, Maybe...). For example, the semantics of // bb a :: Behavior (Behavior a)
bb = time => bb(time)(time) Joining a nested behavior yields the result of the result of the behavior at time T: first we get the value of the outer behavior at |
@yelouafi @mweststrate Also take a look on https://github.com/lihaoyi/scala.rx |
@yelouafi I know what you talking about monads. |
I know, I had some previous discussions with @paldepind. And I think also flyd streams implement 'step' behaviors (esp. b/c of |
@yelouafi Elm lost signal approach. But anyway Elm did not have FRP earlier. |
Yes. That is correct.
I think it is complex and unnecessary. I think using behaviours and events as applicative functions and using lifting is better. My reasons:
I think the last point is important. Let me try and explain it better. What I mean is that the body of the functions you pass to On the other hand I can see the appeal of automatic dependency tracking. It definitely allows for some neat code. I'm just afraid it's too fancy. Btw, the way you create behaviours is quite similar to flyd-scanmerge.
Both yes and no 😄 The below is a bit of a brain dump. So feel free to move along. I don't think JavaScript is particularly well suited for FRP. One big problem is that if you implement FRP with a push bashed approach it is impossible to rely on garbage collection. You need weak references–which JS does not have. Conal Elliot has written as section in it here. Some FRP libraries in JavaScript tries to solve the problem by using a form of laziness/hot- and cold-observables. This breaks the semantics, adds complexity and does not seem to be in the spirit of FRP to me. So in JavaScript I prefer an Elm/Redux like approach. I still have hope in FRP though. But I don't think FRP is best combined with virtual DOM. FRP gives us an abstraction for values that can change over time. I.e. we know exactly when any of our data changes. Instead of using virtual DOM we should hook our behaviours directly into the DOM. This avoids the indirection with virtual DOM where the view function requires plain data. It also makes the entire dataflow into the view explicit. I was in fact working on such an approach at one point. I gave up on it though. However, the Haskell FRP library Reflex takes exactly such an approach. The author of Reflex has written a great comment about why FRP do not need virtual DOM here. I highly recommend looking into Reflex and Reflex-DOM. It's very interesting. If you're curious I was at one point working on an FRP library for JS with the goal of mimicking classic FRP with a simple implementation. It's called hareactive. It has separate events and behaviours. It was supposed to rely heavily on lifting by implementing the fantasy land applicative functor spec. Furthermore it performed very well in by benchmarks. Even slightly better than Most.js which is very fast and a superb FRP library. But I gave up on FRP in JavaScript and moved on. Good luck with this library @yelouafi. You do some very great work 👍 |
Hi @paldepind. Thanks for commenting. I do agree with your points about dynamic depenencies. It's all related to 'easier is not always simpler'. Having explicit dependencies gives us code that is more predictable, easier to test and easier to reason about. Also the single state atom pattern of Redux and Elm makes it possible to express any kind of derivation without resorting to a complex implementation or potential memory leaks. Also transactional semantics are automatically guaranteed: like in Redux, where every action updates the whole application state in a single transaction. Dynamic computeds are easier to write, more powerful, but as you said harder to test (at least unit testing seems impossible, you'll have to trigger some signal from the root nodes and observe the effects on the computed; I'm also interested to know how @mweststrate proceed with testing MobX computeds in real world). So it's a matter of choices and tradeoffs as usual.
Thanks @paldepind. I started this just as an exercice to combine mobx/ko observable with Redux reducers and quickly found myself reinventing FRP stepper behaviors. Also was an occasion to learn more about implementing glitch-free dependency graphs (I'll have to take a look at your hareactive lib). Not intended to be something usable. At least in the short run it's more a test bag for some ideas, and a mean for learning new things |
I very much agree. There is never a silver bullet and one has to both appreciate the advantages and be aware of the limitations of ones approach.
Hareactive is not glitch free (but flyd is). I only got around to implement the core of the library. Functor, applicative functor and monad instances of Event and Behaviour along with merging and a few other things This is off topic. But may I ask what you think of Redux compared to MobX? As in what would you choose in "the real world" today? |
I made a comment on the mobx repo. I admit I m biaised toward Redux b/c I think it's better to put as much as possible on the 'pure side' (well except IO b/c I think imperative style is more suitable to express sequences of operations). Redux may have some ceremony but this can also be a good point. The app structure can be more visible and easily approachable by new team members. When I approach some code base, most of the time I spend at start is to understand the big picture. And for this reason I think Elm and Redux ceremony is a good thing to have. MobX improves over previous similar libs in many points: for example compared to ko, it's glitch free, doesnt use html templating/bidirectional data binding but instead provides integration with React, has transactions etc... But my concern is that mobx, while it maintains a functional relation between app state and its derivations. It doesnt maintain the same relation between app state and events (which FRP behaviors provide). One can use behaviors on top of mobx but there maybe some challenges regarding to transactional updates from a root event (not sure because didnt try it) Of course I m talking from my very opinionated POV. Others may not have concerns with the imperative update style. |
MobX solves this by switching between push and pull based FRP. If a derivation is in use by some side effect it is push based, and the dependency tree will allow changes to be pushed to observers. If the derivation is not (indirectly) in use by a side effect, all observers are unsubscribed and only closure references remain, allowing them to be safely GC-ed. ...or the dep tree is re-instantiated if new observers arrive. I'll elaborate more on this in my upcoming egghead lesson.
Computed values are just thunks that are either evaluated eagerly or lazily. So even when they have no observers they can be tested by just inspecting their values. In my experience testing in MobX is the same as in any OO based environment; create some objects, inspect the values. Since all derivations in MobX are run synchronously, active observers are very straight forward to test as well, without having to do async stuff in the tests.
This is addressed by using |
@mweststrate That sounds like what Rx and other reactive libraries are doing.
I think this very accurately describes the primary problem. Unless I'm mistaken something, a MobX observable must be mutated. That is the opposite of being reactive. MobX observables are not reactive. It's a bit weird that in the "Becoming fully reactive" blog post mutations are used even though "full" reactiveness is claimed. It mostly looks like a more modern version of Knockout. That's not bad per see but I wouldn't call it fully reactive. And personally I don't want to use a solution that does not work with pure data. |
I'd be really curious if you could elaborate on this. To me it doesn't sound like switching between push and pull based FRP. Pull is when one propagate changes not by pushing them through the dependency graph but by pulling from the leaves. It sounds like the type of lazynes/activation approach that many reactive libraries use. Kefir has a description of it here. But I might be misunderstanding you. Also, I don't understand what you mean with closure references? |
I think you are confusing a few concepts here. I know there are a lot of definitions / ideas about what reactive is, but I think this is one of the more accepted once:
That is exactly what happens with computed values, autoruns and observers in MobX. Their behavior is defined once, during creating. Regardless why (or how) the values they use change in the future. This doesn't make it less reactive then for example RxJs where events are emitted, which is as imperative as mutating values. Reactiveness is about how systems react to changes, but not about how the changes are caused. In fact it is more declarative reactiveness as in Rx, in Rx you still need to define how there should be reacted to events happening over time, where in MobX you only define the relation (derivation) but leave out the how / when.
EDIT (sorry, didn't read kefir docs well enough) yes the activation mechanism in Kefir seems similar indeed. |
@mweststrate I am talking about the observables. Not the computer values. Those with the |
ah ok. I guess the word 'reactive' should then be restricted to cyclejs only. |
Why? Do you think that CycleJS is the only library that satisfies the definition? If you do, then that is interesting. Also, I think you can perfectly well call your library reactive. But you specifically claim that it is fully reactive. To me that is incompatible with the fact that all state changes in your library originates as non-reactive mutations. |
Hi! I think your approach with behaviours too pretty similar to Subjects of RxJS. I mean smth like
The text was updated successfully, but these errors were encountered: