turbine icon indicating copy to clipboard operation
turbine copied to clipboard

Hello world example

Open dmitriz opened this issue 7 years ago • 17 comments

I have tried to write the simplest possible example to illustrate the framework's value, by showing its reactive behavior and tried to put some hopefully helpful inline comments.

Here is the brief version:

const main = function* () {
  yield div('Welcome from Turbine!')
  const { inputValue: name } = yield input({
    attrs: {
      autofocus: true, 
      placeholder: "Your name?"
    }
  })
  yield div(['Hello, ', name])
}

Having that kind of examples on the main page, was how Angular got its traction, I think.

I would even prefer to replace

  const { inputValue: name } = yield input({
    attrs: {
      autofocus: true, 
      placeholder: "Your name?"
    }
  })

with the less verbose

  const { inputValue: name } = yield input({
      autofocus: true, 
      placeholder: "Your name?"
  })

Let me know what you think.

dmitriz avatar May 19 '17 12:05 dmitriz

Just to show how verbose is React in comparison :)

class HelloTurbine extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }
  handleChange(e) {
    this.setState({name, e.target.value})
  }
  render() {
    return (
      <div>
         <div>Welcome from Turbine!</div>
         <input onChange={this.handleChange} placeholder='Your name?' autofocus />
         <div>Hello, {this.state.name}</div>
      </div>
    );
  }
}

dmitriz avatar May 19 '17 13:05 dmitriz

Also the desugared chain version can be mentioned along and looks even simpler for this example:

const main = () =>
    div('Welcome from Turbine!')
        .chain(() => input({
            attrs: { autofocus: true, placeholder: "Your name?" },
            output: { name: 'inputValue' }
        })
        .chain(({ name }) => div(['Hello, ', name]))
}

In comparison, the un-version would need a separate state management and we would need to resort to the oninput event:

const reducer = (name, newName) => newName

const view = (name, dispatch) => [
    div('Welcome from Turbine!'),
    input({
            attrs: { autofocus: true, placeholder: "Your name?" },
            oninput: () => dispatch(e.target.value)
    })
    div(['Hello, ', name]))
]

In Redux there would be even more code with all the actions needing being passed to the global state.

So it is remarkable how even for that simple example, the Turbine can cut everything down to a single function. 😄

dmitriz avatar May 23 '17 03:05 dmitriz

Although the React example is more verbose, one advantage is that the rendering of the content is completely independent of where data needs to go. For example, in the React example I can change

      <div>
         <div>Welcome from Turbine!</div>
         <input onChange={this.handleChange} placeholder='Your name?' autofocus />
         <div>Hello, {this.state.name}</div>
      </div>

to

      <div>
         <div>Welcome from Turbine!</div>
         <div>Hello, {this.state.name}</div>
         <input onChange={this.handleChange} placeholder='Your name?' autofocus />
      </div>

and it will work as expected.

It doesn't seem so obvious how to make the same change given the Turbine example:

const main = function* () {
  yield div('Welcome from Turbine!')
  const { inputValue: name } = yield input({
    attrs: {
      autofocus: true, 
      placeholder: "Your name?"
    }
  })
  yield div(['Hello, ', name])
}

How do I render the last div before the input? How would the example change?

Also, yeah,

  const { inputValue: name } = yield input({
      autofocus: true, 
      placeholder: "Your name?"
  })

is nicer, cleaner, and shorter. What other things do the components accept besides attrs?

trusktr avatar May 26 '17 06:05 trusktr

@trusktr

You raise some good points. How to put the output before the component is exactly the questions I asked in https://github.com/funkia/turbine/pull/31#issuecomment-301054029 and it appeared the Turbine provides a very nice solution with the loop function, except that the example there won't work anymore, because now the input would have to explicitly declare its output (which is a good thing).

So we can rewrite the example as

const main = loop(({name})  =>
    div('Welcome from Turbine!')
        .chain(() => div(['Hello, ', name]))
        .chain(() => input({
            attrs: { autofocus: true, placeholder: "Your name?" },
            output: { name: 'inputValue' }
        })
})

Now the name is taken from the argument and the output is being passed back and "looped". And there is still no need for a state as the argument behavior name is taking care of it.

The order independence in the React example is not really specific to React. E.g. in Mithril you would write it as

const main = name => [
    div('Welcome from Turbine!'),
    div(['Hello, ', name]),
    input({
        autofocus: true, 
        placeholder: "Your name?",
        oninput: e => name = e.target.value
    })
]

which is, of course, impure, but neither is React with setState.

You could get a pure version with un by splitting off the state updating reducer:

const reducer = (name, action) => action

