paldepind / functional-frontend-architecture

A functional frontend framework.
MIT License
1.44k stars 87 forks source link

flimflam - yet another approach #23

Open jayrbolton opened 8 years ago

jayrbolton commented 8 years ago

I have spent some months updating and refining an approach to an FRP driven pattern that I'm calling "flimflam". Main points:

Examples:

jayrbolton commented 7 years ago

For anyone interested, I have simplified this concept further into a single state object that contains multiple flyd streams to control the UI. The snabbdom gets patched everytime any stream in the state is updated. Main homepage: http://flimflamjs.github.io/ Collection of core UI components: https://github.com/flimflamjs/ff-core

It is similar to the union-type/Elm/Redux concepts, but diverges significantly in its use of many Flyd streams. I have found it very fun to program in, as I personally enjoy abstracting everything with streams/Flyd. Many people may find the heavy use of streams to be too difficult or too weird.

dmitriz commented 7 years ago

@jayrbolton Interesting project! I wonder about the reasoning to prefer attaching actions directly to state, rather than passing as another argument:

// Render the state into markup
const view = function(state) {
  return h('body', [
    h('button', {on: {click: state.showModal$}}, 'Say Hello')
  , modal({show$: state.showModal$, body: h('p', 'Hello World!')})
  ])
}
jayrbolton commented 7 years ago

Hi @dmitriz, thanks for the feedback! The initial reasoning is for simplicity in implementation, and also being able to scope the event stream easily. Eg you could initialize multiple states, and they'd each have their own showModal$ event stream. Another example is if you have multiple counters, you could initialize each counter object separately, and each object would have a different increment$ stream, so you would never have to worry about those event streams conflicting.

However, I agree that it could also be cleaner to keep the actions separated. I've actually been experimenting with a modification of this architecture that would separate out the actions. The event streams ("actions") instead are pulled out of the virtual DOM, are received only as input into your state functions, and are initialized by a snabbdom module:

Here is a counter example:

