-
Notifications
You must be signed in to change notification settings - Fork 27
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 #51
Comments
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 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 |
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 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:
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.
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
What do you mean with "that's exactly what's happening"? To me you're constructing a machine that you put 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. |
❤️
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? |
Whatever they do, I'm guessing the |
@paldepind Is that a good way to think about it? Or is there a another way? |
I have put this probably simplest possible example to illustrate it here: It does not use any methods, so might be easier to understand. Independently, the output is extracted and passed to the next component as argument. |
I suppose. But even pure functions can have some imperative annoyances if they are composed together.
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...
Exactly. Well I value both... But with Reactive Magic I decided to try out one extreme and found it to be quite interesting.
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...
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
What do you mean? How can a pure function have imperative annoyances? To me, pure and imperative are mutually exclusive. You write
And
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.
Sorry. GitHub turned it into a relative link. The correct link is http://vindum.io/blog/lets-reinvent-frp. |
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)
Hmm. Perhaps I didn't explain that well. When you have a
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:
What do you think? I'm going to look into Hareative more now and see what I can figure out. |
This reply got a bit long. Sorry.
Thank you for the explanation. I think I get it now 😄 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, 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.
Thank you. I'm glad you liked it 😄. You make some very good points.
I do try to give an explanation of it in the blog post. But, I guess it's not sufficient. 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
Yes! That is absolutely correct and very well spotted 👍. 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 "
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 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 😄
I will 😄. I've added the function to Hareactive. It's currently called 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 const moment = (f) => (t) => f((behavior) => behavior(t)); It can be used like this: const z = moment((at) => at(x) + at(y));
In the above example, I'd probably still use const baz = moment((at) => at(booleanBehavior) ? at(foo) : at(bar); In that example, the const totalSum = moment((at) => at(listOfNumbers).map(at).reduce((n, m) => n + m, 0);
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 hope you enjoy it 🎉 I took a quick look and it seems interesting and gave a good impression. When I get the time (I currently have exams 😢) I'll take a close look. I currently use Trello. |
const baz = moment((at) => at(booleanBehavior) ? at(foo) : at(bar);
Would it be different from the one below? const baz = lift((bool, f, b) => bool ? f : b, booleanBehavior, foo, bar) |
Yeah. I wasn't clear enough about that. The difference is mostly an implementation detail that can give better performance. The |
The sidebar lives as a global value inside a 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 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 (
Thanks dude! You tend to write some really interesting libraries so I'm always happy to learn from you! |
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.
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 😄
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.
Me too. It makes some patterns much nicer to code 👍
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.
|
* TOC {: toc } ### Last week Last week was productive in terms of publishing two podcast and recording a third. I also worked a bit freelance. I only need to do a dozen hours freelance at this point in conjunction with the money from the podcast sponsorship to make ends meet. Eventually with more sponsorship and Patreon, maybe I can slowly lower the number of hours freelance towards zero. ### Github Issues / Projects I also started my Dec Regroup Projects, including moving my todos to Github Issues / Github Projects. I now have three Github projects: * [To Do](https://github.com/stevekrouse/futureofcoding.org/projects/3) is my pipeline of tasks * [To Research](https://github.com/stevekrouse/futureofcoding.org/projects/2) is my pipeline of links and topics * [My podcast pipeline](https://github.com/stevekrouse/futureofcoding.org/projects/1) is where I organize my guests I'm a bit worried about how this new system will interact with this log. While it felt silly to copy and paste todo lists over and over in this log, it's also a bummer doing things in my new system doesn't show up in here. This log is supposed to be a log of all my work on this project, so I'd love to pull in Github issues activity somehow... Now that I say it "out loud" I wonder if I can do that automatically... For now though, I may just copy and paste some things from Github issues here when relevant. ### Jekyll refactoring I get really fustrated with Jekyll sometimes. My main issues were: * I was having trouble with importing page snipets * I wanted to be able to preview my site locally with all the CSS. (Github Pages had some setting I didn't have set.) After many annoying hours I was able to make both of these things work. My main next big issue is a navbar on all my pages of: * Home * About * Podcast * Log * Community * Contact * Fork Of course new styles and logo would be nice as well, and maybe a commenting system, but it's not top priority. And to be honest, starting the Patreon is bigger priority than all of these but I'm a bit scared so I keep procrastinating... ### Turbine 7GUIs!! After doing 3 hours of emails yesterday (I was sick towards the end of last week, so I took Thursday off and didn't do my emails Friday, sat, or Sun), I spent a few hours messing with Turbine, and then ~7 hours today in it. So fun! Despite being rough around the edges, it feels like there's really something wonderful here - I'm very impressed with the design. Polishing this is a million times better than having to start from scratch. To take it for a spin, I've been doing the 7GUIs tasks in Turbine, and whenever I get stuck or find a bug, I write it down, and sometimes open an issue. I'm tracking all the progress in [this Github issue](#96), which I will copy and paste here: #### Issue opened * [typescript issue on starter kit](funkia/turbine-starter#2) * [combining model and view](funkia/turbine#81) * [devtools](funkia/turbine#82) * [a single page documentation cheatsheet](funkia/turbine#84) * [Text input bug](funkia/turbine#86) * [versioned documentation and various styles of `output`](funkia/turbine#85) #### To open issue ##### Issues using `lift`: 1. Do I import it from `jabz` and do `lift(f, b1, b2)`? Or I use it as `b1.lift(f, b2)`? 2. It doesn't work in the view with error `Attempt to sample non-replaced placeholder` 3. Doesn't seem to work for 4 behaviors but the documentation says "You can also combine in this fashion any number of behaviors" ##### Constructors need to be documented across all projects * I need the constant behaviors, streams, `IO.of` to be easier to get at than going to the tests to find them. ##### filterApply bug `filterApply(Behavior.of(undefined).map(() => () => true), stream).log()` upon an event from `stream` errors: `predicate.at(...) is not a function`. If you replaced `undefined` with `1`, the error goes away. ##### * `time.log()` does nothing It should at least error if it's not going to show you anything, but I might prefer it to show me all the milliseconds. ##### when doesn't log correctly `when(Behavior.of(true)).log()` errors `Cannot read property 'Symbol(Symbol.iterator)' of undefined` but `yield sample(when(Behavior.of(true)).map(x => console.log(x)))` outputs an `OfFuture` value #### To look into more * What is the `moment` function (described [here](funkia/turbine#51 (comment)))? * Use [`loop`](https://github.com/funkia/turbine#loop) successfully without `this.source.addListener is not a function` error * What's the difference between `go` and `fgo`? Also, the placement of arguments here is confusing: ```javascript const tempView = ({ c, f }) => go(function*() { ``` * When you can do infix dot vs prefix * I want a dummy model so I can see the view while I mess with stuff and not get `The generator function never yielded a monad and no monad was specified.` without ```javascript const tempModel = fgo(function* ({ }) { yield Now.of(10) }); ``` * Need to call the function created with `performStream` or `performStreamLatest` (I don't get the difference): `performStreamLatest(book_.map(() => withEffects(() => alert('hi'))()))` * It's also quite annoying to keep the output and inputs in sync always. ### Priorities 12/12/18 - 12/21/18 * freelance 10 hours * Patreon 20 hours * Turbine 20 hours * finish 7GUIs * work on various issues ### Push to vacation or 2019 * publish Tudor and Vlad episodes * p4 stuff * website design and improvements
@ccorcos
This is an answer to paldepind/flyd#142 (comment).
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.
The text was updated successfully, but these errors were encountered: