Skip to content
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

Reactive Magic #142

Closed
ccorcos opened this issue Apr 13, 2017 · 17 comments
Closed

Reactive Magic #142

ccorcos opened this issue Apr 13, 2017 · 17 comments

Comments

@ccorcos
Copy link
Contributor

ccorcos commented Apr 13, 2017

Hey there, just thought you might be interested in a little library I build on top of Flyd called Reactive Magic.

Perhaps the most interesting thing is how you magically combine streams:

const x = Value(1)
const y = Value(1)
const z = Derive(() => x() + y())

I built some abstractions and created an interesting React API. Its pretty neat actually. I don't like how magical it is in some ways, but it actually makes it much faster to build things.

@paldepind
Copy link
Owner

Hello @ccorcos.

Thank you for sharing the library. That is pretty cool 😄 Back when I wrote Flyd I actually toyed with a similar idea myself. I agree with you that while it looks very cool it can seem a bit too magical.

it actually makes it much faster to build things.

I'd like to hear more about that and your experiences. As far as I can see it actually doesn't offer that much.

The example you posted could be created with lift almost as easily.

const x = stream(1);
const y = stream(1);
const z = lift((x, y) => x + y, x, y);

And in my opinion the latter code has the advantage that the logic (the addition) is a "plain" function. This has the benefit that I could just as well throw in R.add.

const x = stream(1);
const y = stream(1);
const z = lift(R.add, x, y);

Again, thanks for sharing this. I'd love to hear more 😄

@ccorcos
Copy link
Contributor Author

ccorcos commented Apr 14, 2017

I'd like to hear more about that and your experiences. As far as I can see it actually doesn't offer that much.

Check out the example app and specifically the React component subclass.

I totally agree that there there are benefits to taking a strictly functional and idiomatic approach to programming. But also think that I often spend too much time thinking about the architecture (like this project built on Snabbdom) rather than building stuff.

In terms of building things fast, you can create global singleton store like this store that holds the mouse position:

const MouseStore = Store({ x: 0, y: 0 });

document.addEventListener("mousemove", function(event) {
  MouseStore.x = event.clientX;
  MouseStore.y = event.clientY;
});

And then you can use this store magically reactively in any of your components.

const r = 10;

export default class Ball extends Component {
  getStyle() {
    return {
      position: "absolute",
      top: MouseStore.y - r,
      left: MouseStore.x - r,
      width: r * 2,
      height: r * 2,
      borderRadius: r,
      backgroundColor: "blue",
      pointerEvents: "none"
    };
  }
  view() {
    return <div style={this.getStyle()} />;
  }
}

You can also create local state by instantiating a store inside the component itself.

class Counter extends Component {
  store = Store({ count: 0 });

  increment = () => {
    this.store.count += 1;
  };

  decrement = () => {
    this.store.count -= 1;
  };

  view() {
    return (
      <div>
        <button onClick={this.decrement}>{"-"}</button>
        <span>{this.store.count}</span>
        <button onClick={this.increment}>{"+"}</button>
      </div>
    );
  }
}

And if you want to get fancy, you can derive values as well to create new stores:

const SizeStore = Store({
  height: window.innerHeight,
  width: window.innerWidth
});

window.onresize = function() {
  SizeStore.height = window.innerHeight;
  SizeStore.width = window.innerWidth;
};

const InfoStore = Store({
  x: Derive(() => MouseStore.x / SizeStore.width),
  y: Derive(() => MouseStore.y / SizeStore.height)
});

class Info extends Component {
  view() {
    return (
      <ul>
        <li>x: {InfoStore.x}</li>
        <li>y: {InfoStore.y}</li>
      </ul>
    );
  }
}

And thats basically it! Create stores, import what you need, use it wherever you need it, and everything just magically works -- you don't even have to think!

I just started a new job at Notion and their entire architecture works in a very similar way. It's a very complex piece of software but building new editor blocks is surprisingly easy.

@paldepind
Copy link
Owner

@ccorcos Sorry for taking so long to reply to this. I totally forgot about it 😭

Thank you for linking to arbol. That's a pretty interesting idea.

I totally agree that there there are benefits to taking a strictly functional and idiomatic approach to programming. But also think that I often spend too much time thinking about the architecture (like this project built on Snabbdom) rather than building stuff.