// count is a plain-js object with static data
function view(count) {
   // no logic goes in the view, it is read only, with some declarative event stream bindings
  return h('div', [
    h('p', 'Total count is ' + count)
    // "streams" is a custom snabbdom module that generates events streams from the dom
  , h('button', {streams: {click: ['add, 1]}}, 'increment')
  , h('button', {streams: {click: ['add', -1]}}, 'decrement')
  , h('button', {streams: {click: ['add', -count.total]}}, 'reset')
  ])
}

// Our UI logic comes from functions that take input/event streams as arguments, and return a stream of state data
function counter(add$) {
  return flyd.scan((sum, n) => sum + n, 0, add$)
}

// this initialization would get called once per page. its main responsibility would be take your event streams from the dom and use them to initialize your UI logic. Similar to Cycle.js
function init (dom) {
  // dom is a function that fetches the event streams that we declared in the view
  const add$ = dom('add')
  return counter(add$)
}

render(init, view, document.body)

Let me know what you think! I have actually implemented all this in an experimental repo, and also did a multi-counter example.

dmitriz commented 7 years ago

@jayrbolton

Hi @dmitriz, thanks for the feedback!

You are welcome!

The initial reasoning is for simplicity in implementation, and also being able to scope the event stream easily. Eg you could initialize multiple states, and they'd each have their own showModal$ event stream. Another example is if you have multiple counters, you could initialize each counter object separately, and each object would have a different increment$ stream, so you would never have to worry about those event streams conflicting.

The way I see it, the initialization part is separate from my functions. That what e.g. in CycleJS is done by drivers and should ideally be small and moved away from most of my code, which is mostly pure functions.

When initializing, I can always create new streams or reuse the parents. In fact, I could do both with my very same pure component. But with both objects put together, I wouldn't have this freedom, would I?

However, I agree that it could also be cleaner to keep the actions separated. I've actually been experimenting with a modification of this architecture that would separate out the actions. The event streams ("actions") instead are pulled out of the virtual DOM, are received only as input into your state functions, and are initialized by a snabbdom module:

Here is a counter example:

  , h('button', {streams: {click: ['add, 1]}}, 'increment'),

That looks a bit "magical" to me, as I have no idea where that stream is and where the event goes.

Does it have any advantage over this:

function view(actions, count) { 
    ...
  , h('button', {onclick: () => actions(['add', 1])}, ...

This way I can test it as pure function of its arguments. One nice byproduct I find that it looks very similar to React-Redux style, so I can reuse their code. Perhaps you are doing something similar but using some magic API?

// Our UI logic comes from functions that take input/event streams as arguments, 
// and return a stream of state data
function counter(add$) {
  return flyd.scan((sum, n) => sum + n, 0, add$)
}
// this initialization would get called once per page. 
// its main responsibility would be take your event streams from the dom and use them to 
// initialize your UI logic. Similar to Cycle.js
function init (dom) {
  // dom is a function that fetches the event streams that we declared in the view
  const add$ = dom('add')
  return counter(add$)
}

render(init, view, document.body)

So the dom('add') feels again "magic" to me :) Is this part of the driver? If yes, should the driver be aware of the dom events?

Here is some description of the architecture I came up with, any feedback is welcome! https://github.com/cyclejs/cyclejs/issues/501#issuecomment-298511669

jayrbolton commented 7 years ago

This is the kind of feedback I wish I had gotten a year ago! I read through your post on Cycle.js and I think we share a lot of the same sentiments, especially around simplifying view functions, not querying on classnames, etc.

Regarding reducer functions: I myself am not a proponent of simplifying everything to reducer functions. I think you lose a lot of the power of streams when you do that. For example, I have had a lot of success using abstracted undo functionality that I can then combine with some abstracted ajax functionality that gives a lot of power in a small amount of declarative code, which is not possible with reducers. You can always have "reducer" style functions in a call to scanMerge for one of your streams.

I've now used the Flimflam style architecture in two large production apps and have had good success. My major lessons have been:

Regarding the "magic" actions: I agree that it looks too much like magic, and another developer I showed it to had a similar reaction. We could easily do the style you suggest, where you pass an actions function down through your view tree. My reasoning was simply that I wanted to get rid of the boilerplate of having to pass that argument everywhere. In my examples, the way the streams property works is that it is a snabbdom module. In this snabbdom module, streams get generated automatically by the h function and aggregated into one big dictionary of streams. That dictionary of streams gets passed into the init function by the render function. But I will most likely make it more explicit by using an actions argument.

Also, I want to present another idea we've developed that is helpful to manage a large one-page app. We tentatively call the pattern "models" (this is a loaded term, I know). These are functions that take input streams (actions), do some stream logic, and create a result object of streams. There is a model() function that takes an object containing many streams, and converts it into a single stream of flat objects. Here is an example:

// A model for creating and deleting foos via ajax
function manageFoos (newFoo$, deleteID$) {
  const postResp$ = flyd.flatMap(postRequest, flyd.map(setDefaults, newFoo$))
  const postError$ = flyd.map(formatError, flyd.filter(statusErr, deleteResp$))
  const postedFoo$ = flyd.map(resp => resp.body.foo, flyd.filter(statusOk, postResp$))

  const deleteResp$ = flyd.flatMap(delRequest, flyd.map(id => ({foo_id: id}), deleteID$))
  const deleteError$ = flyd.map(formatError, flyd.filter(statusErr, deleteResp$))
  const deletedFoo$ = flyd.map(resp => resp.body.foo, flyd.filter(statusOk, deleteResp$))

  const foos$ = flyd.scanMerge([
    [postedFoo$, appendFooToArray]
  , [deletedFoo$, deleteFooFromArray]
  ], [])

  const error$ = flyd.mergeAll([postError$, deleteError$])
  const loading$ = flyd.merge(
    flyd.map(R.always(true), flyd.merge(newFoo$, deleteID$))
  , flyd.map(R.always(false), flyd.merge(postResp$, deleteResp$))
  )

  // This converts the object containing mutiple streams into a single stream of plain, static objects
  return model({
    loading: loading$
  , error: error$
  , data: foos$
  })
}

const newFoo$ = flyd.stream()
const deleteID$ = flyd.stream()
const foos$ = manageFoos(newFoo$, deleteID$)
newFoo$({name: 'bob'})
foos$().loading // returns true
// ... wait for ajax to finish ...
foos$().loading // returns false
foos$().data // returns [{name: 'bob', id: 0}]

The user of the manageFoos function would call it once for the page, at the top level init function, and is free to use the foos object in any view they want. The interface for this function is easily documentable, and it is also easy to unit test.

Finally, I invited you to a repo called Uzu, where I am in the process of implementing these ideas. I would like Uzu to be the next generation of flimflam (and I think it is a better name, lol), and maybe you'd be interested in helping.

paldepind commented 7 years ago

This is an interesting discussion :smile:

I think you have some really good ideas and some impressive implementations, @jayrbolton. I looked into flimflam a while ago and really liked it.

I'm currently working on a new approach to functional frontend development myself that has important similarities to flimflam.

Regarding reducer functions: I myself am not a proponent of simplifying everything to reducer functions. I think you lose a lot of the power of streams when you do that.

I strongly agree with this. Not only do you loose the power of streams. You also loose clear definitions of your data. In flimflam I can look at a value and see it's definition which will tell me everything about what influences the value. For instance, in the example on the flimflam webpage, if I look at the line

const celsius$ =  ...`

I know that the right-hand side of the equals sign will tell me everything about what celsius$ is. With reducer functions (as they're used in this repository and in redux) you loose that since an unlimited number of actions can affect a value in the state/store.

However, in flimflam the way events are handled breaks the principle of having "definitions". When I look at this line:

const keyupCelsius$ = flyd.stream()

I don't know what will affect keyupCelsius$. I just know that somewhere in the view someone will push into that stream. Since the pushing is impure the dependencies aren't included in the definition of keyupCelsius$. I think that is a problem and it is one of the things we're fixing in the new framework I'm working on.

If you're curious the framework is called Turbine. A while ago I wrote an implementation of the fahrenheit to celcius converter from the flimflam frontpage with Turbine. That example can be found here. We also have a bunch of other examples and some documentation.

Here are a few key ideas in the approach we're taking:

I hadn't seen Uzu before. I'll be sure to take a look at it :smile:

jayrbolton commented 7 years ago

Hi @paldepind, it's nice to know you've looked at and considered flimflam before. Uzu is just an iteration of flimflm -- I'm going to change the name and migrate everything, but get rid of the event stream initialization that you and @dmitriz cited.

Of course I always check on your new projects as you work on them and have taken a look at the funkia repos in the past. I'd enjoying trying a project in Turbine, and I would love to join the project as a contributor rather than work on a separate library. However, I have a few major hangups:

dmitriz commented 7 years ago

This is the kind of feedback I wish I had gotten a year ago! I read through your post on Cycle.js and I think we share a lot of the same sentiments, especially around simplifying view functions, not querying on classnames, etc.

Glad to hear!

Regarding reducer functions: I myself am not a proponent of simplifying everything to reducer functions. I think you lose a lot of the power of streams when you do that.

Reducers are great and simple if you can reduce your code to them, pun intended ;) Also they make your code accessible to the very large Redux community. But of course, you should use the more powerful stream versions if you need. But only if :) Why might be in 5% of the cases. That was what I meant saying "use the real thing only when needed".

For example, I have had a lot of success using abstracted undo functionality that I can then combine with some abstracted ajax functionality that gives a lot of power in a small amount of declarative code, which is not possible with reducers.

I'd be curious to see the examples.

You can always have "reducer" style functions in a call to scanMerge for one of your streams.

That would be the reducer way. :)

I've now used the Flimflam style architecture in two large production apps and have had good success. My major lessons have been:

Strongly separate your UI logic from your presentation layer. That includes decoupling your "state tree" of nested data/streams with your "DOM tree" of nested markup. The state tree and the virtual-DOM tree should not try to match up. When you create some UI logic, it should be totally divorced from any markup. This goes against the "component" style of doing things where you might have the UI logic, the markup, and even the style for a Modal all wrapped up in one package. I think this will bite you in very large apps -- instead, totally decouple the UI logic from the markup for the modal.

Good point, totally agree. The view should be aware of the state but not the other way. But a truly reusable component should hold both the state and the view. With former unaware of latter. Would you agree?

Keep view functions simple, declarative, and read-only Streams have a bit of a learning curve, but I think they are the best way to manage logic for highly asynchronous UI.

Streams are more powerful but you can't avoid the cost of extra complexity.

Imagine a UI where you make three ajax requests for every click, with two of the requests happening asynchronously, and the third one starting when the first of the two have both finished. Also, we want to show different notification messages for each ajax response that each automatically hide after 5 seconds. With most frameworks, this is a nightmare. But with streams, it is almost trivial!

Any good examples?

Don't use JSX, because it is better to know that h() is a function that takes a string, an object, and an array. JSX obscures the semantics and syntax of javascript.

LOL, love that :) The JSX is perhaps the worst step backwards (from JSON to XML) in the recent JS history. https://github.com/jsforum/jsforum/issues/1

Also, use postcss and commons.css (similar to basscss) for styles and keep it decoupled from the JS.

I actually prefer pure JS with no CSS. One language is easier to manage than two.

Regarding the "magic" actions: I agree that it looks too much like magic, and another developer I showed it to had a similar reaction. We could easily do the style you suggest, where you pass an actions function down through your view tree. My reasoning was simply that I wanted to get rid of the boilerplate of having to pass that argument everywhere. In my examples, the way the streams property works is that it is a snabbdom module. In this snabbdom module, streams get generated automatically by the h function and aggregated into one big dictionary of streams. That dictionary of streams gets passed into the init function by the render function. But I will most likely make it more explicit by using an actions argument.

You can always pass it in a single object. Like {state, action}. I just feel it is nice to have simple reference to the pure state.

Also, I want to present another idea we've developed that is helpful to manage a large one-page app. We tentatively call the pattern "models" (this is a loaded term, I know). These are functions that take input streams (actions), do some stream logic, and create a result object of streams. There is a model() function that takes an object containing many streams, and converts it into a single stream of flat objects. Here is an example:

const postResp$ = flyd.flatMap(postRequest, flyd.map(setDefaults, newFoo$))

I am a bit lost here - was the postRequest and other vars defined?

dmitriz commented 7 years ago

@paldepind

Hi Simon, many thanks for taking your time to comment, greatly appreciated!

Amusingly, I had a related discussion with @JAForbes on Mithril's Gitter, where I was suggesting exactly along the lines of Turbine :) Namely, that the view function should return the event streams, rather than accept them as callback style functional arguments.

Which is of course, mathematically dual. Like functions are dual to values. And values are primary, whereas callback functions create indirections, adding the function invocation details. But then James convinced me that emitting stream would create more complexity in practice, whereas functions are simpler objects by their nature, because they lack the time component. So you pay the price of the function callback indirection but win on the complexity side but getting rid of the time. Granted, it will not suffice in all cases, but in many it will.

So he had convinced me of the simplicity of the strategy I tried to outline in that Cycle issue. If I were to follow it, I would come up with this kind of implementation of the same example:

// everything here are pure values, no streams
const model = ({farenAction, celciusAction}) => ({faren, celcius}) => ({
   celsius: farenAction ? farenToCel(faren) : celcius,
   faren: celciusAction ? celcToFaren(celcius) : faren
})

// pure function accepting pair of functions and pair of values and returning vnode,
// again no streams
const view = ({farenAction, celciusAction}) => ({faren, celcius}) => [
    div([
        label("Farenheit"), 
        input({value: faren, oninput: e => farenAction(getValue(e))})
    ]),
    div([
        label("Celcius"), 
        input({value: celcius, oninput: e => celciusAction(getValue(e))})
    ])
]

It is surely a very naive implementation and possibly problems will arise when trying to scale it, and I would like to understand those problems better. Perhaps this example is just too simple?

I would also like to understand better the problem with the stream mutation. The way I see it, the functions themselves are pure and testable. The actual mutation is separated away into the drivers (like in the Cycle). But again, I might be missing some point here.


Funny enough, we also had a long discussion with James about behaviour vs stream in https://github.com/MithrilJS/mithril.js/issues/1822 and on Gitter. In fact, he wanted to see streams as continuous functions of the time but decoupling away the time, which is kind of resembles what you call a behaviour, where my point to view streams as discreet sequences of values seems to fit with yours.

So I wonder about your opinion on the subject of the scan implementation raised in https://github.com/MithrilJS/mithril.js/issues/1822#issuecomment-298231446

paldepind commented 7 years ago

Of course I always check on your new projects as you work on them and have taken a look at the funkia repos in the past. I'd enjoying trying a project in Turbine, and I would love to join the project as a contributor rather than work on a separate library. However, I have a few major hangups:

That's really great to hear. Then I'll do my best to address your points :wink: Maybe we can work them out :smile:

With the functional abstractions for jabz and hareactive, I understand where you're coming from, but I think they are too esoteric for most web developers, and I would feel very uncomfortable trying to introduce such things into a typical javascript development group. I think if you are getting that far away from typical javascript, why not transpile haskell? I would rather have a better balance of accessibility with FRP, even if i sacrifice purity.

I think this is a very valid criticism. For a typical JavaScript developer, there will be a bit of a learning curve. Accessibility is important to me. But, I also think some of the approaches we're taking has clear benefits when using the framework and I wouldn't want to sacrifice that. I definitely took inspiration from Haskell. But only because I though it would make for a better framework for JavaScript.

In my opinion some of the things that may make Turbine harder to learn initially actually makes it easier to use once learned. Sometimes simple tools can only solve complex problems in complex ways. But sometimes complex tools can turn complex problems into simple problems. I think FRP itself is a great example of that. It does have an initial learning curve and some added complexity. But when that is mastered problems that previously where hard are now simple.

Are there any things in particular you think are too esoteric? We are definitely not trying to make things more complex than strictly necessary. Maybe having Jabz as a peer dependency is a mistake. One doesn't have to understand nor use Jabz to use Turbine. We could just reexport the few functions from Jabz that Turbine users need.

My hope is that with good well thought out documentation we can make things understandable and approachable even for normal JavaScript developers. But, my vision is also that Turbine will push JavaScript developers in a more functional direction. We could have coded it all Haskell and transpiled it. But then we wouldn't be giving JavaScript developers a functional framework.

I find typescript to be extremely heavy-handed and would prefer a plain-JS (and even es5-compatible) approach. (I have actually been experimenting with a comment based type inferencer here that you might find interesting: http://github.com/jayrbolton/tipo/).

In what way do you find TypeScript to be extremely heavy-handed? Writing types definitely has an overhead but it can also be very helpful.

TypeScript has a new feature that seems a bit similar to tipo. It can now do type checking in plain JavaScript files (see here). No annotations are necessary. It uses inference to the extent possible and TypeScript definition file if they are available (which they are for many libraries).

By writing Turbine in TypeScript we get the benefit that we can support both JavaScript and TypeScript. Our users can decide for themselves what they prefer. And even if they use JavaScript they can benefit from our types. Either by using TypeScript to check JavaScript files or from the fact that some editor will use TypeScript types from libraries to offer suggestions even in plain JavaScript files.

I'm not crazy about introducing generators as the core mechanism for generating markup. It's very interesting the way you are using them, but I quite find the virtual-dom style very nice

I fully agree. We did realise that along the way and thought hard about the problem. To solve it we have created an alternative way of expressing views that don't use generators and that looks pretty much like virtual-dom. Here is the tempeature converter rewritten using that style.

function model({ fahrenInput, celsiusInput }) {
  const fahrenChange = fahrenInput.map((ev) => ev.currentTarget.value);
  const celsiusChange = celsiusInput.map((ev) => ev.currentTarget.value);
  const fahrenToCelsius =
    fahrenChange.map(parseFloat).filter((n) => !isNaN(n)).map((f) => (f - 32) / 1.8);
  const celsiusToFahren =
    celsiusChange.map(parseFloat).filter((n) => !isNaN(n)).map((c) => c * 9 / 5 + 32);
  const celsius = stepper(0, combine(celsiusChange, fahrenToCelsius));
  const fahren = stepper(0, combine(fahrenChange, celsiusToFahren));
  return Now.of([{ fahren, celsius }, []]);
}

const view = ({ fahren, celsius }) => div([
  div([
    label("Fahrenheit"),
    input({ props: { value: fahren }, output: { fahrenInput: "input" } })
  ]),
  div([
    label("Celsius"),
    input({ props: { value: celsius }, output: { celsiusInput: "input" } })
  ])
]);

const component = modelView(model, view);

As you can see the model is nicely seperated from the view. And the view looks pretty much like virtual-dom. However, this still does not depend on any impure pushing into streams. The view function returns a component. The component contains both the DOM and the output streams from the DOM. So instead of pushing into streams the view creates and returns streams. This solves the problem I mentioned earlier. As far as I understand, in Uzu any number of locations in the view can trigger a specific action. So there is no single definition that tells me everything about the input streams.

I would prefer not to tightly couple the streams with the markup.

Again, I agree. When using the modelView function as I did in the example above streams are not tightly coupled to the markup. Instead, the view is a function takes a plain-JS objet and creates a view. But the object contains streams instead of plain values.

For example, imagine having two sections of a page where on section A, we create and list foos, while on section B we update the foos, and also create bars, and also list both of them. Both sections need to share the same listing of up-to-date foos. I think in Turbine or Cycle, you'd have to have a complicated tree of streams deriving from forms and buttons that yield ajax responses, that then yield different markup tables of the same data. I would argue that this would be much easier to create and manage using a single tree of unbroken markup that simply reads from a plain-JS object.

I definitely think that what you're describing could be implemented in Turbine with a single tree of unbroken markup. If you describe the the example in a bit more detail I'll take a stab at implementing it in Turbine to see how it works out.

paldepind commented 7 years ago

@dmitriz

Hi Simon, many thanks for taking your time to comment, greatly appreciated!

You too. I appreciate it as well. Also, I apologise for not having replied to the issue you recently opened in the Snabbdom repo.

Amusingly, I had a related discussion with @JAForbes on Mithril's Gitter, where I was suggesting exactly along the lines of Turbine :) Namely, that the view function should return the event streams, rather than accept them as callback style functional arguments.

That sounds interesting. Could you maybe send a direct link to one of the messages?

Which is of course, mathematically dual. Like functions are dual to values. And values are primary, whereas callback functions create indirections, adding the function invocation details. But then James convinced me that emitting stream would create more complexity in practice, whereas functions are simpler objects by their nature because they lack the time component. So you pay the price of the function callback indirection but win on the complexity side but getting rid of the time. Granted, it will not suffice in all cases, but in many it will.

I don't really understand this?

So he had convinced me of the simplicity of the strategy I tried to outline in that Cycle issue. If I were to follow it, I would come up with this kind of implementation of the same example:

As far as I can tell farenAction and celciusAction are impure functions in that example. One benefit of having the view create and return streams is that we get completely rid of the impurity.

Funny enough, we also had a long discussion with James about behaviour vs stream in MithrilJS/mithril.js#1822 and on Gitter. In fact, he wanted to see streams as continuous functions of the time but decoupling away the time, which is kind of resembles what you call a behaviour, where my point to view streams as discreet sequences of values seems to fit with yours.

That's an interesting discussion. Both you and @JAForbes has good points. In my opinion, you are both right and you are both wrong.

Having a discrete sequence of time-ordered events is very useful. But, having a continuous function over time is also useful. None of them is more powerful than the other. None of them can replace the other. So I don't agree with James when he writes this

I think we shouldn't think of discrete points in time but a curve, a function, a formula, an infinite continuous curve described by a function. Why? Because it's more powerful to model our programs using an abstraction that is unaware of discrete points in time.

It is not "more powerful" to use an abstraction that is unaware of discrete points in time. How would you use such an abstraction to model mouse clicks? Mouse clicks are events that happen at discrete points in time. The world contains both things that are continuous (like time) but it also contains things that are discrete (like mouse clicks).

Arguing about which of these representations are best is like arguing about whether functions are better than lists. Some things are best expressed as functions and other things are best expressed as lists. Separate things should have separate representations. Mixing them together only creates problems.

So my key point is this: Only having a single thing called "stream" or "observable" is too limiting. What is needed it to have both a structure that represents discrete events and a structure that represents continuous things that always has a value. That is exactly what classic FRP did and it is what I'm doing in the FRP library Hareactive. We have streams which are discrete and we have behaviours which are continuous.

If you're interested I have written a bit more about it in the Hareactive readme. I have also written a (currently unpublished) blog post about classic FRP. In the blog post I try to explain the ideas in FRP, the semantics of streams and behavior and why these are fundamentally different things.

paldepind commented 7 years ago

@dmitriz

Let me just point out that if we use an abstraction that is unaware of discrete points in time a function like scan doesn't make any sense. One cannot define scan on a function from time. Thus, with only that representation, one can't have scan at all. So when discussing scan one has to use a discrete representation.

dmitriz commented 7 years ago

@paldepind

Hi Simon, many thanks for taking your time to comment, greatly appreciated!

You too. I appreciate it as well. Also, I apologise for not having replied to the issue you recently opened in the Snabbdom repo.

Thanks, and no worries, whenever you find time.

Amusingly, I had a related discussion with @JAForbes on Mithril's Gitter, where I was suggesting exactly along the lines of Turbine :) Namely, that the view function should return the event streams, rather than accept them as callback style functional arguments.

That sounds interesting. Could you maybe send a direct link to one of the messages?

Here is about the place (begins from 6am I think): https://gitter.im/lhorie/mithril.js?at=5902d280cfec91927281a42c

Which is of course, mathematically dual. Like functions are dual to values. And values are primary, whereas callback functions create indirections, adding the function invocation details. But then James convinced me that emitting stream would create more complexity in practice, whereas functions are simpler objects by their nature because they lack the time component. So you pay the price of the function callback indirection but win on the complexity side but getting rid of the time. Granted, it will not suffice in all cases, but in many it will.

I don't really understand this?

I am sorry for being cryptic. I was referring to the maths duality between values and functions - the pairing (the so-called K-combinator):

const T = (x, f) => f(x)

Now you can recover the value x by knowing the functional f -> f(x) for all functions f, but you would get only special functionals, i.e. not every functional f -> F(f) arises that way. Which means, passing a callback (the f in this case) will have perhaps too general signature for your use case.

Saying it differently, if you accept callback f as parameter and write your function as

const fn = (anything, f) => ...

and then you want to transform it with some HOF like HOF(fn), the question arises for which fn that transformation HOF should be defined. Here it looks like it should be defined for all fn, however, only special fn ever arise in your use cases, so you are losing some flexibility.

Don't know how much useful is that difference though in practice... but it gives some merit to directly emitting streams vs callbacks by nailing down the essence of it on the conceptual level.


I understand that your reasoning is from another angle that the stream should be owned by the view and not come from outside.

But I guess from James and many other JS dev perspective, functions are easier than streams, so they would only use streams if they can't get away with functions :)

So he had convinced me of the simplicity of the strategy I tried to outline in that Cycle issue. If I were to follow it, I would come up with this kind of implementation of the same example:

As far as I can tell farenAction and celciusAction are impure functions in that example. One benefit of having the view create and return streams is that we get completely rid of the impurity.

The way I see it, they both enter as parameters, just like in the K-combinator. Which makes both the reducer and the view pure. Just like the K-combinator is pure in both x and f. Even if you can always pass an impurely implemented f! But that is a detail hidden inside f. Whereas we are looking at f as formal parameter. This is what makes the reducer pure, even when at the runtime you can pass a stream. But only inside your driver. I find it quite fascinating. I hope this makes sense.

That's an interesting discussion. Both you and @JAForbes has good points. In my opinion, you are both right and you are both wrong.

πŸ˜„

I had look at Hareactive some days ago with great interest. It is a bit dense e.g. explaining the behaviour. The input text can actually be modelled by the stream (you would lose the typing events with behaviour) and the mouse movement can be, in principle, regarded as discrete movement across the screen cells by a technical person. The behaviour model has some simplicity advantages but some implementation oriented folks may be hard to convince.

There are few things a bit puzzling there, I can write an issue or two.

But your blog post really does great job at explaining it. Would you allow to share it?

Especially when you define the scan from streams into behaviour. That addresses exactly the problem in our discussion with James, very helpful, thanks!

So you regard the state as behaviour, not stream, that is very interesting, and perhaps is missing in places like Cycle, making it more complex and less intuitive.

dmitriz commented 7 years ago

Just wanted to comment on this:

const view = ({ fahren, celsius }) => div([
  div([
    label("Fahrenheit"),
    input({ props: { value: fahren }, output: { fahrenInput: "input" } })
  ]),
  div([
    label("Celsius"),
    input({ props: { value: celsius }, output: { celsiusInput: "input" } })
  ])
]);

That might hit some confusion/resistance as the function seems to return something you don't see in the return statement. You only see the vnode.

It has some common feature with what I was trying to propose to James, but I was thinking of exporting events instead:

const view = ({ fahren, celsius }) => {
  const fahrenStream = flyd.stream()
  const celciusStream = flyd.stream()
  const vnode = div([
    div([
      label("Fahrenheit"),
      input({ props: { value: fahren }, oninput:  fahrenIStream("input") })
    ]),
    div([
      label("Celsius"),
      input({ props: { value: celsius }, oninput: celsiusInput("input") })
    ])
  ]);
  // and now we return everything explicitly the Cycle way
  return {vnode, farhrenStream, celciusStream}
}

However, it is less general than updating external stream with the same values, when you simply pass an empty stream as argument. But the latter is more powerful, because you can reuse stream, passing them to several views, which will save you some heavy stream combining.

Another advantage of updating stream passed as argument, is that it looks semantically the same as passing a callback function, a pattern that many people should like because of the familiarity.

Of course, it plays out the trick I mentioned above, that the view can be treated as pure, even if streams are mutated at the run time.

JAForbes commented 7 years ago

Usually, in UI programming time is absolutely irrelevant. Not always, but usually. Usually we want a ratio, or the latest computed value. But we're not interested at all in the particular events, we're interested in a result based on a relationship we defined via a pure function.

I'm not saying we should never have a discrete model, I'm saying that we emphasise discrete far too much.

Clicking is a great example of a discrete model, there's no way around it. But its also not the common case at all. A common case is "the borders colour is green if this text is a valid email". Yeah they're are individual input events. But we aren't modelling our application using events, we just need our equation to hold for all text values.

For movement events, we only emit discrete mouse events because we have no way to model a continuous curve of the mouse's position. But that, again, is just an implementation detail. And if I want to create a relationship between the mouse's x / window's width and use that to decide how much red there should be in a colour, I shouldn't be thinking in terms of discrete events. I can just think of an equation.

But coming back to discrete events. Let's think of a game like space invaders, whenever I press a mouse button, I shoot a bullet, sounds discrete. But if we have a stream of actions, where the action may or may not be a click, we have a continuous model of whether we are shooting.

const shooting = actions.map( a => a.name == 'ButtonDown')

The source of that action is a mouse click, but its at the edge of the system, we don't need to think about it after this point:

{
  onclick: () => actions({ name: 'ButtonDown' })
}

Its so often, even in the case of mouse clicks, and button presses, the most unimportant detail in the system, we get to model our application and remove the entire dimension of time, that's very powerful.

In the rare case we want to think of time, we can, that's okay, but I wish we'd emphasise it far less, because the value of abstracting over time is that we don't need to think about it. And yet it seems all FRP proponents ever talk about it is time.

Some things can only be modelled using discrete events (like visualisations of discrete events) and in those cases we shouldn't hide from that, but "arrays over time" and "promises but with multiple values" and marble diagrams, observe-ables are common examples of discrete evangelism, and they are often encouraging an imperative, procedural conceptual model: being notified then modifying something. That's a shame in my opinion.

Also comparing discrete to continuous generally may seem bizarre, but in the context of solving problems with this data type in user interfaces; it isn't at all. Discrete isn't necessarily imperative, but modelling things as responses to discrete events is. Modelling thing as relationships is always declarative. Hopefully we can agree declarative code is usually something to strive for.

A great example of the success, simplicity and flexibility of continuous only is Excel. Somehow the business sector is fine modelling in a reactive way with no concept of time.

So I think we should aim for continuous, encourage continuous, push discrete to the edges of our system, just like side effects. It leads to a model that is easier to reason about because its missing an entire dimension we'd usually have to consider.

dmitriz commented 7 years ago

Thanks @JAForbes for chiming in, just wanted to recommend @paldepind unpublished blog post, where many questions from our discussion have been answered, in particular, thescan is defined to return behaviors not streams: http://vindum.io/blog/lets-reinvent-frp/

paldepind commented 7 years ago

@dmitriz

I understand that your reasoning is from another angle that the stream should be owned by the view and not come from outside.

Yes. Exactly. That way we get output from the view and a pure way. Without using selectors, without pushing into streams and without going through some dispatcher.

The input text can actually be modelled by the stream (you would lose the typing events with behaviour) and the mouse movement can be, in principle, regarded as discrete movement across the screen cells by a technical person.

Yes. You could do both of these things. But you would loose precision in what you're talking about. Conceptually the movement of the mouse is continuous. So if you represent it with a continuous structure in code then the code is intuitive. If instead, you represent mouse movements with a discrete approximation then the code is no longer talking about the actual thing. It's just talking about an approximation.

I think representing things that are conceptually different with different structures is very beneficial. In the beginning, I thought that having both behavior and stream was more complex. But now I actually think it makes things more simple. An analogy could be strings and numbers. In theory, one could represent all numbers as strings. And one might even say that getting rid of numbers as a primitive is simpler. But of course, it just makes a bunch of other things much more complex.

There are few things a bit puzzling there, I can write an issue or two.

That would be wonderful :smile:

Just wanted to comment on this:

const view = ({ fahren, celsius }) => div([
 div([
   label("Fahrenheit"),
   input({ props: { value: fahren }, output: { fahrenInput: "input" } })
 ]),
 div([
   label("Celsius"),
   input({ props: { value: celsius }, output: { celsiusInput: "input" } })
 ])
]);

That might hit some confusion/resistance as the function seems to return something you don't see in the return statement. You only see the vnode.

I may not seem like it at first. But it actually is a part of the return statement. What these element functions create are not vnodes. They create components. And components contain some DOM along with some output streams/behaviors. Furthermore each component passes along the output from it's children. So the output from the component returned by input "bubbles up" and becomes part of the output from the top-most invocation to div. Let me know if that explanation does not make sense.

However, it is less general than updating external stream with the same values, when you simply pass an empty stream as argument. But the latter is more powerful, because you can reuse stream, passing them to several views, which will save you some heavy stream combining.

Of course, it plays out the trick I mentioned above, that the view can be treated as pure, even if streams are mutated at the run time.

I can see that the view is pure from the outside. But inside it isn't which is think is a downside. I don't think merging streams is a problem. I actually think it is an advantage because merging makes it explicit that the output stream is made up of several other streams.

JAForbes commented 7 years ago

Thanks @dmitriz πŸ‘

paldepind commented 7 years ago

@JAForbes

Thank you for commenting. It's really great to have your input as well. I agree with most of what you said.

Usually, in UI programming time is absolutely irrelevant. Not always, but usually. Usually we want a ratio, or the latest computed value. But we're not interested at all in the particular events, we're interested in a result based on a relationship we defined via a pure function.

If I understand you correctly you are saying that we rarely want to talk explicitly about time in our programs? I think that is true. Butb I still think time is relevant at an implicit level. Usually what the user sees at the screen is different from what he saw a minute ago. So time does play a part implicitly even though a program doesn't explicitly mention it.

I'm not saying we should never have a discrete model, I'm saying that we emphasise discrete far too much.

Ok. Thank you for clarifying. I completely agree with you on that :+1:

In the rare case we want to think of time, we can, that's okay, but I wish we'd emphasise it far less, because the value of abstracting over time is that we don't need to think about it. And yet it seems all FRP proponents ever talk about it is time.

Or maybe one could say that FRP talks about time so that others don't have to?

So I think we should aim for continuous, encourage continuous, push discrete to the edges of our system, just like side effects. It leads to a model that is easier to reason about because its missing an entire dimension we'd usually have to consider.

I don't fully agree with that. I agree that the source of discrete phenomenon is often input from the user. Mouse clicks and keypresses. These happen at the edges. And you can often turn those into continuous things and start modelling relationships. But, discrete things can also come from internal things.

To continue you asteroids example. Let's say I have the position of my spaceship and the position of an asteroid. Both of these are continuous. But, I want to know when my spaceship collides with an asteroid. That is suddenly a discrete list of collision events. And it's happening somewhere inside my code. I can't push it to the edges.

So while you can go from discrete things (streams) to continuous things (behaviours) you can also go the other way around. In FRP-like libraries that only have a single stream/observable the difference between when something is continuous and when something is discrete is very implicit. I don't think we need to push all discrete things to the edges. Discrete isn't bad. I just think we need to be explicit about what is continuous and what is discrete.

Edit:

Discrete isn't necessarily imperative, but modelling things as responses to discrete events is. Modelling thing as relationships is always declarative. Hopefully we can agree declarative code is usually something to strive for.

I don't understand this? Why is modelling things as responses to discrete events imperative? In your example you turn a discrete event stream into a continous shooting behavior. Isn't that turning something discrete into a continuous thing in a declarative way?

JAForbes commented 7 years ago

Thank you for commenting. It's really great to have your input as well.

Absolute pleasure, I'm a long time fan of your work as you probably know, so its always good to have a yarn πŸ˜„

But I still think time is relevant at an implicit level.

Agreed

Discrete isn't bad. I just think we need to be explicit about what is continuous and what is discrete.

I like this too.

To continue you asteroids example. Let's say I have the position of my spaceship and the position of an asteroid. Both of these are continuous. But, I want to know when my spaceship collides with an asteroid. That is suddenly a discrete list of collision events. And it's happening somewhere inside my code. I can't push it to the edges.

Well at this point we're really debating the merits of modelling techniques ⛳️ so all I can say is, in my implementation I would model collisions without respect to time, but instead as a function of dimensions and position. Then I would store all the collisions, and in the next frame process them, we end up with the familiar function state -> action -> state, and we are not relying on time at all, just relationships.

But I can imagine a lot of things in a game where discrete is unavoidable, and when that occurs I don't think we should avoid discrete. I think one example could be animations, trying to abstract away time from animation code is probably severely misguided, but then again I probably wouldn't use streams to model it, I'd use a promise/task/future.

The source of my frustration is that continuous is simpler and is often the right answer to a modelling question, but many don't know its an option because most resources emphasise time, but do not emphasise relationships. So often I field questions like "I want this to happen when that happens" and when we get to the point where its "this is f(that)" they seem to always respond "that's so much easier"

e.g. https://gitter.im/lhorie/mithril.js?at=58bb5fd2e961e53c7f9debd8

You can tell by the names of the functions in this chat, the streams are called computeX or computeY, and instead it should just be X and Y. After explaining this:

This is great, it gets me unstuck

And

Anyway, the big part was solved removing those computations from views

This sort of thing comes up a lot, where someone is thinking "when this event flows through the stream I'll do this procedural thing and return it", but they immediately get confused when they have to do something as trivial as merge 2 streams, and these are very smart people.

So I often rant about "is vs when". And now I see a lot of people seem to be enjoying streams a lot more, its now a part of mithril 1.0, its seen as a staple for solving common UI problems like cross component communication, where previously it was seen as impractical or convoluted.

And I'm not taking credit for that but I am definitely observing a trend of people getting tripped up on discrete and imperative, and the moment continuous and declarative clicks, they are fine.

JAForbes commented 7 years ago

I don't understand this? Why is modelling things as responses to discrete events imperative? In your example you turn a discrete event stream into a continous shooting behavior. Isn't that turning something discrete into a continuous thing in a declarative way?

Discrete isn't necessarily imperative. But all procedural code is discrete. Procedural code is a series of discrete steps. Code that isn't discrete cannot be procedural.

When we step into the discrete model, its natural to continue thinking in terms of procedure. It's also how web programmers have been working for decades "when this mouse clicks on that button I'm going to modify this variable" or "when this button is pressed I'm going to push onto this list"

But if we model as relations, notions of modification or mutation make no sense at all. I guess it's a discussion of affordances.

jayrbolton commented 7 years ago

Regarding continous vs discrete: Could you all give a real-world application example where a discrete-focused system (like flyd) will cause you significant engineering trouble, whereas if you switched to a system like Hareactive, your problems would be mitigated? I have personally never experienced such a difficulty with flyd.

I understand the conceptual distinctions you all are putting forth around continuous vs discrete, and I understand that continous streams have the potential to be more declarative. But I'd also like to relate these ideas back to how it actually affects real-world application building in a practical way using a concrete example.

Regarding returning actions: Thanks @paldepind for the alternate definition of the celsius-fahrenheit converter with a stronger view-model separation. I think I am now convinced that returning the DOM + the streams in your div or h() function might be the cleanest way.

I actually have some paid time right now to devote to getting one of these types of libraries off the ground, including making a Bootstrap-style prototyping library, with all base UI components and styles. I'd also be interested in creating components like FRP ajax or websockets in hareactive. I will post in the Turbine repo to see if I can join you there, but otherwise I'll be working on all that with Uzu.

JAForbes commented 7 years ago

I have personally never experienced such a difficulty with flyd.

Just making sure we're talking about the same thing πŸ˜„

What I was discussing wasn't specific to the implementation details of any library (e.g. I use flyd everywhere!), instead its how we model using a particular library. And how a lot of blogs and courses, and talks etc focus on a sequence of events as opposed to relationships.

And this is all in response to talking with devs working on real world apps. There seems to be a pattern of over thinking streams, and over emphasising time, often when its of no modelling benefit.

I think of it like Monads vs Functors, pick the least powerful/complex tool by default. Continuous is simpler, yet in the context of "Observables" I notice people choose discrete by default, maybe because its more natural for them, but I think a large part of the problem is the way people who understand streams, frame it

jayrbolton commented 7 years ago

That makes sense. I think I specifically had in mind what Simon said about behaviors and streams: it is analogous to not separating strings and ints, or perhaps ints and floats (which we are all too familiar with). I can think of many real world examples why we want to separate ints and floats -- eg currency calculation. I was wondering if there were similar specific examples for streams/behaviors.

dmitriz commented 7 years ago

@paldepind

Don't use virtual DOM. Instead, the created view listens directly to streams and updates the DOM accordingly. This avoids the overhead of virtual DOM and allows for a very natural style where streams are inserted directly into the view.

Could you explain this? I've thought the point of virtual DOM was to avoid the overhead of the real one :)

Yes. Exactly. That way we get output from the view and a pure way. Without using selectors, without pushing into streams and without going through some dispatcher.

Using a dispatch can be great for adoption though, due to its familiarity to anyone using Redux, which is a massive community. Even with its downsides it might be worth it. A code pattern using dispatch would be instantly readable for them.

Yes. You could do both of these things. But you would loose precision in what you're talking about. Conceptually the movement of the mouse is continuous

I can see where you come from, both models may work for the text input, but in case of the input text, the discrete stream model is perhaps all that you need?

There are few things a bit puzzling there, I can write an issue or two.

That would be wonderful πŸ˜„

Started, see here: https://github.com/funkia/hareactive/issues/18#issuecomment-299453989

I may not seem like it at first. But it actually is a part of the return statement. What these element functions create are not vnodes. They create components. And components contain some DOM along with some output streams/behaviors. Furthermore each component passes along the output from it's children. So the output from the component returned by input "bubbles up" and becomes part of the output from the top-most invocation to div. Let me know if that explanation does not make sense.

I think I can see the problem. We really want to return the event streams from the same function. But on the outside we still want the return value to be a Node, or more precisely, to conform to the Node interface. It is conceptually the same as multiple exports from a module with one being default. Except that we want the same from a function instead of module ;)

So I can see now better your idea of using the go(generator) syntax. It would be really neat just to write

const glorifiedInput = go(function*() {

    // return vNode and export event stream in one line!
    return input({ type: 'text', oninput: e => yield e.target.value })

    // or even shorten to 
    return input({ type: 'text', inputVal: yield })
});

and consume it like

const mainView = go(function*() {
    return div([

        // inline the Node and get its value in one line!
        const name = yield glorifiedInput(),

        // name is available, so we can just use it!
        div(`Hello, ${name}`)
    ])
})

What do you think?

dmitriz commented 7 years ago

@paldepind

Of course, it plays out the trick I mentioned above, that the view can be treated as pure, even if streams are mutated at the run time.

I can see that the view is pure from the outside. But inside it isn't which is think is a downside. I don't think merging streams is a problem.

Can you explain how is it not pure inside? What about the T-combinator? Or the plain function composition:

const compose = (f, g) => x => f(g(x))

I actually think it is an advantage because merging makes it explicit that the output stream is made up of several other streams.

It would be nice to limit the stream interface use as much as possible. As there is no JS standard for streams, unfortunately, any code using streams must rely on a custom library. And by limiting the interface, you would gain better interoperability with more universal interface to allows for easier plug-in of a larger variety of libraries.

That is why I feel, using any additional method on the stream side is a minus, and I see some merit in Meiosis idea to limit to only two stream methods -- map and scan. ;)

dmitriz commented 7 years ago

@jayrbolton

  , h('button', {streams: {click: ['add', 1]}}, 'increment')

I like this way too :)

There is a downside here, unfortunately, which is to reference the stream by the name 'add' embedded as string. That does not feel JS-native, more like passing through selectors, which suffers from the same problem -- it is less explicit inside your code, where 'add' looks like any other string. So by looking at the code, the special "magic" meaning of that string is not clear. It makes the string semantics context-dependent, which I feel is a minus adding extra complexity.

Another downside, your stream library is not explicit here. Of course, you can make it your custom plugin's dependency, but it would be neat to have a more universal plugin with thin interface, to make the stream library a simple parameter rather than dependency. Like https://github.com/ohanhi/hyperscript-helpers accepts any hyperscript library as parameter rather than depend on any specific one.

So this is where the generators come to help if I see it correctly. Their "scary" yield command solves exactly our problem -- it looks like nothing else and hence instantly recognisable. The price is, of course, you have to wrap your function as a go(generator) to be able to yield inside. But it does have advantages to consider:

It is not to say, I don't see merit in the beautiful simplicity of the dumb plain functions (it is the opposite as I've tried to explain in that Cycle issue). Give me a plain function any day!

But there is the cost of the external "magic" API. Or the extra verbosity by declaring the stream in one line and piping into it (see my other posts here).

Perhaps both ways should be made available at this stage, then later we'll see which approach gets better adoption.

And perhaps, there is a way to decouple the complexity by plugging a driver into the plain function?

What do you think?

dmitriz commented 7 years ago

@jayrbolton

Finally, I invited you to a repo called Uzu, where I am in the process of implementing these ideas. I would like Uzu to be the next generation of flimflam (and I think it is a better name, lol), and maybe you'd be interested in helping.

Thank you, I love the name, not sure what it means and where it comes from, but instantly memorable and recognisable :)

If I see it right, h = require('uzu/h') is your element creator function. What I would find really awesome, would be being able to plug into it any other element creator library -- React.createElement or Inferno or Preact or Mithril or Angular or Vue or hyperscript or snabbdom or whatever. Just like the hyperscript-helpers work. That would make all your components instantly pluggable into any framework with only small adapters and no other overheads. I'd find it very powerful. What do you think?

dmitriz commented 7 years ago

@jayrbolton

Regarding continous vs discrete: Could you all give a real-world application example where a discrete-focused system (like flyd) will cause you significant engineering trouble, whereas if you switched to a system like Hareactive, your problems would be mitigated? I have personally never experienced such a difficulty with flyd.

I find the @paldepind blog post does a great job here. The basic class of examples is when you have continuous moving visual objects responding to the user actions (which can be both discrete like mouse clicks and continuous like mouse move).

So on the one side, you have discrete user actions. On the other side, smooth continuous reaction is a better UX than the discrete jumps. So the stream is a better model for the user actions (in most cases), where the behavior is better to model the UI state. And scan is transforming former into latter. Trying to replace the behavior UI state model by the discrete stream would force you into the discretisation of the screen, going down the pass of the implementation details, and away from the simplicity of the continuous model.

But I can see where your questions come from and, yes, you can implement the behavior by keeping piping values into a flyd stream over small negligible periods of time, making it look like continuous. Which should work fine. But you would need to write imperative loops piping your events, which would feel somewhat awkward in FP.

Instead, as @JAForbes is pointing out, it should really be a relationship, basically a dumb plain function from the time to the object coordinates. And functions are both easier than streams and JS-native.

JAForbes commented 7 years ago

but note that on the mobile, the user action will likely be always discrete).

You can model touch input either way. pan/pinch/hold etc.

I wrote a library that is designed to abstract over mouse and touch in a continuous way.

I just want to point out, my aside on imperative responses and continuous relationships is completely orthogonal to the design of returning vtrees that emit to streams vs returning streams of vnodes. Both are valid and can be modelled using either approach. I was really just clarifying my position.

I think in the past @dmitriz when we were discussing returning a stream of vtrees, I was refuting that it was automatically simpler. When I was asking what the return type of the view function was (in our gitter chats), it was hard to pin down, it seemed like magic behaviour. It also may require an operator like flatMap or chain which means we're moving into monad territory, and so that is going to be more complex than just functors.

But that isn't to say you can't have a consistent return type, or model it in a way that isn't magic. And that isn't to say flattening nested streams is bad. Maybe there are benefits to that approach which far outweigh the simpler implementation, maybe it ends up making the application simpler. I was just refuting that it was automatically simpler.

dmitriz commented 7 years ago

@JAForbes

but note that on the mobile, the user action will likely be always discrete).

You can model touch input either way. pan/pinch/hold etc.

You are right, I was thinking dragging over mobile screen were inferior UX to simple taps, but sometimes unavoidable, so I have to take it back. :)

I wrote a library that is designed to abstract over mouse and touch in a continuous way.

Awesome library, nicely hiding the ugly parts πŸ‘

I think in the past @dmitriz when we were discussing returning a stream of vtrees, I was refuting that it was automatically simpler. When I was asking what the return type of the view function was (in our gitter chats), it was hard to pin down, it seemed like magic behaviour. It also may require an operator like flatMap or chain which means we're moving into monad territory, and so that is going to be more complex than just functors.

Yes, you have convinced me that passing actions as arguments is simpler which works great if you can reduce your code to a pure reducer :)

Otherwise, as @jayrbolton and @paldepind point out, when you pass an actual stream (as opposed to functorially lifting your pure reducer) you are stuck with mutating that argument stream, leading to impure functions in your code. That is the problem @paldepind pointed out here. And once you mutate your arguments, a lot of simplicity is lost, so I see the merit to look for other ways.

So you try to return the streams instead of mutating them, and then run into that "return type" question, for which I have tried to propose the interface solution. Basically, you decorate your node with streams that should still keep all the node's properties. But possibly Turbine provides a better more elegant way with its generators?

Yes, you do run into the stream-of-stream problem when lifting your views, so the Monad interfaces seems unavoidable, but it is already there and hidden in your library, so a fair cost to reduce the complexity on the other end imo. Not to say I'd rather not stay with Functors, but that seems to lead to impure code, unfortunately.

JAForbes commented 7 years ago

Awesome library, nicely hiding the ugly parts

Thank you πŸ˜„

you are stuck with mutating that argument stream, leading to impure functions in your code.

I really like where these alternative interfaces are leading, but at the same time I don't think the view is impure if it has event listeners that call streams. If the view is impure because it contains functions that call functions, then compose is impure and Future is impure.

The view function returns the same output given the same input, so its pure.

I think a lot of this comes down to what you consider a "value" though.

paldepind commented 7 years ago

@JAForbes

Well at this point we're really debating the merits of modelling techniques ⛳️ so all I can say is, in my implementation I would model collisions without respect to time, but instead as a function of dimensions and position. Then I would store all the collisions, and in the next frame process them, we end up with the familiar function state -> action -> state, and we are not relying on time at all, just relationships.

That is not what I meant. Sorry, I didn't explain myself properly. Here is another try :smile:

Let's say we want to model this scenario: We have the position of the spaceship and the position of an asteroid. Positions are defined for all moments in time and can change infinitely often. That means they are continuous. Whenever the difference between these two positions becomes small enough we have a collision. We can count the number of collisions. That means they are discrete. Not only that, we need to count the number of collisions if each collision should subtract health from the spaceship.

So, our model should turn the interaction between two continuous behaviour into a discrete stream. There is no way to move collision detection and response to the edge of our model.

My point is that discrete phenomenon can unavoidable occur at all places. Things that are conceptually discrete can arise from the interaction between continuous things. And when we model those we can't push them to edges.

JAForbes commented 7 years ago

You could model it that way @paldepind, but if we receive a list of collisions as part of our continuous stream of actions, then we've moved our discrete modelling out of the stream, and now we're just working with functions of state.

E.g. take streams out of the equation, is this discrete?

State -> Action -> State
update(
  { collisions: [1,2,3,4 ], health: { 1: 100, 2: 50, 3: 80, 4: 100 }}
  , { action: 'ProcessCollisions' }
)

We can say the new health is a function of the existing health, the collisions and the action.

We could run that function 100 times with the same input it would give us the same output. So we can model collisions as continuous, we can push discrete to the edges here.

paldepind commented 7 years ago

I don't think I understand your point @JAForbes.

we receive a list of collisions as part of our continuous stream of actions

If we can count something it is discrete.

You can count how many actions occur. That means they are discrete. So that is not a continuous stream of actions. It is a discrete stream of actions.

The fact that you can store the collisions in a list means they're discrete. Anything that you can store in a list can be counted so it is discrete.

I think it is very helpful to think about the semantics of behavior and stream. I write about that in my blog post. Behaviors are conceptually a function from continuous time to value. Streams are conceptually a list of time ordered values. When thinking about a phenomenon one can ask: Does it make sense to think of this as a function over time or as a list of values over time?

It is easy to see how collisions can be thought of as a list of values over time. [[3, {}], [8, {}]] means that a collision happens at time 3 and time 8.

dmitriz commented 7 years ago

@JAForbes

I really like where these alternative interfaces are leading, but at the same time I don't think the view is impure if it has event listeners that call streams. If the view is impure because it contains functions that call functions, then compose is impure and Future is impure.

The view function returns the same output given the same input, so its pure. I think a lot of this comes down to what you consider a "value" though.

Indeed, both input and return values must be correctly defined :)

The way I see it, the input should include the UI events as well, then the output stream is completely determined. And when testing you have to supply those events as input, then you can test you view like any other pure function :)

paldepind commented 7 years ago

@dmitriz

Can you explain how is it not pure inside?

@jayrbolton

I really like where these alternative interfaces are leading, but at the same time I don't think the view is impure if it has event listeners that call streams. If the view is impure because it contains functions that call functions, then compose is impure and Future is impure.

Not all function calls are equal. You have to make a distinction between calling a pure function and calling an impure function.

Calling streams is impure. And if code calls a function that is impure then that code is also impure.

If your event listeners call streams then they are impure.

dmitriz commented 7 years ago

@paldepind

Not all function calls are equal. You have to make a distinction between calling a pure function and calling an impure function. Calling streams is impure. And if code calls a function that is impure then that code is also impure. If your event listeners call streams then they are impure.

I can see your point. What I mean is, you can hide all the impure function calls inside your drivers. But your view and reducer functions can remain pure in their parameters.

Then calling streams is done inside the driver, and so is allowed to be impure.

Does it make sense?

EDIT. I have removed what I previously said about difference between functions and streams passed as parameters. Actually I don't see any difference. Even with streams, the impurity is hidden inside the calls, and is not our view's responsibility. And yes, both times it is pure on the outside but can be impure at the call time.

JAForbes commented 7 years ago

@paldepind

If we can count something it is discrete.

You can count how many actions occur. That means they are discrete. So that is not a continuous stream of actions. It is a discrete stream of actions.

Just because we can count something, doesn't mean we are counting something.

A list is discrete, but the list is a value within a stream, we're not modelling with any awareness of discrete events, the entire program is modelled as a function ignorant of counts of actions. Time isn't part of the model at all. Instead one frame we compute a list of collisions, from a list of positions and dimensions (these aren't events). In another frame we process that list. But we receive the entire list in one frame and we process them without regard to the list's length. So the model isn't discrete.

The fact that you can store the collisions in a list means they're discrete. Anything that you can store in a list can be counted so it is discrete.

But the discrete model isn't at the level of streams, at the level of a list, which we receive in its entirety and process relationally: we've removed time.

If this was a SQL database, we could write a query relationally.

with pair as (
  select e1, e2 from entities e1 
  cross join entities e2
  where e1.id <> e2.id
)
with collisions( uuid, uuid, bool ) as (
  select e1.id, e2.id, not (
     e1.right <  e2.left 
     or e1.bottom < e2.top
     or e1.top > e2.bottom
     or e1.left > e2.right
  ) as collided
)
select * from collisions -- or do whatever

There is absolutely no notion of time here. And while there are many entities, and they are discrete. We are not working with discrete events in time. Its not that the discrete entities are a problem, its that discrete events in time are a problem. This relationship can be expressed to hold continuously for all entities without regard for time. Just because we could count the number of entities, doesn't mean we are counting them in our model, that's the difference.

Not all function calls are equal. You have to make a distinction between calling a pure function and calling an impure function.

Calling streams is impure. And if code calls a function that is impure then that code is also impure.

But we are not invoking streams in the same invocation when we create the virtual dom. So the view is pure. If this weren't true, the Future/Task/IO monad wouldn't be pure.

const impure = x => console.log(x)
const pure = x => () => console.log(x)

The 2nd example is pure, it has no side effects and it returns the same function when given the same args. The same is true of a view function.

function view(){
  return m('button', { onclick: () => actions( 'INCREMENT') })
}

We're not writing to the stream when we call view(), in fact, this function doesn't do anything at all unless we pass its output to a render function, its 100% pure.

paldepind commented 7 years ago

@JAForbes

I don't think we mean the same things when we say discrete/continuous.

Instead one frame we compute a list of collisions, from a list of positions and dimensions (these aren't events). In another frame we process that list. But we receive the entire list in one frame and we process them without regard to the list's length. So the model isn't discrete.

To me that description sounds very discrete. You talk about one frame, then another frame and another frame. I can count the number of frames. That is what I would call discrete.

If this weren't true, the Future/Task/IO monad wouldn't be pure.

When you program in the IO monad you never mention any impure functions. There is not a single subexpression inside IO-code that is impure. Not a single subexpression that doesn't satisfy referential transparency.

I think there is a difference between a function that is pure and a function that is implemented in a pure way.

Sure, this implementation of map is pure:

function map(fn, list) {
  const list2 = [];
  for (var i = 0; i < list.lenght; ++i) list2[i] = fn(list[i]);
  return list2;
}

But while the function is pure the implementation has none of the qualities of pure code.

This view function is also pure.

function view(){
  return m('button', { onclick: () => actions( 'INCREMENT') })
}

But it returns data that contains an impure function with side-effects. And someone else will take the impure function and call it. So while view is pure the program as a whole is not. Your program loses all the qualities of pure code. It is no better than this piece of code:

function view() {
  return m('button', { onclick: () => globalstate.count++ })
}

The view function is still pure. Is it a good functional program? No.

As you showed with console.log example you can take any impure function, wrap it in a function call and call the result pure. But while you do get a pure function you don't get a pure program.

JAForbes commented 7 years ago

I think there is a difference between a function that is pure and a function that is implemented in a pure way.

That's fair. I just wanted to pin down that a view function with a stream in an event listener is pure. I view this as a description of what we'd like to happen, its just more concrete than having a DSL or something like the Free Monad, and I see benefits to that. Its a description of the exact code we'd like to execute without actually executing that code.

But while you do get a pure function you don't get a pure program.

I see what you're saying and the value of it. But that's not necessarily true. I could compose an entire program without ever invoking those inner functions. I'm sure you've done this too. Maybe its preferable to write it the way you're suggesting, but that's a different conversation I think, one where I'd probably agree with you in many contexts.

To me that description sounds very discrete. You talk about one frame, then another frame and another frame. I can count the number of frames. That is what I would call discrete.

The implementation will always be discrete, that's how CPU's work. I'm using frames to explain to you the implementation outside of the model. But the model isn't aware of the other frames, just like the SQL query I wrote isn't aware of the number of rows. Its the same relationship for 0 rows as it is for 1000 rows.

You can model a relation discretely, you might have a case statement that depends on the count(*) of a particular query. This is a lot like stream abstractions that are aware of time, they might invoke delay, throttle, keepLatest, dropRepeats, buffer, startWith etc. They are abstractions that rely on an awareness of a discrete sequence of events, an awareness of the implementation and I'm saying its preferable to push that awareness to the edge of the system. So we can work without any interest in the underlying mechanism of updating or synchronisation, just as we do in a spreadsheet.

paldepind commented 7 years ago

That's fair. I just wanted to pin down that a view function with a stream in an event listener is pure. I view this as a description of what we'd like to happen, its just more concrete than having a DSL or something like the Free Monad, and I see benefits to that. Its a description of the exact code we'd like to execute without actually executing that code.

Ok. But the difference between that and actually impure code is small. In daily talk Haskell programmers call code that uses IO for impure code. Even though IO is pure as it's just a description of what we'd like to happen (as you say). But for all practical purposes code that uses IO is no better than normal imperative code. So calling it impure is an accurate description of the characteristics of the code.

In Turbine we express the dataflow between model and view in a completely pure way. I think that gives a great practical advantage.

The implementation will always be discrete, that's how CPU's work.

That depends on how far down you go. At the physical level CPUs are continuous. At least so I've been told :smile: But again, I think we're talking past each other with regards to discrete/continuous. Here is a fantastic podcast with Conal Elliott in which he talks about how he invented FRP and his view on the relationship between the continuous and discrete. Possibly he explains it better than I do.

JAForbes commented 7 years ago

@paldepind I'll give it a listen πŸ˜„

JAForbes commented 7 years ago

That was a great listen. I listened three times. So many things to reflect on, but ironically I found a lot of what he said to echo my thoughts. I also think my previous arguments hold. Perhaps I'm not communicating them very well though. There are a lot of things you're saying where I don't agree with your logical deductions, on purity, on what makes something continuous/discrete, on CPU's in particular being continuous πŸ˜„ - but I think I agree with you on far more things than I disagree with and I'm happy we disagree on these points, because its our diverging perspectives that have lead to this interesting discussion. I also think we may be talking past each other as you say, but not just on continuous vs discrete but on streams themselves. I haven't adopted Conal's Stream/Behaviour/Event vernacular, and I think that is leading to confusion.

What I'm saying is, its better to move from Event's to Behaviours as quickly as we can. And that is always possible. All it requires is changing our model from "when did x happen" to "is x happening"? Being as opposed to Doing, as Conal puts it.

From what I can tell from your blog post (please correct me): a behaviour is always continuous while at the same time, it's a Functor which means you can have a Behaviour of any value, in typescript Parlance: Behaviour< Array<Collision>> is valid denotationally, because the type system tells us it must be so.

I'd also say: A CPU cannot be continuous, even if the electrical signal is. A CPU has a speed, and that is how many instructions it can handle at a given rate. And those instructions aren't just samples of a curve, from the perspective of the CPU they are discrete instructions and each instruction can have an abrupt output in the system. I think it's important to accept that computers internally operate on a discrete model, but that doesn't mean we have to build our abstractions in the same way. So arguments of the form "X events are discrete so therefore your model must be discrete" would logically mean we can never able to model continuous systems, and that obviously isn't true.

A function is continuous at a point if it does not have a hole or jump. A "hole" or "jump" in the graph of a function occurs if the value of the function at a point c differs from its limiting value along points that are nearby. Such a point is called a discontinuity. A function is then continuous if it has no holes or jumps: that is, if it is continuous at every point of its domain. Otherwise, a function is discontinuous, at the points where the value of the function differs from its limiting value (if any).

In Conal's model, time is a real number instead of a natural number, but that is only important as an allegory or primitive. You could substitute real numbers for any other domain, and as long as our model supports every infinitesimal possibility in the type's domain we are continuous. If we wanted, we could imagine a universe that only had natural numbers, and in that universe, we could have a continuous model of that domain. We can take this further...

Counter intuitively, if our model is a list, as long as we support every possibility from an empty list to an infinite list, and our model is unaware of each case: we are continuous. If our model was simply binary e.g. a Behaviour<Boolean> as long as we can support true and false without modelling each specific case: we are continuous.

In other words, we can compress continuous space into events, and we can expand discrete events into a continuous signal that demonstrates whether or not the event is taking place. Its just a matter of resolution.

// continuous boolean model (unaware of the specific value)
// modelled as a shared relationship for all values
b.map( x => !x )

// discrete boolean model (aware of each "event" as a discrete unit)
// modelled as discrete outputs for discrete inputs
s.map( x => x == true ? false : true )

The first example is relational, the second is conditional.

Notice the output is the same, but the model is very different. A discrete model is aware of each possibility, and handles them discretely. If we have a case statement for each discrete possibility, we are not continuous.

Even the notion of a click being discrete is entirely abstract. In reality a mouse button receives an amount of continuous pressure, there is a threshold where we decide that pressure warrants a press. Then there is a continuous amount of time a user can hold the button down before releasing, and releasing in itself is a continuous model of force. Finally we encounter a minimum force threshold and we call this a click. But we can always go back to the force/pressure model. And a click is just a merging of the continuous force with the continuous time held, and we convert it into a discrete model. A discrete model is just a highly specific low resolution continuous model.

Just as clicks are modelled as discrete abstraction on top of a continous framework, we can easily go the other direction.

We could say "when the user clicks we emit 1" which is discrete but now we have a number, we can create a continuous model: a.map( a => a + b ). This is a relationship that is unaware of discrete events, and on its own, holds for any possible value in its type domain (-Infinity to +Infinity). Now we can take that continuous model and plug that into the rest of the application. We can always do this, and usually (not always) its advantageous to do so.

In the past @dmitriz and I had a discussion about continuous models having the same output irrespective of the number of events. And I conceded back then, but now I rescind that πŸ˜„ ... the fact the sum changes if we receive a different number of discrete events doesn't change the fact that there are no "holes" or "jumps" in a model: (a,b) => a+b. We are modelling at the level of a relationship between a and b that holds for any a and b, so its continuous. And this is why I disagree that using scan automatically means we are discrete.

That's also why my example of using SQL to model collision detection as a relationship is relevant. Its a model unaware of the specific number of entities. It holds for all possibilities in its domain (including 0,1 and Infinity). Even though in practice the database would never evaluate an infinite table that's completely orthogonal: the model is continuous.

I think the universe is always continuous / analogue while abstractions can be discrete. Operating systems are digital / discrete. Events, as we think of them, do not actually exist physically. I'm currently reading a book on serial verb construction, how we model events in different languages, and event modelling is unsurprisingly unscientific there as well.

It's up to us overcome the specific discrete technical environment we inhabit. That's a lot like Conal modelling an image not as a 2d buffer of pixels, but as a infinite continuum of colour that can vary. Or in his animation example he turned a low resolution event emitter into a high performance curve. Even though in reality the computer was discretely emitting events, he still chose to model continuously and to great advantage. We can always return to the discrete buffer of pixels when we need to, but not within our model; at the edges of the system, when we must interface with a discrete operating system or environment.

And! That's not to say events are bad, or that discrete is bad. Its just that a model that is unaware of discrete events is simpler, and simplicity is valuable. And I wish the Observable/Stream community would emphasise Stream's power for modelling relationships, instead of always focusing on "Arrays over time", which to me is reductive. Not because its inaccurate (its perfectly accurate), but because the idea of what an Array is, (to many developers) is overly concrete and limited, and therefore their models end up being far too concrete and limited.

dmitriz commented 7 years ago

@JAForbes

In the past @dmitriz and I had a discussion about continuous models having the same output irrespective of the number of events. And I conceded back then, but now I rescind that πŸ˜„ ... the fact the sum changes if we receive a different number of discrete events doesn't change the fact that there are no "holes" or "jumps" in a model: (a,b) => a+b. We are modelling at the level of a relationship between a and b that holds for any a and b, so its continuous. And this is why I disagree that using scan automatically means we are discrete.

It is perhaps worth mentioning that we had this discussion in the context of deciding what would be best fit for the Mithri's stream implementation, a discussion with somewhat different goal. Namely achieving the maximum simplicity that lets people get their stuff done in the fastest possible way. Even if it may come at possible conceptual costs or the impurity (which is both acceptable and common in Mithril). With that goal in mind, in my opinion, the discrete stream model for everything would be the simplest one and better known (not least due to the popularity of the wonderful flyd). In this context, the pure stream based is supported by the numerous mature stream libraries, not least the Mithril's own one. Which is still made optional, perhaps, because people still feel repelled by the complexity, even as it is. Introducing continuous behaviour there would probably raise the complexity even further, which may not be in line with Mithril's goals.

And as the Mithril's example shows, people are generally sceptical and reluctant in getting too deeply into what they think are "more theoretical concepts". Just think of the Redux's popularity despite of the complexity cost by avoiding any reactivity, and despite of its honest statements and references to RxJS implementations

I think making streams more simple and accessible to broader public is a worthwhile goal and I am sure both of you would do great job at it. One thing I'd think would makes streams simpler, is building a better link with arrays. Cutting out a period, a snapshot from a stream, and replacing time with discrete order, defines a simple pure function from streams into arrays. Any array method can now be used.

Of course, it is a different issue in the more broad context of this discussion. For which however, I would like to see more concrete practical real world examples, where we can tests and compare different theoretical approaches and use the results to make compelling arguments to others. Especially, being a mathematician myself, I have to watch for being regarded by others as too abstract and theoretical :)

Finally, I'd like to share the project I have just started working on. It is at very early stage but I have tried to explain the main principles and provided a simple working example, which I call the "active counter", and the todos example is currently being ported from Mithril and reworked. Of course, I am very curious about any feedback from both of you, but please keep in mind, the goal is not to replace the more advanced and ambitious projects discussed here, but rather simplicity, the ease of use and universal interoperability with existing technologies. I surely owe some of my inspirations the discussions with both of you, but I have yet to find a good way to mention it.

paldepind commented 7 years ago

@jayrbolton I didn't get around to answer your question here

Could you all give a real-world application example where a discrete-focused system (like flyd) will cause you significant engineering trouble, whereas if you switched to a system like Hareactive, your problems would be mitigated? I have personally never experienced such a difficulty with flyd.

I understand the conceptual distinctions you all are putting forth around continuous vs discrete, and I understand that continous streams have the potential to be more declarative. But I'd also like to relate these ideas back to how it actually affects real-world application building in a practical way using a concrete example.

and here

I think I specifically had in mind what Simon said about behaviors and streams: it is analogous to not separating strings and ints, or perhaps ints and floats (which we are all too familiar with). I can think of many real world examples why we want to separate ints and floats -- eg currency calculation. I was wondering if there were similar specific examples for streams/behaviors.

I think this is a really good question. When you're used to a library that only has streams or observables it is not immediately obvious why you need behavior. On the other hand, I'd say that once you've gotten used to thinking about behaviors and streams it will be very hard to go back to a library that doesn't make the distinction.

To answer your question I've written a blog post: Behaviors and streams, why both?

In the blog post, I discuss some of the benefits to keeping streams and behaviors separate. I'd love to hear what you all think about the blog post and if the reasons I give makes sense to you.

@JAForbes Sorry I haven't replied to your last post yet. I haven't forgotten. I just haven't found the time yet.

JAForbes commented 7 years ago

@paldepind do not feel obliged by any means to reply. Time is fleeting and there are far more important things in life than programming debates 🌈

To answer your question I've written a blog post: Behaviors and streams, why both?

I loved your blog post btw

paldepind commented 7 years ago

@JAForbes

do not feel obliged by any means to reply. Time is fleeting and there are far more important things in life than programming debates 🌈

Thank you :smile: But, I'd very much like to reply. I still think there are some interesting things left in the conversation.

I loved your blog post btw

Are you thinking about the previously linked blog post? This is a brand new blog post solely about the benefits of making a distinction between behaviors and streams. I think some of the points I make will resonate with you as they are similair to the arguments you present against the problems of only working with streams as "arrays over time".

JAForbes commented 7 years ago

I still think there are some interesting things left in the conversation.

πŸ‘

This is a brand new blog post

oh! I missed this one.

Reading πŸ‘“

JAForbes commented 7 years ago

Libraries that can’t tell the difference between behaviors and streams can’t prevent people from carrying out operations that don’t make sense.

I've felt this pain before. Great point.