Open paldepind opened 7 years ago
I hear you on all those points. I'm a huge fan of purity and functional programming, but I've had a slight change of perspective recently. I was diving deeper and deeper into point-free programming because it was so declarative, but I started to realize how opaque it was.
I then realized that my real interest is the "mental models" we create to represent the things we think about, and not necessarily the mathematical beauty of functional programming. To give you a little taste, I wrote this article about a better mental model for number systems, and I created this prototype instrument to explore a better mental model for creating music (select the notes in your scale on the left and play them on the right or using the asdf keys on your keyboard, rotate the pie to change modes, slide the keyboard to change inversions).
So now back to Reactive Magic and Turbine -- I totally agree with all of your points, but what if we think about these things from a mental model perspective. A Value
is like a cell in Excel. Its totally impure, but the abstraction clean and intuitive. When you say "The input to counter is mixed with the output from the counter.", my thinking is "That's exactly whats happening!". We're constructing a machine using Value
s as an abstraction over things that change and wire all these pieces together as if they aren't changing. What make Reactive Magic so interesting to me is the mental model fits very comfortably in my head. I don't have to think about thing. I can just grab the Value
s I need, put them together, and it works!
I have some more ideas when it comes to programming mental models, but I'm really interested to understand Turbine better. It amazes me that the program you wrote works the way you intend. I don't think I can fully understand this without seeing the type definitions written out. I can't even figure out why we are yielding anything from the counterModel.
As far as the jQuery plugin concept, one thing I like about arbol is how I've created a clean division between the pure and declarative world, and the impure effectful world. Integrating an external library is actually quite easy. For example, I'm using keymaster
for reacting to hotkey events. I create an element that is effectively just a mock HTMLElement, and then I write some hooks to patch elements together. Maybe this approach is totally irrelevant for Turbine, but I thought it was worth noting because it gives you a way of escaping all the gnarly abstraction to write your own effectful imperative code...
@ccorcos
I was diving deeper and deeper into point-free programming because it was so declarative, but I started to realize how opaque it was.
I definitely agree that point-free programming can be very hard to read. But I don't agree that point-free programming is more declarative. I'm not really a fan of the way in which many people use the word "declarative" to mean "doesn't look like actual code". IMO as long as a function is pure it is 100% declarative. It doesn't get more declarative just from being written in a point-free style.
I then realized that my real interest is the "mental models" we create to represent the things we think about, and not necessarily the mathematical beauty of functional programming.
I completely agree that mental models are highly important. But, I would argue that mental models go hand in hand with math. Because math gives us a precise language. And without precision our mental models are just handwaving. I'm very inspired by Conal Elliott who invented FRP. He talks a lot about having simple mental models (he just calls it "semantic", i.e. figuring out the meaning of things). And one of his major points is that a mental model can't be simple without being precise. Because if we can't be precise about it then we don't really understand it. Here is a relevant quote:
Everything is vague to a degree you do not realize till you have tried to make it precise. โ Bertrand Russell
If we do not make our models precise we cannot know if they are simple. As an example of this, the mental model for a behavior is a function of time. That's all. That is not only precise mathematicallyโit is also extremely simple and can be easily visualized. Every single function in Hareactive on behaviors can be understood based on this simple mental model.
So now back to Reactive Magic and Turbine -- I totally agree with all of your points, but what if we think about these things from a mental model perspective. A
Value
is like a cell in Excel. Its totally impure, but the abstraction clean and intuitive.
I'm not sure if the abstraction is clean. What does it mean to be "like a cell in Excel"? A cell in Excel is a piece of text. A Value
isn't just a string? How do you explain what get
, update
and DerivedValue
mean without talking about implementation details?
When you say "The input to counter is mixed with the output from the counter.", my thinking is "That's exactly whats happening!". We're constructing a machine using
Value
s as an abstraction over things that change and wire all these pieces together as if they aren't changing. What make Reactive Magic so interesting to me is the mental model fits very comfortably in my head. I don't have to think about thing. I can just grab theValue
s I need, put them together, and it works!
What do you mean with "that's exactly what's happening"? To me you're constructing a machine that you put Values
into. However, it wont treat at values the same. It will read from some of them and write to others. The machine itself doesn't indicate to which it will do what. You'll have to know about the internals of the machine to know. It doesn't seem like a nice machine to use.
On the other hand, a component in Turbine has one hole for input and another hole for output. This makes it easy to see what goes in and what goes out. To me that is not only a nicer mental model, it is also a nicer thing to code with.
I think it's very interesting to hear your thoughts on mental models. If I understand correctly what you value is the intuition in the model and how easy it is to wrap one's head around. That seems a bit different from my approach. Because I also what the model to be precise in a mathematical sense. I'd love to hear more about how you think about the mental model in Reactive Magic?
If you're interested in my angle you may want to hear this podcast with Conal Elliott where he talks about how he invented FRP based on an idea about giving precise and simple meaning to things. You may also be interested in this blog post of mine where I discuss some of the same things and "reinvent" FRP based on those principles.
@ccorcos
I created this prototype instrument to explore a better mental model for creating music
โค๏ธ
I can't even figure out why we are yielding anything from the counterModel.
const app = go(function* () {
const {
count // get the end of the "count" output wire
} = yield counter({ delta: Behavior.of(1) }); // of this component
yield counter({ delta: count }); // and connect it to this component's input hole.
});
Oh, you meant about the
const count = yield sample(scan((n, m) => n + m, 0, changes));
Yeah, I'm not sure what that is either, I glazed over it, and the README doesn't really say? ~@paldepind Can you point to the explanation for those?~ https://github.com/funkia/hareactive#stateful-behaviors-work
Whatever they do, I'm guessing the yield
is for the same reason as I described: take the end of the output wire, then return it to be used as input somewhere else (and that the output wire just isn't from a UI component in this case).
@paldepind Is that a good way to think about it? Or is there a another way?
@ccorcos @trusktr
I have put this probably simplest possible example to illustrate it here: https://github.com/funkia/turbine/pull/48
It does not use any methods, so might be easier to understand.
Every yield
"writes" a new dom node,
which all get concatenated together at the end.
Independently, the output is extracted and passed to the next component as argument. That way there is no need to deal with any state outside, which can cuts down the code a lot. Just think of all those actions boilerplatte you would need to write with Redux instead. ;)
IMO as long as a function is pure it is 100% declarative. It doesn't get more declarative just from being written in a point-free style.
I suppose. But even pure functions can have some imperative annoyances if they are composed together.
But, I would argue that mental models go hand in hand with math. Because math gives us a precise language.
Math definitely offers some guides for how to think about things. But it's not the only precise language. You can write very imperative code with in-place mutations that is very straightforward and precise.
One example of where I think math can start to convolute is that many people do not understand integrals and derivatives simply as summing up values and taking the difference between values. When you become very comfortable with math, that's what you eventually come to understand, but until then, you have to learn these awkward motions for moving numbers and symbols around for no apparent reason...
If I understand correctly what you value is the intuition in the model and how easy it is to wrap one's head around. That seems a bit different from my approach...
Exactly. Well I value both... But with Reactive Magic I decided to try out one extreme and found it to be quite interesting.
I'd love to hear more about how you think about the mental model in Reactive Magic?
Its hard to explain in a way. The goal is for the mental model to be totally obvious, the way a storyboard is totally obvious. Have you ever checked out Apparatus? Its a little clunky to use at times, but I think the mental model is totally obvious. That's the kind of thing I'm going for.
This might be a bit of a stretch, but one way I think about it is that I don't want to get bogged down writing the causality of one thing happening from beginning to end all at once. I want to be able to break things up into pieces the way I think about things. I want to be able to create contraints as I go without having to keep the big picture in my head or diligently creating the perfect abstractions around pure functions... Think about this in regards to Excel -- you can take a cell and derive it from any other cell. And you can keep on doing this until you have a very complicated system. And it doesnt really matter what order you do all of this in. Similarly with Apparatus...
If you're interested in my angle you may want to hear this podcast with Conal Elliott where he talks about how he invented FRP based on an idea about giving precise and simple meaning to things. You may also be interested in this blog post of mine where I discuss some of the same things and "reinvent" FRP based on those principles.
I'll check these out and get back to you. Thanks!
@paldepind this link is broken: https://github.com/funkia/turbine/issues/vindum.io/blog/lets-reinvent-frp/
@trusktr I think the "take the end of the wire" analogy is really good. It works very well for Component
but not quite as well for Now
. Does the explanation of Now
in the Hareactive readme make sense to you? If you have any questions I'd love to hear them.
@ccorcos
I suppose. But even pure functions can have some imperative annoyances if they are composed together.
What do you mean? How can a pure function have imperative annoyances? To me, pure and imperative are mutually exclusive.
You write
I don't want to get bogged down writing the causality of one thing happening from beginning to end all at once.
And
Think about this in regards to Excel -- you can take a cell and derive it from any other cell. And you can keep on doing this until you have a very complicated system.
What is the difference? It seems the same to me. In Excel you begin by defining some cells in terms of other cells. Isn't that the same as writing the causality between things?
Btw, I've figured out a way to get something very similar to the automatic in Reactive Magic but in a way that is pure and fits in well with the FRP semantics. I'll be adding it soon to Hareactive.
link is broken
Sorry. GitHub turned it into a relative link. The correct link is http://vindum.io/blog/lets-reinvent-frp.
How can a pure function have imperative annoyances?
Even with pure functions, if they aren't composed together well, then you end up with all these ridiculously named intermediary variables -- which is why I think point-free is more declarative.
const x = 1
const xPlus2 = add(2, x)
const xPlus2Times4 = times(4, xPlus2)
const y = minus(1, xPlus2Times4)
y = pipe([
add(2),
times(4),
minus(1),
])(x)
What is the difference? It seems the same to me. In Excel you begin by defining some cells in terms of other cells. Isn't that the same as writing the causality between things?
Hmm. Perhaps I didn't explain that well. When you have a Component
in reactive-magic
then the view
function is "reactive" so that if you .get()
any Value
s, then whenever those values update, the function will re-run. Its the same exact thing as constructing a stream in flyd except you don't have specify all the dependencies up-front. In a big nasty component, you might have all kinds of helper functions that get called in the view
function. And what I find elegant about reactive-magic
is that if you need to change some style based on whether or not the sidebar is open, you can simply .get()
that value and everything just works. And imagine this function is used across many components in deep places -- you don't have to track down everywhere its called inside of every view
function and add the sidebar state as a dependency of the view
stream. And if you need to add a new dependency, you dont have to go through this tedious process all over again -- its basically just magic ;) So in terms of causality, I guess my point is that you dont need to know everything that causes a component to update before you write the function. And when you have a big complicated application with lots of different shared helper functions, this is incredibly convenient. For example, I work at Notion (let me know if you want a free subscription ๐). Its crazy how hard it is to coordinate so many things that are fundamentally impure like the contenteditable API, and I've found this approach to be very useful.
Btw, I've figured out a way to get something very similar to the automatic in Reactive Magic but in a way that is pure and fits in well with the FRP semantics. I'll be adding it soon to Hareactive.
Please, do tell!
I read your blog post -- very nice explanation! I really like how you derived everything starting with the fundamental motivations.
A few things that immediately caught my eye:
circle
function with no indication of how works without breaking purity..reduce
slower and slower.What do you think? I'm going to look into Hareative more now and see what I can figure out.
@ccorcos
This reply got a bit long. Sorry.
Hmm. Perhaps I didn't explain that well. When you have a
Component
inreactive-magic
then theview
function is "reactive" so that if you.get()
anyValue
s, then whenever those values update, the function will re-run. Its the same exact thing as constructing a stream in flyd except you don't have specify all the dependencies up-front. In a big nasty component, you might have all kinds of helper functions that get called in theview
function. And what I find elegant aboutreactive-magic
is that if you need to change some style based on whether or not the sidebar is open, you can simply.get()
that value and everything just works. And imagine this function is used across many components in deep places -- you don't have to track down everywhere its called inside of everyview
function and add the sidebar state as a dependency of theview
stream. And if you need to add a new dependency, you dont have to go through this tedious process all over again -- its basically just magic ;) So in terms of causality, I guess my point is that you dont need to know everything that causes a component to update before you write the function. And when you have a big complicated application with lots of different shared helper functions, this is incredibly convenient.
Thank you for the explanation. I think I get it now :smile: You like how you can just use the stuff you need and changes will just propagate without you having to think about it. I can definitely see the convenience of such an approach.
How does the view get ahold of the sidebar-is-open state? Is it exported from a file so that anyone can simply grab it? If any code can just grab the stuff it wants to use doesn't it make it hard to figure out what the consequences of changing something might be? I'd be worried that the code might end up hard to understand and maintain? For instance, a
might be used in b
. I now want to change something in b
and for this is makes sense to also change a
. But, I had forgotten that c
also used a
and by changing a
I break c
.
I think with too many implicit dependencies code can be hard to understand. It sounds like it could give some of the same problems as in imperative programming where one variable may be mutated many places and where it gets hard to figure out exactly what happens to the variable.
I read your blog post -- very nice explanation! I really like how you derived everything starting with the fundamental motivations.
Thank you. I'm glad you liked it :smile:. You make some very good points.
you introduce the
circle
function with no indication of how works without breaking purity.
I do try to give an explanation of it in the blog post. But, I guess it's not sufficient. circle
is simply implemented as this pure function:
function circle(radius: number, x: number, y: number): Image {
return [{ radius, x, y }];
}
The source code for the entire library and the examples are here btw. The circle
and stack
function is defined here.
the way streams work is not performant. It will grow unbounded making
.reduce
slower and slower.
Yes! That is absolutely correct and very well spotted :+1:. Implementing pure FRP without such leaks is actually quite tricky. I've written a bit about it in this comment (the relevant part starts with "Now
solves FRPs notorious problem ..."). There I try to explain why implmenting scan
without leaks is tricky, compare various scan
implementations, and show how the scan
in Hareactive is both pure and memory safe. There is also an explanation in the Hareactive readme.
it appears you have to use requestAnimationFrame for the whole architecture to work. Something about this feels less elegant than simply reacting to an event when it happens. Otherwise, you're unnecessarily running through a loop when you dont have to.
Indeed. There are two approaches to implementing FRP: "push-driven" and "pull-driven". Push-driven is when you push events from the top down. I.e. what you describe. Pull-driven is when the state is computed from the bottom and up. The code in the blog post is "pull-driven". Pull-driven is easy to implement in a purely functional way which is why I did that in the blog post.
For the purpose of animations, pull-driven is actually very elegant. Note how most things in the examples in the blog post are changing continuously. For instance, the position of the ball changes arbitrarily often. How would you represent the movement of the ball as reacting to an event? How often should these events fire? 60 per second? 120 per second? Any number you choose would be arbitrary and inappropriate in some cases. The beauty of the pull approach is that we don't have to make that choice. We can simply pull the system in requestAnimationFrame
and everything will smoothly follow the framerate of the user.
As you pointed out, the pull-driven approach also has a downside. If we're reacting to something that changes rarely and in response to user events then pulling every frame is totally inefficient. That is why Hareactive uses a combination of push and pull. For things that are event driven we use push and for things that "changes all the time" we use pull. In this way, we can get the best of both worlds :smile:
Please, do tell!
I will :smile:. I've added the function to Hareactive. It's currently called moment
(we might rename it to derive
or something else). It works a lot like DerivedValue
in reactive-magic but is completely pure. I'll try an explain it below. Please let me know if the explanation doesn't make sense.
Semantically, behaviors are a function from time to a value. So, in theory, we could create a function with the following semantic:
const at = <A>(t: Time, b: Behavior<A>) => b(t);
However, implementing this in practice is impossible since users could then sample behaviors at any moment in time. Arbitrarily into the past or even in the future. But, if the at
function where partially applied with a time value inside the library we could give users this partially applied at
. That is the idea behind moment
. Semantically moment
is simply this:
const moment = (f) => (t) => f((behavior) => behavior(t));
It can be used like this:
const z = moment((at) => at(x) + at(y));
moment
takes a function that takes a function of type <B>(b: Behavior<B>) => B
. I.e. it gets a partially applied at
. Conceptually, each time the function passed to moment
is invoked it receives at
partially applied to the current time. So everything is pure and the semantics are very simple.
In the above example, I'd probably still use lift
as I've argued previously. But, I think moment
can be super useful in cases where behaviors are nested or dependencies are dynamic. The discussion over in the Reactive Magic thread in the Flyd repo convinced me of that. Here is a simple example
const baz = moment((at) => at(booleanBehavior) ? at(foo) : at(bar);
In that example, the baz
behavior would dynamically switch between listening to foo
and bar
depending on booleanBehavior
. lift
can't do that :smile:. Another use case could be if you have a behavior of type Behavior<Behavior<number>[]>
and you want to create a behavior of the sum of the changing numbers in the changing array. With moment
you could simply do:
const totalSum = moment((at) => at(listOfNumbers).map(at).reduce((n, m) => n + m, 0);
I'm going to look into Hareative more now and see what I can figure out.
Let me know how it goes! I hold your opinions in high esteem and I really appreciate the great feedback you've already given us.
I work at Notion (let me know if you want a free subscription ๐).
I hope you enjoy it :tada: I took a quick look and it seems interesting and gave a good impression. When I get the time (I currently have exams :cry:) I'll take a close look. I currently use Trello.
@paldepind
const baz = moment((at) => at(booleanBehavior) ? at(foo) : at(bar);
In that example, the baz behavior would dynamically switch between listening to foo and bar depending on booleanBehavior. lift can't do that ๐
Would it be different from the one below?
const baz = lift((bool, f, b) => bool ? f : b, booleanBehavior, foo, bar)
@dmitriz
Would it be different from the one below?
Yeah. I wasn't clear enough about that. The difference is mostly an implementation detail that can give better performance. The lift
code does the same. But the function to lift will be called whenever foo
or bar
changes. The version using moment
figures out which behaviors are actually used at a given time. So when booleanBehavior
is true
it will not listen to changes from bar
.
How does the view get ahold of the sidebar-is-open state? Is it exported from a file so that anyone can simply grab it? If any code can just grab the stuff it wants to use doesn't it make it hard to figure out what the consequences of changing something might be? I'd be worried that the code might end up hard to understand and maintain? For instance, a might be used in b. I now want to change something in b and for this is makes sense to also change a. But, I had forgotten that c also used a and by changing a I break c.
The sidebar lives as a global value inside a World
object. You're right that this started to get complicated and can become hard to understand, but I wonder if this is not always the case. Purity tends to come with extra verbosity which itself can hinder understanding. So what's important with reactive magic is to maintain clear semantics and abstractions around what something is or does. So in your example, if a
, b
, and c
have a well-defined purpose, then if you change a
for the sake of b
, then its likely that you intended for c
to change as well.
I read through the documentation again and its still hard for me to really grasp. I think whats missing for me is a tutorial that builds up a few entire examples app from scratch. That's what you started to do in your tutorial, but you never really showed you the circle function works and how you start everything up. Otherwise I'm just a little lost. I did something like that here building larger and larger abstractions, just for the sake of learning. I know there's a lot of complexity to hareactive and turbine, but I'm curious if you stripped everything away, what it would look like to build a simple counter like this but with the turbine architecture. Maybe you'd build just exactly like your blog post? But then I'd like to see some follow-up examples that try to explain in a minimal way how you deal with the shortcomings we discussed.
I'm really happy Reactive Magic was able to inspire you to write moment
:)
Also, I'm working on this project grap (for lack of a better name) and I'm building it with Reactive Magic. Its still a work in progress, but there's already a decent amount of complexity and I think it might be an interesting experiment for you to test out with turbine. You can boot it up pretty easily (npm install && npm start
), and for the most part, all it is right now is an infinite canvas where you can right click to create blocks, move blocks around, select multiple blocks, delete blocks, zoom the canvas with control+scroll (or pinch on a trackpad), and translate the canvas by scrolling. But I'm imagining doing this in a pure functional way and its hard because there is so much interdependent state. For example, when you click and drag a single block, you need to drag another other blocks that might be in selected as well. Thus the most intuitive way to deal with this is to have a global variable representing the selection. Otherwise, you'll be passing this selection around everywhere.
Let me know how it goes! I hold your opinions in high esteem and I really appreciate the great feedback you've already given us.
Thanks dude! You tend to write some really interesting libraries so I'm always happy to learn from you!
@ccorcos
Purity tends to come with extra verbosity which itself can hinder understanding.
I definitely agree that is what happens in many pure approaches. But it doesn't have to be that way. We are trying to avoid it in Turbine.
I read through the documentation again and its still hard for me to really grasp. I think whats missing for me is a tutorial that builds up a few entire examples app from scratch. That's what you started to do in your tutorial, but you never really showed you the circle function works and how you start everything up.
Thank you for the critique. We definitely need to improve the documentation. Is there anything specific that is tricky to grasp? If so please share it as it would be helpful to know what to focus on in the documentation :smile:
I know there's a lot of complexity to hareactive and turbine, but I'm curious if you stripped everything away, what it would look like to build a simple counter like this but with the turbine architecture. Maybe you'd build just exactly like your blog post? But then I'd like to see some follow-up examples that try to explain in a minimal way how you deal with the shortcomings we discussed.
In a sense, there is some complexity. I think mostly due to our use of monads which is not that familiar to JS developers. But I think the complexity is appropriate. I.e. it solves real problems. I've compared it to callbacks vs. promises. Promises are more difficult to understand than plain callbacks. But, the complexity has the effect that code using promises ends up being simpler.
I'm really happy Reactive Magic was able to inspire you to write
moment
:)
Me too. It makes some patterns much nicer to code :+1:
Also, I'm working on this project grap (for lack of a better name) and I'm building it with Reactive Magic. Its still a work in progress, but there's already a decent amount of complexity and I think it might be an interesting experiment for you to test out with turbine.
I gave it a spin. It looks like something that would be interesting to program with Turbine. I do have a lot of stuff on my plate though. But, where do you think would be an appropriate place to start? Something that's particularly challengin?
@paldepind I think I came up with a good example motivating my divergence from pure functional code.
Suppose you have a gigantic application represented as a tree of components. Suppose you want to share some global state amongst a few of these components deep inside the tree. A realistic example of something like this is a button in a page that closes a sidebar, or a color theme.
In a pure functional application, you have no choice but to thread that state through the arguments all of the top-level components to the components that need them. To alleviate some pain, you might wrap up all this state in an object and pass the entire object down to all children components. But that means that some components are going receive state they don't need causing them to re-render unnecessarily. Needless to say, this can be quite frustrating.
With reactive magic, all you need to do is .get()
that global variable wherever you want to use it and it just works! Its magic! โจ
@ccorcos
This is an answer to https://github.com/paldepind/flyd/issues/142#issuecomment-303168820.
Again, thank you for the feedback and thank you for taking the time to take a look at Turbine. It is really useful to get feedback like that and I'm grateful that you're shaing you opinion ๐
For using a jQuery plugin we'd probably have to create a mount hook that gives the raw element. We haven't done that yet though. Regarding
scrollTo
I'll get back to you with an example.Let me know if you figure it out ๐. We have tried to make Turbine as powerful as possible. There are some functional frameworks that achieve purity by limiting what one can do. In Turbine we have tried to create an approach that is pure but without making things harder.
Turbine is completely pure. The answer to the question "is it pure" should always be "yes".
sidebarOpen
would probably be created inside a component and then the component would have to pass it down the children that need it.You wouldn't be able to just import it. We can do that with a few behaviors. For instance, the mouse position, the current time and keyboard events can simply be imported. That is because they "always exist" in the browser. But
sidebarOpen
would be created inside some component so it can't be a global behavior that can be imported.That is a good example. Here is how one could write that using Turbine.
You can check out the example live here.
I think your example is nice. But, I think the one I wrote in Turbine is even better. I think the Turbine code avoids some problems. Problems that I see in many frameworks and some we've particularly tried to avoid in Turbine. Here are some of the problems.
delta = new Value(1)
doesn't actually tells me what delta is. It says thatdelta
is equal to 1 but that obviously isn't the entire truth. This means that if I want to know whatdelta
actually is I'll have to find all the lines that usedelta
. In a real app that can be hard.inc
method I cannot see who may call it. It might be called once, twice, or many times in the view. This makes it hard to figure out when exactly the side-effect thatinc
has is triggered.It looks like both
count
anddelta
are input to the component. But, sinceCounter
callsupdate
oncount
it actually uses it as an output channel. This means that whenever aValue
instance is passed to a component it's hard to know if it's actually input or output.I apologize for being a bit hard on your example ๐ The problems I pointed out are found in most React code. Let me explain how Turbine avoids them.
const
the definition tells everything about the defined value. This makes it very easy to look at the code and figure out what things are.counter
is a function that returnsComponent<{count: Behavior<number>>}
.I think the properties I described above makes Turbine code easy to understand and will make it scale really well with increasing complexity.