Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hello world example #48

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open

Hello world example #48

wants to merge 5 commits into from

Conversation

dmitriz
Copy link
Contributor

@dmitriz dmitriz commented May 19, 2017

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
Copy link
Contributor Author

dmitriz commented May 19, 2017

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
Copy link
Contributor Author

dmitriz commented May 23, 2017

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. 😄

This was referenced May 23, 2017
@trusktr
Copy link

trusktr commented May 26, 2017

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?

@dmitriz
Copy link
Contributor Author

dmitriz commented May 26, 2017

@trusktr

You raise some good points.
How to put the output before the component is exactly the questions I asked in
#31 (comment)
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.

@limemloh
Copy link
Member

@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 😃
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);

@dmitriz
Copy link
Contributor Author

dmitriz commented May 26, 2017

@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.

@paldepind
Copy link
Member

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.

@dmitriz
Copy link
Contributor Author

dmitriz commented May 27, 2017

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.

@ccorcos
Copy link

ccorcos commented May 30, 2017

@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?

@trusktr
Copy link

trusktr commented May 31, 2017

@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' } })

@paldepind
Copy link
Member

@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
Copy link
Member

@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.

@dmitriz
Copy link
Contributor Author

dmitriz commented May 31, 2017

@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?

@trusktr
Copy link

trusktr commented May 31, 2017

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?

@paldepind
Copy link
Member

paldepind commented May 31, 2017

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

@dmitriz
Copy link
Contributor Author

dmitriz commented May 31, 2017

@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
Copy link
Contributor Author

dmitriz commented May 31, 2017

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants