yelouafi / cfrp

Attempt to implement (a subset) of [Classic FRP](http://conal.net/papers/icfp97/)
MIT License
83 stars 4 forks source link

Behaviour like a Subject in RxJS #1

Open xgrommx opened 8 years ago

xgrommx commented 8 years ago

Hi! I think your approach with behaviours too pretty similar to Subjects of RxJS. I mean smth like

const s = new Subject();
s.next(...);
gaearon commented 8 years ago

README mentions it:

In case you're wondering events are just like Rx Subjects. We use them to hookup callbacks in UI (e.g. React's onXXXX={increment$.next()}). In fact you can replace this with Rx Subjects and it'll work the same way.

In Rx, Subjects are subject to caution; but I prefer this style of pushing events from React Components into callbacks instead of pulling Event Streams based on some fragile CSS selector.

xgrommx commented 8 years ago

@gaearon In Rx all are push based collection.

xgrommx commented 8 years ago

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

yelouafi commented 8 years ago

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

yelouafi commented 8 years ago

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)

xgrommx commented 8 years ago

@yelouafi I found it similar because Property of Bacon also has values always. I love this picture from Kefir.js http://rpominov.github.io/kefir/docs-src/images/stream-and-property.png Also if I use Bacon I can emulate FRP behaviour. In Rx I can make property via publish().refCount() or publishValue(value).refCount() because in Rx all cold. Behaviour approach has behaviour a hot stream, and will be multicasted every time. Also xstream (a new library) has only hot streams. Hot stream = Behaviour in time?

xgrommx commented 8 years ago

@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 of aka pure and flatMap aka bind. This is pain in js and it isn't haskell =(

Also take a look on this repos https://github.com/ivan-kleshnin/csp-frp-foundation http://santoshrajan.com/frpjs/basic-examples/ https://github.com/SodiumFRP/sodium

yelouafi commented 8 years ago

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 f(b1, b2) and have the lang/runtime do the lifting under the hood. In haskell you can have something less or more closer using type classes and/or custom infix operators (like in reactive-banana f <$> b1 <*> b2 ...). In javascript we cant do this of course but I think the concept of computed in mobx/ko is a perfect expression of implicit lifting. You just write the expressions using plain old JS operators and functions and the lib automatically msintains the resulting relation: this is like if the lib continually apply the operators/functions to the behaviors in time

xgrommx commented 8 years ago

@yelouafi But in js you can emulate it with map and ap

const f1 = x => y => x * y;
const res = b1.map(f1).ap(b2);
// or
const res of(f1).ap(b1).ap(b2);
yelouafi commented 8 years ago

Cf the example of allDone in todoList (see readme). How can you express that behavior using just map an ap?

xgrommx commented 8 years ago

@yelouafi I like a composition of lift and ap and map https://github.com/briancavalier/most-behavior/blob/master/examples/bounce/index.js#L89-L103

mweststrate commented 8 years ago

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 ????.combineLatest(???,???,???).map(???? = ???).

Or in other words if your map expression is x$ ? y$ : z$ you can get away with a switchMap in Rx, but these expressions can become arbitrarily complex, and dynamic, which makes something like Rx cumbersome because you need to statically encode all the relations between observable, in contrast to TFRP where these relations are determined at runtime based on the path that is chosen at runtime. So for example if x$ then z$ won't be observed, without needing to choose an Rx operator that explicitly exhibits that behavior. Or another operator if you need x$[y$ + z$] etc etc

xgrommx commented 8 years ago

@mweststrate Interesting but roughly Observable is monad. it has several laws for map, flatMap, etc. For example we have of and flatMap => monad, map => functor, combineLatest(streamOfFn, stream, (f, v) => f(v)) => applicative functor. Your idea pretty similar to knockout computed property or ember computed property. But monad need for resolving side effects in functional programming (because we need an order for computation of side effects). As I told earlier https://github.com/paldepind/flyd has almost the same behaviour.

yelouafi commented 8 years ago

@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 join for Behaviors can be represented using this hypthetical function

// 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 time, then we get the value of the resulting value at time. That's all. No side effects are involved here.

xgrommx commented 8 years ago

@yelouafi @mweststrate Also take a look on https://github.com/lihaoyi/scala.rx

xgrommx commented 8 years ago

@yelouafi I know what you talking about monads.

yelouafi commented 8 years ago

As I told earlier https://github.com/paldepind/flyd has almost the same behaviour.

I know, I had some previous discussions with @paldepind. And I think also flyd streams implement 'step' behaviors (esp. b/c of lift and the ability to snapshot the value at any moment of interest). As I was told, earlier versions of flyd had the automatic dependency tracking included but were removed later, b/c @paldepind think it was not needed in his applications, (he was (and still?) also in favor of a more Elm like approach)

xgrommx commented 8 years ago

@yelouafi Elm lost signal approach. But anyway Elm did not have FRP earlier.

paldepind commented 8 years ago

And I think also flyd streams implement 'step' behaviors (esp. b/c of lift and the ability to snapshot the value at any moment of interest).

Yes. That is correct.

As I was told, earlier versions of flyd had the automatic dependency tracking included but were removed later, b/c @paldepind think it was not needed in his applications,

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 observe are implemented in a way that hard codes their dependencies on specific behaviours. This makes them harder to test, reuse and compose. Lifting a pure function don't suffer from this. You still have the "plain" pure function that you can reuse, test easily and compose with other pure functions.

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.

(he was (and still?) also in favor of a more Elm like approach)

Both yes and no :smile: 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 :+1:

yelouafi commented 8 years ago

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.

Good luck with this library @yelouafi. You do some very great work :+1:

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

paldepind commented 8 years ago

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.

Also was an occasion to learn more about implementing glitch-free dependency graphs (I'll have to take a look at your hareactive lib).

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?

yelouafi commented 8 years ago

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.

mweststrate commented 8 years ago

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.

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.

I'm also interested to know how @mweststrate proceed with testing MobX computeds in real world

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.

..but there maybe some challenges regarding to transactional updates from a root event (not sure because didnt try it)

This is addressed by using action (or transaction). They can be safely nested and only when the outer transaction completes changes are visible to the outside world . Note that (trans)actions are still synchronous.

paldepind commented 8 years ago

@mweststrate That sounds like what Rx and other reactive libraries are doing.

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).

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.

paldepind commented 8 years ago

@mweststrate

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.

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?

mweststrate commented 8 years ago

Unless I'm mistaken something, a MobX observable must be mutated. That is the opposite of being reactive.

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:

The essence of functional reactive programming is to specify the dynamic behavior of a value completely at the time of declaration. -- Heinrich Apfelmus

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.

To me it doesn't sound like switching between push and pull based FRP

EDIT (sorry, didn't read kefir docs well enough) yes the activation mechanism in Kefir seems similar indeed.

paldepind commented 8 years ago

@mweststrate I am talking about the observables. Not the computer values. Those with the @observable annotation. You change them by mutating them which violates excactly the principle you quoted. Hence you can't claim to be fully reactive.

mweststrate commented 8 years ago

ah ok. I guess the word 'reactive' should then be restricted to cyclejs only.

paldepind commented 8 years ago

@mweststrate

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.