Isolate alternatives with Subject and fractality issues
See original GitHub issueThis issue is basically a compilation of stuff said in the Gitter. Shoutout to @laszlokorte and @whitecolor for discussing these things with me. There are a lot of details, so feel free to point out and correct any mistakes.
Before you read any of this, I want to emphasize that idiomatic Cycle is currently neither truly pure nor truly fractal, but there are several possible solutions.
isolate
is impure
isolate
is necessary for ensuring two instances of a component are unique, but it also introduces a great amount of complexity to Cycle, and many drivers need to be aware of it. These circumstances create a lot of controversy over whether we should use isolate
by default or if there is a better solution (e.g. #259).
Cycle’s goal is to be pure and referentially transparent, but as long as we have isolate
, it cannot be.
isolate(Component) // impure
isolate(Component, scope) // pure <==> Component is pure
Since we can never be sure that a component is pure, it is safe to pessimistically assume that all components are impure.
The minimum requirements for a truly practical and pure isolate
are threefold:
(1) We must always pass an explicit scope to isolate
(2) In order for this to scale, we must use a scope generator, from the top all the way down (humans are bound to create duplicates).
So far, the code would look like this:
(cc @theefer)
function Component(sources) {
const iso1 = sources.isolate;
const [scope1, iso2] = iso1.next();
const [scope2, iso3] = iso2.next();
const { isolate: iso4 } = Child({ ...sources, isolate: iso3 });
// ...
return {
// ...
isolate: iso4
};
}
(3) This behavior is extremely redundant, and this purity criterion also requires us to pass it down as an extra sink on all components. The best way to practically do this is to use a pure functional language like Elm or Purescript, where we can then hide the isolate
generator in a state monad.
The Fractal Problem
@staltz says that Cycle is a Fractal Architecture (http://staltz.com/unidirectional-user-interface-architectures.html). While this is true by interface, certain implementation details prevent it from being true in practice.
In Cycle’s case, being fractal means that you can take any app’s main function and pass it some drivers, or alternatively call a wrapped run(main, drivers)
function, and expect it to behave normally.
But what if it uses isolate
? We can’t be sure that the child (possibly third party) component uses the same instance that we use. In the current source, isolate
always counts up from zero. This means that two components using different isolate
instances can clash. We’ve decidedly refused to use random number generators, since they are never truly safe. Using an incremental id is usually the safest bet, but only in a singular context, which we cannot assume.
One possible solution would be to somehow make sure that all components receive the same isolate
, but that would only increase Cycle’s overall complexity.
Subjects to the rescue?
This idea came with some initial disapproval, but I’ve thought very much about it and I think it is a possible way forward.
Instead of using DOM.select
, we can use an event handler like in rx-recompose: https://github.com/acdlite/recompose/tree/master/src/packages/rx-recompose
(cc @vladap)
In the following code, DOM.handler
is equivalent to createEventHandler
in rx-recompose. createEventHandler
simply returns a function that acts like Subject
, but proxies calls to onNext
. Using $x$
is just a contrived convention, but the notation indicates that it is both an observer and an observable (i.e. subject).
export default ({ DOM }) => {
- Button(DOM.select('.Home')).forEach(() => {
- window.location.href = '/';
- });
- Button(DOM.select('.Github')).forEach(() => {
- window.location.href = 'https://github.com/edge/cyc';
- });
+ const $goToHome$ = DOM.handler();
+ const $goToGithub$ = DOM.handler();
+ $goToHome$.forEach(() => {
+ window.location.href = '/';
+ });
+ $goToGithub$.forEach(() => {
+ window.location.href = 'https://github.com/edge/cyc';
+ });
return {
DOM: $.just(
div('.p2.measure', [
h2('About'),
h4([
i('cyc'), ' is a Cycle.js boilerplate built with convenience and speed in mind.'
]),
br(),
- button('.btn.Home', 'Home'), ' ',
- button('.btn.Github', 'Github'),
+ button('.btn', { onclick: $goToGithub$ }, 'Home'), ' ',
+ button('.btn', { onclick: $goToGithub$ }, 'Github'),
])
)
};
}
Although in the end we have two extra lines, this removes the need for isolate
, drastically reducing complexity.
You might say that this is no longer pure, but isolate
and Subjects are two sides of the same coin:
To the parent, the interface and behavior are the same. This follows two Cycle doctrines:
- All read effects are from sources. All write effects are to sinks. ++ Subjects are implicit sources and sinks.
- Parent sources go to Child sources, Child sinks go to Parent sinks. ++ As you can see in the diagram, no read-write arrow crosses a component context (lines in the diagram).
Not only is it more predictable, using subjects is about as pure as Javascript purity goes.
This also solves the fractal problem, because all Subjects are inherently unique.
What using this idiom implies for other drivers is an important topic for discussion, which I’ll start off with this dialogue:
Q. How would this work for the HTTP driver?
A. { url, response$: $response$ }
Q. But doesn’t that violate the second doctrine? The subject will be passed into a driver, and a read event will bypass the parent sources.
A. Possibly, but if you look at it like metadata, which is more obvious as we are only passing a function containing a reference to the actual subject, it is no different from theoretical driver interop. The handler is an implicit source/sink, but we’ve conflated the two for simplicity. Although it may not seem to follow the doctrine, it is still fractal.
But what about cycle-restart?
Using a subject for DOM events seems to exclude it from cycle-restart. In this case, we can add some code to the DOM driver to replay the subject’s last event. This also solves https://github.com/Widdershin/cycle-restart/issues/41.
(cc @Widdershin)
If this concept becomes idiomatic, it could be standardized and applied to other drivers. For example, an HTTP.handler() could be implemented, mitigating the aforementioned context-crossing.
The Fractal Problem (cont.)
This discussion does, however, bring up an important issue. Ideally, in an application implementing cycle-restart
, every driver in an application should be restartable
. This is the only way for an application to be truly restartable, as child applications with drivers would otherwise be ignored. In order for this to work, we need to explicitly pass down rerun
to every component.
But what if a third party application uses its own instance of cycle-restart? This instance problem concerning cycle-restart, isolate, and other similar implementations could be partially solved by wrapping every application’s run(main, drivers)
as follows:
export default ({ DOM, HTTP }) => {
run(main, {
DOM: DOM || restartable(makeDOMDriver('#root')),
HTTP: HTTP || restartable(makeHTTPDriver())
});
}
or possibly:
export default availableDrivers => {
run(main, {
DOM: restartable(makeDOMDriver('#root')),
HTTP: restartable(makeHTTPDriver()),
...availableDrivers
});
}
This means any available drivers would be reused, and would automatically be restartable. However, this would still require us to pass down restart
and isolate
in every component.
And what if we wanted hydratable drivers (not yet implemented)? Now every driver would also need to be wrapped in hydratable
. This cannot scale.
Cycle seems to require a lot of passing-down-common-objects in order to be more correct as an architecture. While React solves this with this.context
available to all impure components, we must solve it in a pure way to get closer to our goal of purity.
Monads are a viable solution; they are great for concisely and efficiently keeping things in a context, as well as reading and writing any necessary information. This implies that we should switch to a language that can facilitate this, like Elm or Purescript.
Conclusion
- Using Subjects instead of
isolate
would reduce a lot of complexity and make components much more predictable. It should live comfortably in both Javascript and compile-to-js environments. - More importantly, the need for the switch to a functional language like Elm or Purescript to solve certain fundamental problems may be more imminent than we thought.
Gists:
Further Reading:
- https://gitter.im/cyclejs/core?at=56d7436c0bdb886502f6d263
- https://gitter.im/cyclejs/core?at=56e5cd900055f8f35a82ccce
- https://gitter.im/cyclejs/core?at=56e6002e89dd3cce10061e42
- https://gitter.im/cyclejs/core?at=56e62ff13194fbd11096bb33
related: #259
Issue Analytics
- State:
- Created 8 years ago
- Reactions:6
- Comments:16 (11 by maintainers)
Top GitHub Comments
I think that there’s value in discussing these ideas, but they overlook a key aspect of what I think is one of Cycle’s biggest strengths, and the biggest reason to use it over Redux/Flux/Elm.
In Cycle, applications read from top to bottom in the following order:
They are extremely easy to read once you’re familiar, as you can always trace the source of where some data is flowing from just by reading the definitions.
With the ideas proposed in this issue, the order would be more like Redux/Flux/Elm.
The difference there is that any part of your view can dispatch an action and change the application. You can’t just trace back the flow of definitions to find all changes to state, you have to read the whole app.
In my opinion,
Subject
should not be used in application code. It makes it much harder to trace the flow of data in your app. They aren’t declarative, and as a result usage isn’t self describing.Purity is a tradeoff. I think it’s much more compelling to aim for the most useful feature of pure functions, writing deterministic applications.
cycle-restart
demonstrates that a deterministic Cycle app where all changes to state are driven by external drivers allows you to treat the app as if it were a pure function.The architecture problem I’m most excited for right now is how we declaratively express the relationships between nested dataflow objects, without using
Subject
. @staltz has said before that there’s top/down and bottom up, but no middle. I’m not quite sure. I think there’s room to investigate relational ideas and solve problems that way.I think using explicit
isolate
and really cyclic approach makes developer to think more - and in my view that is a good thing and using ofSubject
is seem to be a hack to me because there this no clear repsonsibility separation between READ/WRITE effects - and this is a conner stone in good design and architecture.People complain that cycle is forcing to do some boilerplate and “care about differnt things more”. But if you don’t want to explicit, concise, logically verbose, you probably don’t want to use cycle and declartive aproach in general. There are a lot of framewords that “hide” rough spots and do a lot of “magical” stuff (that actually often turns in to “magical” bugs).
There is a great article by @staltz http://staltz.com/its-easy-to-write-imperative.html. I think the more time and effort invested in thinking about apps logic, possible flaws and conner cases the less time in perspective will be required to support and modify this logic - this what functional programming and cycle in paticular it just forces us to think more. From practice: even the people who don’t know the cycle and actually are yet very bad at understanding functional approach and struggling with it feel by their gut that program becomes more reliable when they start to use this approach.