I definitely agree with this. It's certainly no fun to write functional code when it comes at the cost of ease and productivity. A functional framework should offer an experience that is actually better in the real world. Not just an experience that satisfies some strict property.

One thing I really like about your approach is how you can insert a Valuedirectly into the dom. That is very convenient I think.

But, I still don't see the point of the magic in the Derive function. What are you really gaining from it? It seems like your example only confirms what I wrote earlier. It would be just as simple with lift:

+ const div = (a, b) => a / b;
const InfoStore = Store({
-  x: Derive(() => MouseStore.x / SizeStore.width),
-  y: Derive(() => MouseStore.y / SizeStore.height)
+  x: lift(divide, MouseStore.x, SizeStore.width),
+  y: lift(divide, MouseStore.y, SizeStore.height)
});

To me, that is just as good and a lot simpler since there is no magic.

And, now that we're sharing libraries I'd love to hear what you think about this framework that I'm working on called Turbine.

I apologize for bringing up my own project in a thread about yours. But, it actually has some similarities to Reactive Magic. Namely that we're also inserting reactive values directly into the view. In addition to that, we also use the reactive values to completely avoid the overhead of virtual dom diffing. The idea is basically that since the reactive values tell us exactly when the state change we can use that to know exactly when the DOM has to change. There is no need for a diffing process to figure it out. I wrote a bit more about that here.

Again, I'd love to hear your opinion.

@leeoniya
Copy link
Contributor

leeoniya commented May 20, 2017

i was gonna mention @adamhaile's surplus but looks like that's already on the radar in the linked thread :)

i have a demo [1] for domvm that uses flyd's streams embeded in vtree templates as a form of granular redraw optimization, even though domvm is a traditional vdom lib. it works pretty well [2] considering it's just bolted on :)

EDIT: heh, now that i looked at arbol, it looks almost the same as my demo.

[1] https://github.com/leeoniya/domvm/blob/2.x-dev/demos/streams.html
[2] https://rawgit.com/leeoniya/domvm/2.x-dev/demos/streams.html

@adamhaile
Copy link

Lots of interesting stuff here! Thanks, @leeoniya, for pointing me to the discussion.

Yeah, Reactive Magic definitely has some similarities to (S)[https://github.com/adamhaile/S]. S has both ways of defining dependencies: either automatically via S(() => ... a() ... b() ...) or explicitly via S.on([a, b], () => ...).

Chet, I've got an S -> React binding that's a lot like your Component. I should post it up on github.

I thought about automatic vs explicit dependencies a good bit when designing S, and I have to say, I'm more and more confident that automatic dependencies aren't "too magic," they're usually the right thing. This is a bit intuition and a bit experience after writing a few 100k lines of S code. Let me see if I can summarize.

From least to most important, automatic dependencies are:

  • more concise

  • more powerful. Well, let me put a maybe on this one, since I'm not fully conversant in flyd's API. How would you reference a higher-ordered stream (a stream carried by a stream) with flyd? Would you nest a flyd.combine() inside a flyd.combine()? This isn't a far-fetched scenario: it's common to have a stream carrying an object that itself has streams. Beyond this, automatic dependencies can handle certain scenarios that aren't possible to define statically (if you're familiar with the "constructivity" issue in synchronous reactive research, this is it). For instance, this is a perfectly well defined S program:

var a = S.data(true), // like flyd.stream(true)
   b = S(() => a() ? 1 : c()),
   c = S(() => a() ? b() : 2);
  • more correct. If you forget to include a referenced stream in the list of explicit dependencies, you now have a subtle bug, as your computation will go out of sync with the rest of the system when that stream changes.

  • a better mental model. Explicit dependencies are declarative about how updates flow through the system: you're explicitly defining the edges in the dependency graph. Automatic dependencies are declarative about values: you're explicitly defining the computation's current value. Conversely, explicit dependencies make it harder to reason about values: if your computation references a stream that wasn't declared, then its current value depends on what that stream's value was the last time one of the referenced streams changed. Automatic dependencies make it harder to reason about the dependency graph: you have to scan the function to identify what streams it references. I find it removes a huge amount of mental overhead to stop thinking about how change propagates and instead focus on values, knowing that the system will insure currency in all streams my code references.

