Closed stevekrouse closed 5 years ago
As of right now, the API in Turbine is biased towards separation of model and view. Personally, I feel that the mixing of logic and view in Reflex is messy. But, on a fundamental level, there is nothing in Turbine that prevents one from doing something similar to what is possible in Reflex. The Component
monad has a Now
inside it. So everything that can be done in Now
can be done in Component
. Furthermore, the API could easily make this more comfortable for this.
The first step would be to create a function that makes it easy to run Now
computations inside a Component
. Such a function could be created based on modelView
as below.
function liftNow<A>(now: Now<A>): Component<A> {
return modelView(() => now, () => emptyComponent)();
}
The above function creates a component that runs the given now
computation, outputs its result, and then in the view does nothing (emptyComponent
corresponds to no view). This function could also be added directly to Turbine as a primitive.
With this function, we could do something like this
const counter = go(function* () {
const { click } = yield e.button("Click me");
const count = yield liftNow(H.sample(H.scan((_, m) => m + 1, 0, click)));
yield e.span(H.format`Button pressed ${count} times`);
});
Here is a CodeSandbox for the code above.
This example combines both the model and the view. The only small difference compared to Reflex is that we have to use the liftNow
function whenever we want to do stateful FRP computations. If I recall correctly, in Reflex their thing corresponding to Now
is not a type but a type class and this type class is implemented by their Widget
type (which corresponds to our Component
). I don't think having to use liftNow
is a problem. Lifting in monads is very common. But, we could do the same thing as Reflex and create some type class (or the equivalent thing in TypeScript). The downside to this is that it increases the complexity of the types which makes things slightly more tricky and less transparent.
Does that help you? What do you think?
Wonderful! I'm trying to apply this to my standard "buttons that make buttons" problem, but am having trouble using it in conjunction with loop
.
const buttonsThatMakeButtons = loop(({ output, count }) => function*() {
const incrementClick = switchStream(
output.map(l => combine(...l.map((o, i) => o.click.mapTo(i + 1))))
);
const count_ = yield liftNow(sample(
scan((n, m) => n + m, 1, incrementClick.map(x => (x % 2 === 0 ? 0 : 1)))
));
const output_ = yield list(
b,
count.map(c => Array.from(Array(c).keys()).map(x => x + 1))
);
return { count: count_, output: output_ };
});
Creates the error Attempt to sample non-replaced placeholder
Can you help?
This appears to be a bug that was fixed in the latest version of Turbine.
Here is a Codesandbox that, as far as I can tell, works as expected: https://codesandbox.io/s/m9o1ok1vyy
The only things I have done is
output
property in Turbine 0.2.0- const b = i => button({ output: { click: "click" } }, i);
+ const b = i => button(i).output({click: "click"});
I know there are a lot of other issues discussing the pros and cons of separating the model and view. I'm simply wondering if it's currently possible to do given the current architecture, and if so, if someone could give me an example of it.
My motivation is that I like how in Reflex you can combine the model and the view seamlessly, but also separate them if you want. I like the extra freedom. The
modelView
function and the architecture it imposes feels a bit restrictive to me.