funkia / turbine

Purely functional frontend framework for building web applications
MIT License
687 stars 27 forks source link

Uncomponentisation and Universalisation? #34

Open dmitriz opened 7 years ago

dmitriz commented 7 years ago

(It is perhaps by now clear that) I find this library really interesting and inspiring.

I have never seen the use of generators in this fashion, if just one new idea to be mentioned, among others.

I really would like to leverage the unproject to make it even more accessible and pluggable.

So anyone can try the Turbine with a simple piece of code, and then instantly plug and use inside any existing working application.

Uncomponentisation

Specifically, what I mean by "uncomponent" here is to enable this example

const main = go(function*() {
  yield span("Please enter an email address: ");
  const {inputValue: email} = yield input();
  const isValid = map(isValidEmail, email);
  yield div([
    "The address is ", map((b) => b ? "valid" : "invalid", isValid)
  ]);
});

to be written as pure generator with no external dependency:

const view = ({ span, input, div }) => function*() {
  yield span("Please enter an email address: ");
  const {inputValue: email} = yield input();
  const isValid = map(isValidEmail, email);
  yield div([
    "The address is ", map((b) => b ? "valid" : "invalid", isValid)
  ]);
}

It is almost the same but now can be used and tested anywhere in its purity. It would be recognised and packaged with go internally by the Turbine at the runtime.

Universality

What I mean by universality is to be able to plug this into any application. Here is how the un tries to make it work:

// the only place to import from un
const createMount = require('un.js')

// configure the mount function only once in your code
const mount = createMount({ 

  // your favorite stream factory 
  // mithril/stream, flyd, most, or hareactive?
  createStream: require(...),

  // your favorite element creator 
  // mitrhil, (React|Preact|Inferno).createElement, snabbdom/h, hyperscript, 
  //  or turbine.createElement?
  createElement: require(...),

  // convenience helpers for your favorite 
  createTags: require(...),
 // (possibly to be renamed in createElements)

  // mithril.render, (React|Preact|Inferno).render, snabbdom-patch 
  //  or turbine.render?
  createRender: require(...)
})

Use the mount function to run a Turbine's component directly in DOM:

const { actions, states } = mount({ el, model, view, initState: 0 })

or embed it anywhere into any framework:

const { actions, states } = MyComponent = mount({ model, view, initState: 0 })
    ...
// inside your React Component or wherever
    ...
render = () => 
    ...
    React.createElement(MyComponent, props)
    ...

// or with JSX if that's your thing:
    ...
    <MyComponent prop1={...}  prop2={...}  prop3={...}/>
    ...

Here is an example of the working code

Here is what I think is needed to make it possible:

It would be great to hear what you think about it.

paldepind commented 7 years ago

@dmitriz

(It is perhaps by now clear that) I find this library really interesting and inspiring.

We are super happy to have you around.

I have never seen the use of generators in this fashion, if just one new idea to be mentioned, among others.

We can't take full credit for that idea. Component is a monad and monads are hard to use without do-notation. Of course monads and the idea of do-notation is from Haskell. And using generators to imitate do-notation was and idea I first saw in fantasydo.

Specifically, what I mean by "uncomponent" here is to enable this example

...

to be written as pure generator with no external dependency:

Well, that makes a lot of sense. Using go is already optional in many cases. For instance, one can write div(function*() { ... }). We already have a mechanism in place for handling all the various things that can be converted into a component. dc1f2a25 adds this to runComponent so that the top-most component can also be anything that can be converted to a component. I've also changed the simple example to make use of this.

If I understand your goals in un correctly the intention is that one can write components that have no dependencies on any framework? I think that sounds cool. But doesn't it add some severe restrictions to the type of architecture one can implement?

dmitriz commented 7 years ago

@paldepind

We are super happy to have you around.

Thank you, that means a lot to me!