That's my best effort to summarize my experience to date. Happy to hear your thoughts :).

@paldepind
Copy link
Owner

@adamhaile Thank you for chiming in. It's interesting to hear your take on automatic dependencies. Early on I actually implemented automatic dependencies in Flyd. But, I later removed it because I didn't like it.

more concise

That depends on the use case. With automatic dependencies, you are writing functions that are hard-wired to the specific signals they depend upon. This means that they cannot be reused. For instance, let's say I have to sum both a + b and c + d. With lift I can reuse the addition function

abSum = lift(add, a, b);
cdSum = lift(add, c, d);

If I had written abSum = S(() => a() + b()) I would not have been able to reuse the addition function. Of course, the addition function is trivial. But in the real world one can get significant reuse in this fashion.

more powerful. Well, let me put a maybe on this one, since I'm not fully conversant in flyd's API. How would you reference a higher-ordered stream (a stream carried by a stream) with flyd? Would you nest a flyd.combine() inside a flyd.combine()? This isn't a far-fetched scenario: it's common to have a stream carrying an object that itself has streams.

One could flatten it. There are several ways to flatten a stream. How do automatic dependencies provide a benefit here?

Beyond this, automatic dependencies can handle certain scenarios that aren't possible to define statically (if you're familiar with the "constructivity" issue in synchronous reactive research, this is it). For instance, this is a perfectly well-defined S program:

I am not familiar with the "constructivity" issue in synchronous reactive research. That is what I would call a circular dependency. Some FRP libraries solve that using a form of lazy evaluation. Which also seems to be what you're doing here. The body to S is wrapped in a function which delays its execution. In Turbine we have several ways to establish such circular dependencies.

more correct. If you forget to include a referenced stream in the list of explicit dependencies, you now have a subtle bug, as your computation will go out of sync with the rest of the system when that stream changes.

I don't see how that would end up as a subtle bug? I think it will end up as a very non-subtle bug. If it was a big issue one could implement lift so that it checks that the number of arguments the function takes is equivalent to the number of dependencies given. If not it could throw an error.

a better mental model. Explicit dependencies are declarative about how updates flow through the system: you're explicitly defining the edges in the dependency graph. Automatic dependencies are declarative about values: you're explicitly defining the computation's current value. Conversely, explicit dependencies make it harder to reason about values: if your computation references a stream that wasn't declared, then its current value depends on what that stream's value was the last time one of the referenced streams changed. Automatic dependencies make it harder to reason about the dependency graph: you have to scan the function to identify what streams it references. I find it removes a huge amount of mental overhead to stop thinking about how change propagates and instead focus on values, knowing that the system will insure currency in all streams my code references.

Explicit dependencies are declarative about both values and dependencies. While automatic dependencies are implicit about dependencies. Thus it don't see how explicit dependencies makes it harder to reason about values. They should be equal in that regard.

I think the keys to a good mental model in a reactive library is to make a distinction between values that change over time and events that happen over time. What in classic FRP is called behavior and event. I make a case for the distinction here and I've implemented such a library called Hareactive. It seems to me like S.js only has a representation for values that change over time and not one for events?

@ccorcos
Copy link
Contributor Author

ccorcos commented May 21, 2017

I have some reading to do, and feel free to show off other libraries you think are interesting or relevant!

But to answer your question of "why", I understanding what you're saying about lift, but this is also a trivial case.

Check out this gnarly component I built in a project of mine. Its responsible for the "pie" on the left side in this demo. Its not the cleanest code -- I just hacked it all together as quickly as possible. But I think it really highlights the benefit of everything. For example, in the getStyle function and the viewSlices function, I can grab reactive values from anywhere, and that all gets bundles into the the view function's Derived stream. If you nose around in this project a little bit, you'll notice that the reactive dependencies of any specific function are often layered through other helper functions, making it prohibitive to explicitly trace down every nested dependency.

@ccorcos
Copy link
Contributor Author

ccorcos commented May 21, 2017