const view = (name, dispatch) => [
    div('Welcome from Turbine!'),
    div(['Hello, ', name]),
    input({
        autofocus: true, 
        placeholder: "Your name?",
        oninput: e => dispatch(e.target.value)
    })
]

which is similar to Redux but without touching the global store.

Which also corresponds to the Turbine's (you need to check with @paldepind and @limemloh that it actually works):

const main = modelView(
    ({ name }) => name,
    name => [
      div('Welcome from Turbine!')
        .chain(() => div(['Hello, ', name]))
        .chain(() => input({
            attrs: { autofocus: true, placeholder: "Your name?" },
            output: { name: 'inputValue' }
        })
     ]
)

Or with flyd

const main = name => [
    div('Welcome from Turbine!'),
    div(['Hello, ', name()]),
    input({
        autofocus: true, 
        placeholder: "Your name?",
        oninput: e => name(e.target.value)
    })
]

where name must be passed as flyd stream or created inside the main function. However again, strictly speaking, it is not pure as you have to mutate the stream. On the other hand, it may be seen as the lift of the functional version, where name is passed as function, which is pure.

Some additional verbosity in React comes also from several other parts, like using the class syntax or the named props instead of regular function parameters.

What other things do the components accept besides attrs?

You can see some here: https://github.com/funkia/turbine/blob/master/src/dom-builder.ts#L64 where attrs are made separate from props, but I am not aware of any benefits of not merging them together like React does.

dmitriz avatar May 26 '17 09:05 dmitriz

@trusktr

Although the React example is more verbose, one advantage is that the rendering of the content is completely independent of where data needs to go.

As @dmitriz already mentioned you can achieve this with the loop function, but I would prefer writing the code like this:

const main = loop(({name}) => div([
  div('Welcome from Turbine!'),
  div(['Hello, ', name])),
  input({
    attrs: { autofocus: true, placeholder: "Your name?" },
    output: { name: 'inputValue' }
  })
]));

Here it is easy to change the order without breaking anything.

Which also corresponds to the Turbine's

Almost :smiley: model have to return a Now

const main = modelView(
    ({name}) => Now.of({name}),
    ({name}) => [
      div('Welcome from Turbine!')
        .chain(() => div(['Hello, ', name]))
        .chain(() => input({
            attrs: { autofocus: true, placeholder: "Your name?" },
            output: { name: 'inputValue' }
        })
     ]
)

for better readability I would write it like this:

const model = ({name}) => Now.of({name});

const view = ({name}) => [
  div('Welcome from Turbine!')
  div(['Hello, ', name]))
  input({
    attrs: { autofocus: true, placeholder: "Your name?" },
    output: { name: 'inputValue' }
  })
];

const main = modelView(model, view);

limemloh avatar May 26 '17 11:05 limemloh

@limemloh

Almost 😃 model have to return a Now

Thanks for correction, I was thinking about it for a second and then left it out in the hope I would be forgiven by the overloading :)

So the perfection of this example is broken 😢 https://github.com/funkia/turbine#completely-explicit-data-flow

You are right, no need to chain once the parameters are taken out.

dmitriz avatar May 26 '17 11:05 dmitriz

but I am not aware of any benefits of not merging them together like React does.

attrs is the same as setting attributes on the element which I think is the same as what React does.

paldepind avatar May 26 '17 19:05 paldepind

but I am not aware of any benefits of not merging them together like React does.

attrs is the same as setting attributes on the element which I think is the same as what React does.

I probably should have said it less cryptic :) I mean that in React you simply write

input({ placeholder: 'name' })

without nesting under the attrs. That makes the code easier to read and write.

You lose the separation of props vs attributes but what I said is, I am not aware of any benefits of such separation, at the cost of the more verbosity.

dmitriz avatar May 27 '17 03:05 dmitriz

@dmitriz I would include that desugared example somewhere in the README. That totally made sense to me in terms of category theory and demystified everything that going on in the example with generators... which begs the question -- why use generators?

ccorcos avatar May 30 '17 17:05 ccorcos

@dmitriz

without nesting under the attrs. That makes the code easier to read and write.