I have never seen the use of generators in this fashion, if just one new idea to be mentioned, among others. We can't take full credit for that idea. Component is a monad and monads are hard to use without do-notation. Of course monads and the idea of do-notation is from Haskell. And using generators to imitate do-notation was and idea I first saw in fantasydo.

I think I had in mind the idea to use it for multiple outputs coming out from a UI component. That have a "parallel" flavour comparing to the Haskel's sequential do, which is of course not new to you, but I didn't find the connection obvious.

But now that you mention the do/go, I had a second thought that maybe there should be several roles for the generators, otherwise some people may feel there is "too much magic".

In which case, simply reducing all role to the same looking generator function, may lead to a loss of its role. What I mean is that sometimes you may actually want the proper generator functionality, sometimes you want to use it with the go, and sometimes with the loop. In the first case, you have to run it manually like normally in JS, in the second it used for the sequential go and in the last case it is "looped", where the output becomes available as part of the input. You did not include go as prominently as loop on the main page, so I must have been confused by that. But perhaps it should be part of the core API needed to provide the generators with different roles they can play in the framework?

And if the answer is yes, it would have its place along with other core API:

const view = ({ div, span, input, go }) => go(function* () { .... })

which could be again exported with no dependency.

But now that you mentioned Monadic structure, I could think of perhaps another, simpler way to write this particular example even without generators:

const main = () => 
    span("Please enter an email address: ")
    .chain(() => input())
    .map(({inputValue}) => map(isValidEmail)(inputValue))
    .chain(valid => div([
      "The address is ",
      valid ? 'valid' : 'invalid'
    ]))

Would it work?

If I understand your goals in un correctly the intention is that one can write components that have no dependencies on any framework? I think that sounds cool. But doesn't it add some severe restrictions to the type of architecture one can implement?

Perhaps I will run into more difficulties later on, but at this stage, when I am still shaping the ideas, it does not feel so much of a problem. Not yet. :)

There is the same pattern I can see across various frameworks, where you always have views mapping state into dom and models responsible for updating the state. So it feels natural to leverage this and encapsulate in the same architecture pattern, at least at this level of abstraction.

But otherwise, anywhere else, I like the idea of giving the complete freedom how to write the views and models. So with Turbine, you would even use the generators sometimes wrapped into their specific roles. And internally, there wouldn't be any restrictions at all. You would write exactly the same code as you would write anyway. And some people may prefer to carry their dependencies along rather than export them as parameters, which is fine. Of course, that would add some rigidity and limit the "internal universality" of your components. By which I mean e.g. using the Snabbdom flavour to write dom in React or Mithril and vice versa.

But you can still keep the "external universality" for your Turbine components, that is the ability to plug them into outside frameworks. And only at the very top root level, your component would need to conform to some pattern. For instance, to be used inside a larger React app, you would wrap your Turbine component as React's one and then use it the normal way without even noticing the difference. And you can write Turbine's components with advanced functionality, that people can use in React without even paying attention :)

The only API you would need to connect with React is a Renderer, whose function is to attach the dom (or vdom) element to the parent, and then to be able to subscribe to the updates. Internally it would recognise whether you attach to the real dom element or to some vnode inside a framework. In case of React, that means either calling React.render or wrapping into a thin React.Component class that would live inside its parent and update via the React's native forceUpdate method. So there is some adaptor to write here, but is it a universal one to work for any library wanting to get embedded into React. Which on its root side, needs to provide a dom-like node, which it does anyway, along with its state stream, to be sent directly into the React's renderer. And even if it is not a reactive library like Redux, the un can convert their stores into state streams, and now you are back to the same api.

I'd be very curious to hear your opinion, if that makes sense, and any problem you might see there coming along the way.

dmitriz commented 7 years ago

BTW, here is how I would see a possible way to embed un-component inside the Turbine: https://github.com/uzujs/uzu/issues/5#issuecomment-301454959