Turbine looks really cool! Its basically Cycle.js from what I can tell. A couple concerns of mine:

  1. In this example here, the return value of the view needs to be parsed in order gather the outputs and pass them to the model. This seems nontrivial to implement and nontrivial to maintain typesafety.

  1. In my purely functional experiments, I find myself spending a lot of time trying to partially apply callback functions while maintaining performance (you cant referentially diff these functions). So I'm curious how would create an arbitrary list of counters while binding the index of the counter to the callback function. Or how would you pass a "prop" to the counter (like an incBy number) that gets passed through to the model?

I'll have to dig into it some more, but those are my initial concerns.

@paldepind
Copy link
Owner

@ccorcos Thank you for sharing your initial concerns. I really appreciate it.

Turbine looks really cool! Its basically Cycle.js from what I can tell. A couple concerns of mine:

I get why you say that. At first sight, it has some resemblance to Cycle. That is not a bad thing, Cycle is great in many ways. But there are actually many differences. The more you look at it the less it looks like Cycle I'd say 😉 Here are some of the differences:

  • In Cycle the is one cycle. In Turbine each component has its own independent cycle as you can see here. On top of that one can create additional cycles if there are additional circular dependencies.
  • We do not use selectors to get events from the DOM. Instead, our Component is a structure that returns streams along with the DOM it represents. Also, the desired output is explicitly declared in the view.
  • There are no drivers. Instead, we use a library, IO, to express imperative code in a way that I think is more convenient.
  • Turbine is completely pure. We are more strict in this regard.
  • Our FRP library Hareactive is very different from RxJS.
  • We don't use virtual DOM. Behaviors/streams are plugged directly into the view and you don't have to merge into a single state-stream.

Overall I'd say that Turbine has a slightly higher learning curve because we use some more advanced ideas from functional programming (monads, for instance, are crucial to our approach). But I hope the final experience is a framework that's more convenient and powerful to use.

  1. In this example here, the return value of the view needs to be parsed in order gather the outputs and pass them to the model. This seems nontrivial to implement and nontrivial to maintain typesafety.

You are definitely right that it is non-trivial to implement. We have to pull some tricks to establish the circular dependency. But conceptually it's quite simple to understand. Creating types for it is actually easy. modelView (the function that creates components with separated model and view) has a type like this (I've removed some details):

function modelView<M, V>(
  model: (v: V) => Now<M>, view: (m: M) => Component<V>
): Component<M>;

As you can see, M is that value that the model function returns (the left side in the figure you posted) and V is the output from the view (the right side). The types say that the output from one of them must match the input to the other one. If they don't it results in a type error.

  1. In my purely functional experiments, I find myself spending a lot of time trying to partially apply callback functions while maintaining performance (you cant referentially diff these functions). So I'm curious how would create an arbitrary list of counters while binding the index of the counter to the callback function. Or how would you pass a "prop" to the counter (like an incBy number) that gets passed through to the model?

I don't understand what you mean here. What sort of callback functions do you mean? I don't think they exist in Turbine. We have a special function list for creating lists. The list function takes a dynamic list (a behavior of a list) and uses the data in the list to create components. It then collects the output from each components. We have a counters example that creates an arbitrary list of counters.

I'm not sure if that explanation makes sense or answers your question 😢. If you could expand a bit more on the question I'd like to give a better answer and/or concrete code that implements what you ask for 😄

@ccorcos
Copy link
Contributor Author

ccorcos commented May 22, 2017

Spent some more time looking through Turbine this morning... That's some pretty intense stuff! It's very well thought-out, but building a mental model for how it all works is pretty challenging. I think if you included explicit type annotations in the tutorial, it would be a lot easier to pick up on. I think it might also help me understand how everything works if you had an example that showed me how to get all the way down to the actual DOM node where I could do things like call scrollTo() or instantiate a jQuery plugin or something.

I see what you mean about the differences though. It actually is a bit different. No selectors is 👍 and the code is really clean. I'm still trying to figure out where the challenges will be... Here are some of the things I'm still thinking about:

  • If you had a global application state for sidebarOpen that you needed to access in many places, I'm assuming this would just be a behavior that you can can just import and combine in a model function? It wouldn't be pure though, right?

  • The example I was talking about in the previous comment is about how you might pass a prop to a component like this:

class Counter extends PureComponent {

	state = { count: 0 }

	inc = () => {
		this.setState({ count: this.state.count + this.props.delta })
	}

