funkia / turbine

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

Embedding view into model? #37

Open dmitriz opened 7 years ago

dmitriz commented 7 years ago

I had another look at this inspiring diagram https://github.com/funkia/turbine#adding-a-model and got the feeling that the model really behaves like the parent to the view.

The view's both input and output only go through the model, which exactly mirrors the child-parent component relationship:

// passed only inputs for the model
const model = ({ inputsForModel }) => {
    ...
    // the model is passing only what the view needs to know
    const view = ({ message }) => {
        return div([
            message ? `Your message is ${message}` : `Click button...`,
            button({ output: {updateMe: 'click' } }, `Click me!`)
        ])
    }
    ....
    // prepare message to show
    const message = updateMe.map(getMessage)
    ...
    // return the view with message updated
    return view({ message })
}

It looks like the model "knows" too much about view, but I'd argue that it computes and passes the message exactly the same way as if the view were external. In fact, you could enforce it by rewriting your model as

const model =  ({ inputsForModel, view }) => ...

const view = ({ message }) => ...

const modelView = (model, view) => (...args) => model({ ...args, view })

and then reusing the same model with different views.

The advantage of this is enforcing more separation of concern, where the view receives and emits only what is needed, which need not be passed to the outside parents.

Also it may help to keep the view "dumb" and simple, just passing actions and displaying the current state. That would allow to simplify the structure by possibly entirely moving streams and generators out of the view into the model.

It doesn't mean to use this pattern for everything, but it may help simplifying many use cases.

What do you think?

dmitriz commented 7 years ago

Fitting illustration from https://hackernoon.com/10-react-mini-patterns-c1da92f068c5

data-flow

Shows very similar pattern how the top model sends data to the view and subsequently to child component via the props. However, the props play the "poor man's role" as replacement of proper functional JS-native relation. You really want to pass the items to the child model first, not the view. It is then the child model's responsibility to wrap it in the best way for the view's consumption.

The child view also sends the click event up via props, which again feels like breaking the single responsibility that always bothered me in React -- the child view needs to know too much about the passing props structure. Whence the bulky propTypes list taking more space than the view itself. Here I like infinitely much better the Turbine's output idea with the button yelling into deleteStream and leave it with model to take care of. No more propTypes.

The child model would deal with it or wrap it in suitable form and pass up to the parent model. And that way the more local stuff from the view is split from the fewer higher level actions that really deserve the parent model's attention :)

paldepind commented 7 years ago

I agree that enforcing a separation of concerns between the model and the view is very important. Did you see my post here #35 where I make a similar point?

I think your remarks with regards to React are very interesting 👍 Am I understanding you correctly that you don't like how React uses properties both as input and output? I think that is very true. React and some other libraries have the problem that their components only return a virtual dom. When used in the view they return something like VNode. But in Turbine a components used in the view has type Component<A>. It is a generic type which allows the component to return output to its parent. That is also what makes Component a monad which makes it extremely composable.

dmitriz commented 7 years ago

I agree that enforcing a separation of concerns between the model and the view is very important. Did you see my post here #35 where I make a similar point?

I must have missed it, thank you for calling my attention, you are making good point. But it feels like a general principle to abstract interfaces for both model and view, to reduce their coupling.

I find it somehow easier to always start with the view, not the model, because view is your final goal, where the model is just a mean to it. Also making the view reusable is often much harder.

So in this example, I would start from the lowest level and go up. The button(props, children) is already abstract and reusable. At the next level, what makes them different is the label and clickStream:

Button(label, {output: clickStream})

Here not much happened but the next level is more interesting.

We want to use the buttons actions as state transformer of the counter, which is a reducer with single increment action. That way we can decouple action from the buttons and use them for any events with any view. The view is just a bunch of buttons with clicks, which is simple and reusable. The reducer is a pure decoupled function that only needs to know the action entry interface. You can see it as an external helper to the model.

It could be a more complex stream transformer, but in this example it is just a reducer, which I think is a really good abstraction. Any component will have some pure reducer I think, when you remove the side-effects.

Finally, the model merges several action streams and runs the scan over the reducer, which is another very general reusable pattern. So really the reducer is the only unique piece of code, plus the button labels that can be seen together with the actions as their part.

However, in that example there is no parent component, making the view-model-embedding benefit possibly less visible I think. It would be good to see it in presence of at least 1 parent and 1 child components.


I think your remarks with regards to React are very interesting 👍 Am I understanding you correctly that you don't like how React uses properties both as input and output? I think that is very true.

Good to hear :) I do have to be fair and have to say React had brought some really great ideas. I even liked how it passed the props at the beginning that I missed in Angular, where you would have to declare them explicitly in your directive definition. The Angular directives declarations with multiple pipes felt repetitive and somewhat annoying, whereas in React you could simply do

const Hello = () => div( `Hello ${this.props.name}` )

which felt light and great, until you realise that by looking at it, you have absolutely no clue what the props are.

At least these days we can write

const Hello = ({ name }) => div( `Hello ${name}` )

which is much easier to read exactly because it answers that question. But now, with props, we suddenly run into the silly problem that we can't just write a simple function like this one:

const Hello = ( name ) => div( `Hello ${name}` ).

Which would allow me to relax, knowing that anything I pass to it will be taken as name, whereas with props, I am forced to come up with some unique clever name that now must be in sync between the child and parent, often spread over different files.

Speaking of the tight coupling ... :)

And even with destructuring, the popular class syntax you see everywhere in React code, pushes people through the hoops of the constructors, inevitable this and maybe that, and if you not lucky, the annoying .bind(this) will join in as well ;) For my taste, it just produces a lot of boilerplatte with no benefits.

Another problem with props that I mentioned is when people are abusing them and you have to jump through multiple components chains spread over multiple files, only to understand what your function does. Because again by looking at the props, I have no idea. And worse, you don't even know which props are passed from the outside. :) So now you have the propTypes to solve this problem, which initially looked neat, until again, people started abusing the props.

Sadly the Redux, being otherwise an elegant pattern with great ideas, encourages this "prop spagetti" by making its state global, so you are forced to move it up via the props.

And don't get me started on the JSX ... :)

And yes, as you say, the vdom has some problems too. One is the quite intrusive demand to provide the key attribute every time you iterate over array. Imo, it should be the framework's job to take care of that.

And the problem of vnodes vs components as you mention is another one, though React tries to treat them equally I think. But then either it is stateless or impure or your state is far away in some global Redux store :)


The Monadic idea you raise is very interesting, I have never seen vnodes regarded as Monads anywhere else. What about the fragments aka arrays of components? Composing with arrays of children is still not monadic, unless you regards the fragments as components perhaps?

And actually this describes quite well the typical application flow where you see some information summary first, that the user wants to expand, dispatches the action, and the result can be seen as its main action's output that would be wrapped into the list view, and shown right beneath, which is exactly your monadic composition - very elegant!