So all that can be done in simple-minded redux-style way, can be encapsulated away into plain uncomponents unaware of the streams.

These could even be React or other framework functional components.

Then once mounted as Turbine's component, full control is passed to the Turbine. But now Turbine can take care of higher level stuff without caring about the plain details :)

paldepind commented 7 years ago

@dmitriz

Would it work?

Yes, indeed. That example should work. In fact, one can always rewrite Turbine code to eliminate the use of generator functions. They are not essential. But they make things a lot easier 😄

But now that you mention the do/go, I had a second thought that maybe there should be several roles for the generators, otherwise some people may feel there is "too much magic".

In which case, simply reducing all role to the same looking generator function, may lead to a loss of its role. What I mean is that sometimes you may actually want the proper generator functionality, sometimes you want to use it with the go, and sometimes with the loop. In the first case, you have to run it manually like normally in JS, in the second it used for the sequential go and in the last case it is "looped", where the output becomes available as part of the input. You did not include go as prominently as loop on the main page, so I must have been confused by that. But perhaps it should be part of the core API needed to provide the generators with different roles they can play in the framework?

I am not sure what you means that the should be several roles for the generator functions? In all cases, the generator functions do the same thing. They're sugar for calling chain. I've made a PR #38 with some additions to the documentation. It contains a section about our use of generator functions. I'm not sure if it's understandable but I try to make the point that generator functions are just an easier way to call chain many times.

dmitriz commented 7 years ago

Yes, indeed. That example should work. In fact, one can always rewrite Turbine code to eliminate the use of generator functions. They are not essential. But they make things a lot easier 😄

Ah, that is good to know ;) Yes, big nesting can be annoying, but the generators carry their complexity cost, so without deep nesting the plain function way feels easier.

Even for that example, I'd find it "tolerably" awkward if I writing as

input()
    .chain( ({ inputValue: a }) => input() 
        .chain( ({ inputValue: b }) => span(["Combined text: ", a, b]) ).
    )

Of course, you only need nesting when the current function needs results from computations before the last one, otherwise you can nicely chain them without nesting, but I see your point.

But what is less clear is what is a in this example:

div(`Hello`)
    .chain( a => div( ... ) )

Something like vnode inside the Mithril view function? In other words, is the element itself also part of the output?

Yes, any further explanation for topics such as generators helps, as those are quite new for many people, and I don't quite feel their usefulness is properly explained in most articles.

It might also be good to give the link to Jabz for more details on the go function, since it is a non-standard feature, at least in JS. You probably didn't mean to write do in your examples.

About the roles of generators, I was thinking of the use case when you show the user a sequence of input fields, one at a time. So you stop, and resume when new entry arrives. Would that be another role for generators, where the go would not run? It would be interesting to model this situation as simple generator run, where you can use user's submit event for the next generator run, which would display the next form and so on. Here you use it for resuming rather than do-notation, which is what I've had in mind by another role.

Does it make sense?

paldepind commented 7 years ago

@dmitriz

But what is less clear is what is a in this example:

div(`Hello`)
.chain( a => div( ... ) )

Something like vnode inside the Mithril view function? In other words, is the element itself also part of the output?

The element is part of the Component but not part of the output. If you do div("Hello").chain(a => ...) then a will be an object of all the events on a div element. It will have click, mouseover, etc.

About the roles of generators, I was thinking of the use case when you show the user a sequence of input fields, one at a time. So you stop, and resume when new entry arrives. Would that be another role for generators, where the go would not run? It would be interesting to model this situation as simple generator run, where you can use user's submit event for the next generator run, which would display the next form and so on. Here you use it for resuming rather than do-notation, which is what I've had in mind by another role.

I think that is an interesting idea and it would be exciting to explore. But for now, in Turbine, I think we should try to focus on using generators as normal do-notation. Generators are already confusing enough and I like that we can explain that they only do one single and pretty simple thing (i.e. call chain in a nicer way).