	dec = () => {
		this.setState({ count: this.state.count - this.props.delta })
	}

	render() {
		return (
			<div>
				<button onClick={this.dec}>dec</button>
				<span>{this.state.count}</span>
				<button onClick={this.inc}>inc</button>
			</div>
		)
	}

}

<Counter delta={10}/>

To stretch this abstraction even further, I might want to have two counters: the first counter has a delta of 1, and the second counter has a delta of the value of the first counter. Here's how I would do it using reactive-magic:

import { Value } from "reactive-magic"
import Component from "reactive-magic/component"

class Counter extends Component {

	count = this.props.count || new Value(0)

	inc = () => {
		this.count.update(count => count + this.props.delta)
	}

	dec = () => {
		this.count.update(count => count - this.props.delta)
	}

	render() {
		return (
			<div>
				<button onClick={this.dec}>dec</button>
				<span>{this.count.get()}</span>
				<button onClick={this.inc}>inc</button>
			</div>
		)
	}

}

class App extends Component {

	delta = new Value(1)

	render() {
		return (
			<div>
				<Counter count={this.delta} delta={1}/>
				<Counter delta={this.delta.get()}/>
			</div>
		)
	}

}

What intrigues me so much about this example is how clean the mental model is. It feels very easy to make sense of to me.

@dmitriz
Copy link
Contributor

dmitriz commented May 23, 2017

@ccorcos

feel free to show off other libraries you think are interesting or relevant!

I saw this interesting discussion and taking this "invitation" perhaps too literally :), I'd like to share the un-project, that was also discussed in the context of the Turbine here: funkia/turbine#34

Its main goal is provide a minimal "glue" for other libraries and frameworks,
but it rests on similar ideas of coupling view with model (aka reducer aka update)
as pure functions and move the impurity into the mount function
(similar to mount in Mithril, drivers in Cyclejs, and runComponent in Turbine).

I have tried to extract and abstract the essence of this pattern,
which seems just too similar across the frameworks, not to be extracted :)
The model always updates the state, and the view is controlled by the state,
and dispatches its actions back to the model.

In the basic examples both model and view are basic pure functions, importing abstract element or component creators like div, input, button that can be populated by any vdom or dom library. In the JSX you write them like HTML but I find the functional syntax easier to read, when we want to get away from the static HTML mental model into the dynamic JS view model, where element instances are results of applying element functions (the fact I feel is obscured by the JSX). Having said that, the JSX is merely a syntax compiling to the pure JS, so shouldn't be an obstacle to support if that can help attracting more people who like it :)

In the Turbine, the view function takes the state and returns the component holding some output instead of calling the dispatch function on the action. That can provide some really neat ways of reducing the state bloat by chaining the previous component's output into the following ones (enjoying the monad structure) or internally passing to the previous components via the loop function, which is also very neat.
That way we can significantly reduce the state/reducer/action noise to only what really needs to get out of the view.

However, the abstract pattern of the view taking state and returning component remains the same, where the dispatcher callback is replaced by the component output. And the accompanying model is closing the view's "microCycle" by taking the output and returning the state, wrapped in some monad to make it pure.

The model also holds internally some initial state (the 0 in this example), that can be extracted and passed down to it by the parent in some more complex architecture. That initial state parameter is again similar to the Elm architecture or your JS implementation, which I like a lot :).
In the Turbine, that would be one of the parameters passed from the parent.

So on the abstract level, the model takes some external state and the view's output and returns some updated state (like adding new actions to it), passed back to the parent, that includes the internal state, to be consumed by the view. And again, we arrive to the same reducer picture:

(state, action) -> state

So it looks like there is always the same abstraction behind that I would like to extract
into something universal and reusable. I do believe this can create value
in avoiding rewriting the same things again and again in slightly different frameworks,
and helping using other people's work away from their specific setups.

Which is what the un-project is after and I will love to hear any feedback or critics. 😄

@adamhaile
Copy link

adamhaile commented May 23, 2017

@paldepind slow reply as I was at a conference. Good questions, and let me see if I can elaborate.

I just want to mention, by the way, that I hope I don't seem like I'm coming into your Issues and insulting your work. This is in the spirit of sharing experiences between implementers. I particularly think hareactive is interesting and want to follow its development.