It is indeed easier to read as far as element props go. (yeah that's what he meant @paldepind) What would we do with the output property, etc? For example, in

  input({
    attrs: { autofocus: true, placeholder: "Your name?" },
    output: { name: 'inputValue' }
  })

output is outside of attrs. Maybe it would be a separate options arg? f.e.

  input({ autofocus: true, placeholder: "Your name?" }, {
    output: { name: 'inputValue' }
  })

so sometimes we won't need it, and the extra arg can be omitted:

  div({ class: "foo" })

Another option (but maybe uglier) could be special naming convention:

  input({ autofocus: true, placeholder: "Your name?", _output: { name: 'inputValue' } })

trusktr avatar May 31 '17 05:05 trusktr

@ccorcos

I would include that desugared example somewhere in the README. That totally made sense to me in terms of category theory and demystified everything that going on in the example with generators... which begs the question -- why use generators?

Did you see the section understanding generator functions in the readme? The section includes several desugared examples as well.

We use generators because Component is a monad due to it's chain method. But using chain directly leads to huge amount of nesting which is undesirable. So we use generators to achieve something similar to "do-noation" in Haskell.

It's unfortunate that the generators look like "magic" but really what they do is pretty simple. We can't really avoid them since monads are no fun to use without the special syntax. One way of saying it is that generators are two monads what async/await is to promises.

paldepind avatar May 31 '17 09:05 paldepind

@dmitriz @trusktr

I agree that having attributes directly on the object looks nice. But as @trusktr points out there is more going on than just attributes. In fact, there's a bunch of things that one can specify on the object. For instance you can do something like this div({classToggle: {foo: behavior, bar: behavior2}}) and the classes on the div will be toggled based on whether or not the behavior is true or false.

The advantage of having attrs on it own sub-object is that attributes are then clearly separated from the other things. The downside, of course, is that one will have to do a bit of extra typing.

paldepind avatar May 31 '17 09:05 paldepind

@paldepind @trusktr

The advantage of having attrs on it own sub-object is that attributes are then clearly separated from the other things.

Is that an advantage because otherwise there is some danger that they clash? For instance, if placehoder is declared directly on the node, is there any potential problem? For instance, the output or other prop is also set directly on the node but I've thought there would be no confusion with attributes, since there is no attributes with that names.

I've thought that was how React is dealing with its props vs attributes by making them basically the same with no downside, is it correct?

dmitriz avatar May 31 '17 12:05 dmitriz

For instance, the output or other prop is also set directly on the node but I've thought there would be no confusion with attributes, since there is no attributes with that names. I've thought that was how React is dealing with its props vs attributes by making them basically the same with no downside, is it correct?

We can't make that assumption, we don't know what requirements the DOM has, and there's also custom elements that can have any attribute names imaginable. There could be some CSS library that requires an output attribute for styling, there could be some JS library that reads data from a DOM and looks at an output attribute to get data (even if that DOM is generated with Turbine). Custom Elements can have any attribute like asdf, foobar, anything.

Props are the same as attributes in React. They map to element attributes. But React is different because JSX elements (div, input, etc, not components) only receive data via props, and they don't need any extra meta info passed, like Turbine components and output.

The classToggle example, that's not an attribute, it's a way to specify class toggling. It's not an attribute.

@paldepind WHat about maybe chaining the API instead of an object?

const name = yield input({placeholder: 'name'})
  .output('value')
  .classToggle('foo', behavior)
  .classToggle({bar: behavior2, baz: behavior3})

// pass `name` along here like before, for example

Would that be too imperative?

trusktr avatar May 31 '17 16:05 trusktr

@trusktr I've created an issue specifically for discussing how to set attributes #58. I'll be answering you over there 😄

paldepind avatar May 31 '17 16:05 paldepind

@trusktr

  input({ autofocus: true,  placeholder: "Your name?",  _output: { name: 'inputValue' } })

It is an interesting idea, the props like output have some special functionality, so it would make sense to let them stand out of the rest.

For instance, I have found it very convenient how Angular 1 prefixed its special api methods with $ like in the $scope. This makes it instantly clear that a property like $scope enjoys a special role. By looking at it, I instantly recognise it.

On the other hand, I find it confusing how React lacks to provide any hint about the special role of its methods such as componentDidMount. Here, without reading the manual, I have no clue if that method is a regular one or has any special meaning.

dmitriz avatar May 31 '17 20:05 dmitriz

@trusktr

We can't make that assumption, we don't know what requirements the DOM has, and there's also custom elements that can have any attribute names imaginable. There could be some CSS library that requires an output attribute for styling, there could be some JS library that reads data from a DOM and looks at an output attribute to get data (even if that DOM is generated with Turbine). Custom Elements can have any attribute like asdf, foobar, anything.

That might be another reason to mark the special attributes like output in more unique ways.

Another library would also likely use the flat structure :)

Props are the same as attributes in React. They map to element attributes. But React is different because JSX elements (div, input, etc, not components) only receive data via props, and they don't need any extra meta info passed, like Turbine components and output. The classToggle example, that's not an attribute, it's a way to specify class toggling. It's not an attribute.

You can still write it as attribute as there is no other attribute with that name. But probably marking it somehow would be even better.

dmitriz avatar May 31 '17 21:05 dmitriz