Also, to provide a bit of context on the discussion, automatic dependencies aren't a goal in S, they're a means to an end. The central abstraction in S is the unified global timeline of atomic instants. Synchronous programming literature calls this "logical time" or "the synchronous hypothesis." Automatic dependencies are just one part of achieving that, along with things like guaranteed currency, immutable present state, and so on.

With flyd, the core abstraction, if I have it right, really is the dependency graph. I don't know whether I'd want automatic dependencies in that scenario. It takes the whole set of behaviors to make "logical time" a strong abstraction in S.

So anyway, to your points:

abSum = lift(add, a, b);

In regards to conciseness, I was thinking of the fact that flyd's form requires stream names to be repeated thrice, verses once in S (aka DRY). Expanding add to the full function:

abSum = S(() => a() + b()); // one a, one b
abSum = lift((a, b) => a + b, a, b) // three a's and b's

I see this being particularly an issue when we have a higher number of streams than 2. The real-life case that immediately jumps to mind is the className attached to a component's main element, which often includes numerous decorator classes to indicate different states of the model. Determining which are active can touch on dozens of streams. Repeating each of those three times, and making sure we do so with parallel ordering where needed, strikes me as burdensome.

The verbosity grows with complexity. Say we want to sum a().b().c() to d().e().f():

// S
const sum = S(() => a().b().c() + d().e().f());

// flyd, I think I have this right after reading the docs, but let me know :)
const ab = flatMap(a => a.b, a),
    abc = flatMap(b => b.c, ab),
    de = flatMap(d => d.e, d),
    def = flatMap(e => e.f, de),
    sum = lift((add, abc, def);
// connect lifecycles, so as not to orphan ab, abc, de and def when sum ends
flyd.endsOn(sum.end, ab);
flyd.endsOn(sum.end, abc);
flyd.endsOn(sum.end, de);
flyd.endsOn(sum.end, def);

You say that lift allows you to abstract over the streams and get real-word reuse. Given that we're almost always constructing S computations inside a function, that function has already abstracted over the streams via its parameters. So it'd have to be a case where we wanted to use the same named function with different streams in the same closure. I can't think of a case where that has been a feature I wanted in a real world program. Generally, the definition of the function is closely tied to its context, so function expressions are used rather than named functions. It's possible that we have different styles here, so let me know if you had an example in mind.

var a = S.data(true), // like flyd.stream(true)
   b = S(() => a() ? 1 : c()),
   c = S(() => a() ? b() : 2);

That is what I would call a circular dependency.

On the contrary, b() and c() are both well-founded. Their values are determinate in all program states, either 1 or 2 depending on a().

Some FRP libraries solve that using a form of lazy evaluation. Which also seems to be what you're doing here.

S is an eager library and calls the passed function immediately. b()'s initial evaluation does not call c(), so it doesn't throw, and c() is defined before a() can change and cause it to be called.

One further point I'll add is that S's automatic dependencies are strictly more powerful than explicit ones in that lift is trivial to implement in S (leveraging S.sample()), but automatic dependencies can't be implemented in flyd. I didn't mention this last time because I thought @ccorcos 's Reactive Magic might be a proof-by-counterexample, but after looking at the code, I see he's only generating dependencies from the first evaluation.

Explicit dependencies are declarative about both values and dependencies.

Dependencies yes, but values are only known if we take into account the full history of the system, due to the fact that undeclared streams end up preserving prior states (whatever their value was the last time one of the declared dependencies changed). An S computation is guaranteed to be a function on current state. A flyd dependent stream is only guaranteed to be a function on total history, which isn't much of a guarantee.

To give a concrete example:

var a = flyd.stream(1),
    b = flyd.stream(2),
    abSum = flyd.combine(b => a() + b(), [b]),
    c = flyd.stream(3),
    abcSum = flyd.combine(c => c() + abSum(), [c]);

If we ask "what's the value of abcSum," it's not a() + b() + c(). It's "c() plus the value of b() the last time c() changed, plus the value of a() the last time b() changed before the last time c() changed."

With just two streams and a toy function, the missing dependencies are obvious, but would they be with more streams and a much larger function body? If the recommendation is "don't do that, be sure to list dependencies for all referenced streams," then isn't that identical behavior to automatic dependencies but with a lot more hassle to the user?

You can, by the way, build the same behavior in S, but you have to be explicit that it's what you want, not merely forget to list a dependency for a referenced stream:

var a = S.data(1),
    b = S.data(2),
    abSum = S.on(b, () => a() + b()), // or S(() => S.sample(a) + b()),
    c = S.data(3),
    abcSum = S.on(c, () => c() + abSum());

S.on() and S.sample() limit the events on which the given functions run.

I think the keys to a good mental model in a reactive library is to make a distinction between values that change over time and events that happen over time. What in classic FRP is called behavior and event.

There are two axes available for abstraction: what kind of values we have and what kind of time. FRP abstracts on values, with a taxonomy of events and behaviors, while SRP abstracts primarily on time. I can point you towards papers if you're curious. This paper on Lucid Synchrone is a nice introduction.

It seems to me like S.js only has a representation for values that change over time and not one for events?

Currently, you're correct ... ish. This is an intentional experiment with S. I actually built a couple apps with and without an event type and found the one without much easier to reason about. I also found training (I've got a couple other guys writing S code) much simpler without. In fact, I couldn't come up with a scenario for which an event type led to clearer code, but perhaps you can :). Events are always the point that Reactive Banana tutorials go "ok, I know this is getting confusing ...." I can go into my thinking here, but have obviously abused your time enough at present.

Cheers
-- Adam

@paldepind
Copy link
Owner

@ccorcos

Again, thank you for the feedback. I really appreciate it 👍 Since there are now several things going on in this thread I've answered your comment in a new thread here funkia/turbine#51.

@adamhaile

I found your comment to be really interesting. I'll have to take a look at the paper you linked and then I'll give you a proper reply 😄

@StreetStrider
Copy link
Contributor

feel free to show off other libraries you think are interesting or relevant!

I was off-thread for the whole time and not sure I can catch up with all you'd discussed here. But I got in mind this thing: pmros/cyclow
Magic level from low to medium. Clear and sane structure. Worth a time to investigate this thing.

@paldepind
Copy link
Owner

@adamhaile

I just want to mention, by the way, that I hope I don't seem like I'm coming into your Issues and insulting your work. This is in the spirit of sharing experiences between implementers. I particularly think hareactive is interesting and want to follow its development.

You don't seem like that at all 😄 Sorry if I came across as defensive. I definitely enjoy the spirit of sharing experiences. Having one's work insulted is a great opportunity to learn something and create even better work in the future.

I find S very interesting. I think you make a very good case for automatic dependency management. I am actually seriously considering if I should implement it in Hareactive for the cases where it's useful. If you can say more about the benefits I'd love to hear it 👂

I think maybe we are approaching reactive programming from two different sides. Clearly, you know a lot about synchronous programming. I've never looked much at synchronous programming. But, to be a bit blunt, that's because FRP always seemed superior to me.

On the contrary, b() and c() are both well-founded. Their values are determinate in all program states, either 1 or 2 depending on a().

I can see that it is well-founded 😉 The body of the definition of b mentions c and the body of c mentions b. That is what I call a circular dependency. You could also say that they're mutually recursive.

One further point I'll add is that S's automatic dependencies are strictly more powerful than explicit ones in that lift is trivial to implement in S (leveraging S.sample()), but automatic dependencies can't be implemented in flyd.

In FRP lift is simply a method that we get for free from the fact that a behavior is an applicative. It's not supposed to be "all powerful" like the S function. lift has the appropriate power for some cases. It is just one among the methods in an FRP library. And some of the other ones cannot be implemented by S 😉

The verbosity grows with complexity. Say we want to sum a().b().c() to d().e().f():

I can't dispute that the S code is a lot more concise in that case. If reaching into a heavily nested structure like that happened often to me I'd probably create a helper function to do it.

With Flyd, the core abstraction, if I have it right, really is the dependency graph.

The core abstraction in Flyd is the stream. The dependency graph is merely an implementation detail. But, Flyd does not reflect my current opinions on the ideal FRP library. Hareactive does. In particular, Hareactive follows the semantics of classic FRP where the primary abstractions are behaviors and event (what FRP traditionally call "event" is called "stream" in Hareactive).

FRP abstracts on values, with a taxonomy of events and behaviors, while SRP abstracts primarily on time.

To me, FRP is definitely an abstraction over time. A behavior is semantically a function over time, i.e. a value that changes over time.

I mentioned above that I think FRP is more attractive than SRP. Here are my two primary reasons.

The paper you linked describes their abstraction over time as "infinite sequences". That makes it ill-suited to represent continuous phenomenon as sequences are discrete. The fundamental problem here is that time is treated as if it was discrete. But, that is not the way humans experience time. It's not the way physicists model time either—because discrete time is a poor model of the real world.

It's also clear from the paper that not only is their abstraction over time discrete, the "discreteness" is also exposed in the API. For instance, their fby delays a value by a "unit delay". But again, such unit delay doesn't exist in the real world. Humans do not perceive time as a sequence of atomic instants separated by some "unit delay". This makes it a bad mental model to program in.

FRP, however, defines behavior as functions over time. And, importantly, time is represented as the reals. This means that a behavior in FRP has infinitely dense resolution. This makes it strictly more powerful than the abstraction described in the paper. Continuous behavior can represent things that change infinitely often. Discrete sequences are countable but the reals are uncountable.

The second reason is that SRP, doesn't make a distinction between behavior and event. Conceptually these are two different things. I have written more about why that distinction is useful in a blog post here.

To supplement the blog post, let me offer a real-world example: let's say we want to implement a game where the user controls a circle and the enemy in the game is also a circle. The player starts with 5 lives and each time he collides with the enemy circle a life should be subtracted. Each circle has radius 10 so the circle collides when the distance between them is less than 20.

// Continous behavior of player position as the center of the circle
const playerPosition = ...
// Same for the enemy
const enemyPosition = ...
// The distance between the player and the enemy
const distance = lift(({x, y}, {x2, y2}) => Math.sqrt(...), playerPosition, enemyPosition);
// Behavior that is true whenever the circles overlap
const areCollided = distance.map((d) => d <= 20);
// event that triggers on collision (this is similair to `edge` in SRP)
const collisionEvent = areCollided.changes().filter((value) => value); // keep when changes to true
const nrOfLives = scan((lives, _) => lives - 1, 5, collissionEvent);

Note here how I start out with a bunch of behaviors, but when I want to represent collisions I shift to events. That is because conceptually collisions are event that happens over time. Here making a distinction between the two is highly beneficial. The types tells me what phenomenon I'm modelling and they restrict me to the operations that make sense on them.

Currently, you're correct ... ish. This is an intentional experiment with S. I actually built a couple apps with and without an event type and found the one without much easier to reason about. I also found training (I've got a couple other guys writing S code) much simpler without. In fact, I couldn't come up with a scenario for which an event type led to clearer code, but perhaps you can :). Events are always the point that Reactive Banana tutorials go "ok, I know this is getting confusing ...."

I find it odd that you think event is the hard one to understand. Most reactive libraries in JavaScript only offers an abstraction that is much closer to FRP's event than it is to behavior. IMO neither are hard to explain. I make an attempt in the previously linked blog post.

Also, in the example on the S readme you seem to get around the lack of events by pushing into todos. That is not a good solution to me as I want something that is pure and pushing is not.

@paldepind
Copy link
Owner

@adamhaile

Currently, you're correct ... ish. This is an intentional experiment with S. I actually built a couple apps with and without an event type and found the one without much easier to reason about. I also found training (I've got a couple other guys writing S code) much simpler without. In fact, I couldn't come up with a scenario for which an event type led to clearer code, but perhaps you can :). Events are always the point that Reactive Banana tutorials go "ok, I know this is getting confusing ...." I can go into my thinking here, but have obviously abused your time enough at present.

I've looked a bit more at S. And it actually seems like the signals in S are more like FRP's event than like FRP's behavior.

In particular, the "reducing computation" feature exposes how often a signal is pushed to. Such an operation cannot be explained with the semantics of behavior (a function over time) but can only be explained with the semantics for event. The S function with a seed is comparable to accumE in Reactive Banana and that is a function on Event.

@paldepind
Copy link
Owner

Closing as this it not an issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants