danielearwicker / danielearwicker.github.io

My blog - create an issue if you want to comment!
http://danielearwicker.github.io/
0 stars 0 forks source link

React render function is __not__ pure #2

Open chadrien opened 7 years ago

chadrien commented 7 years ago

Hi,

Regarding your post "MobX - Like React, but for Data", I wanted to add some precision regarding the The render function of a React component is a pure function part.

render is not at all pure. From the definition of a pure function

The function result value cannot depend on any hidden information or state that may change while program execution proceeds

I'm not blaming you or anything, I'd just like to avoid misinforming people that would not know much about FP :)

danielearwicker commented 7 years ago

Thanks for the feedback. I tried to explain that more deeply in the post (hope you kept reading!) but maybe more explanation is needed. There is a sense in which render is pure, but you can't get to it just by looking at syntactic structure.

Consider stateless components. These are just functions that take props as an argument and it's easy to see that they are pure.

But they are equivalent to a class component that has a render method that only depends on this.props - such a component is no less pure in design. It's merely a technical implementation difference.

Similarly, imagine if React was designed differently so that the render method of a component class had the signature render(props, state) instead of accessing those things via this. It would then be obvious that it is a pure function of props and state. Again, no effective difference from the current design in terms of behaviour/correctness.

More abstractly, the wiki article you link to mentions "program execution". Suppose the program is a very inflexible calculator:

Example 1

const x = 2;
const y = 3;

const r = x + y;

console.log(r);

Note that to see the result I have to call console.log so ultimately my program is not pure. This is unavoidable, as I need to modify the state of the universe! Haskell recognises this, in that the type of main is main :: IO ().

If I edit the source and change x to 4 then I can run the program and get 7. But this is allowed because obviously I didn't do that "while program execution proceeds".

Now try:

Example 2

const x = observable<number>(2);
const y = observable<number>(3);

const r = computed(() => x.get() + y.get());

autorun(() => console.log(`Result is: ${r.get()}`));

x.set(4);
y.set(0);
x.set(2);

What I've done here is lifted the definitions of x, y and r, and my logging instruction, into an environment that acts like a re-runnable program. When I change the value of x or y, r is automatically "corrected", and the side-effect is logged to the console again.

The rules are:

Now I don't have to stop my program, edit the code and restart it to see changes.

The advantage is that I can write the relationships between values as a series of declarations, just like in mathematics, just like in Example 1. Then I can apply the above rules to get Example 2, and this preserves the same meaning (and correctness) but means we can try other values without having to restart the program.

I could create a user interface that binds x and y to inputs so the user can change them and see r change. Yet the expression that defines r maps in a precise way onto the pure declaration in Example 1.

Also in a large complex program, only the affected computed values are re-evaluated, reducing the need to recompute everything from scratch.

The danger of mutable state (and impure functions depending on it) is that when the state changes this can result in inconsistency.

But if everything that depends on the change value is recomputed then there is no danger of inconsistency.

Example 2 is not pure, but then neither is Example 1. To be useful, software must be impure.

Also the condition "while program execution proceeds" is a can of worms: the operating system is always running, and any program we execute is a dynamically loaded sub-routine of the operating system. If we edit the code of example 1 and re-run it, we've just edited some data within a still-running program (the OS).

We can take advantage of the concepts of purity and immutability within our programs, but they are only ever islands sitting in an ocean of impurity and mutability.

chadrien commented 7 years ago

Thanks for the feedback. I tried to explain that more deeply in the post (hope you kept reading!) but maybe more explanation is needed. There is a sense in which render is pure, but you can't get to it just by looking at syntactic structure.

Consider stateless components. These are just functions that take props as an argument and it's easy to see that they are pure.

I agree with you that components using props only have a not so impure render (I won't say pure as the usage of this even if it don't produce side effects/updates any property like props isn't perfect, having props as parameters would be more close to a real pure function)

What makes render not pure from what you say in you blog post is that you mention state.

Take this component:

class Foo extends React.Component {
  constructor(props) {
    super(props);
    this.state = { todos: [] };
  }

  componentDidMount() {
    fetchTodos() // get todos asynchronously
      .then(todos => this.setState({ todos }));
  }

  render() {
    return <ul>{ this.state.todos.map(todo => <li>{ todo }</li>) }</ul>;
  }
}

This component's render will be called twice with the same arguments (none) but will return 2 different DOM trees, hence is not pure.

Similarly, imagine if React was designed differently so that the render method of a component class had the signature render(props, state) instead of accessing those things via this. It would then be obvious that it is a pure function of props and state. Again, no effective difference from the current design in terms of behaviour/correctness.

Yes, if React was coded in FP way, render would be pure. But it's not the case :) And yes, huge difference of design/behaviour: having the state being passed as parameter or accessing an object property that can be mutated from outside the function is a huge difference.

I don't see how example 1 and 2 are relevant when speaking pure functions, I may have not got you point there 😕

To be useful, software must be impure.

Of course, I never said the opposite. You can however have a program composed of pure functions and impure functions.

Also the condition "while program execution proceeds" is a can of worms

As we're talking React, "while program execution proceeds" means "while the user uses your JS application", i.e. the users clicks, browses your app, etc. (without browser reload of course 😃)

We can take advantage of the concepts of purity and immutability within our programs, but they are only ever islands sitting in an ocean of impurity and mutability.

Once again, I never said the opposite. I agree with you. Without side effects and I/O any application would be pretty much useless. However, we can take advantage of pure function as they make our application easier to test (if you know that a function will always return the same result from the same arguments it's easier to test than a function that returns Math.random()) and to code (again because when you call a pure function you know that you don't have to deal with weird cases where for some obscure reason the function would return an unexpected result).

I feel like your response is really defensive, I'm sorry if I made feel you like I was saying you're saying bullshit. Your post made mostly sense, I just wanted to say that I disagree with you regarding the specific point of render being pure because in my opinion (and to the opinion of others too, that's a subject I had the opportunity to discuss with coworkers) it's not pure as long as you use this.state in it (and you do talk about state in this paragraph).

danielearwicker commented 7 years ago

Please worry no further about my feelings! 😎 They are unaffected by this kind of discussion.

I agree with you that components using props only have a not so impure render (I won't say pure as the usage of this even if it don't produce side effects/updates any property like props isn't perfect, having props as parameters would be more close to a real pure function)

Right, so to make a slight transformation to your example:

  render() {
    return this.pureRender(this.state.todos);
  }

  pureRender(todos) {
    return <ul>{ todos.map(todo => <li>{ todo }</li>) }</ul>; 
  }

You agree that pureRender is pure, of course. But this is a mechanical transformation which could be performed by an algorithm on any well-written component. What I'm saying is that a machine can find the pure function in a render method! 😆 It's hiding in plain sight. If it's not there, the component would be broken.

chadrien commented 7 years ago

Yes of course, that's what I meant when I said that your entire code base can't be pure.

But I still think that this piece of code is better as when a new developer joins the project, he will see a function that always return the same result for the same arguments and will be confident about using it, rather than some magic function that may or may not give him the result he expects depending on external factors.

The whole point of pure vs not pure is not about writing code for the machine, or writing FP code because it's trendy. To me (I insist on this part, I may be the only one thinking that, though I hope not :3) the point of pure functions and any other kind of design patterns, programmation paradigm, etc. is to write code for humans, because I won't ever be the only one reading the code I'm writing and and want these other people to find the best usable code possible.

And that's why I think it's important not to mislead people that may not be 100% aware of what those techniques are. I know that you can tell what is a pure function and what is not, but think of the people who have no idea what it is, and won't have the desire to search for it on the Internet and then will just take what they just read for granted, and won't even ask themselves if maybe there might be subtle nuances about what can or cannot be a pure function, or a "less-pure function but still pretty pure", etc.

danielearwicker commented 7 years ago

Yes, absolutely - the number 1 priority is write code that others will be able to change without breaking. If all code worked exactly the same way there wouldn't be nothing to explain, but that is not the case of course. All we can do is explain the ideas. Often (as in this case) they are subtle.

For example, I glossed over something. pureRender is only pure under a certain assumption about fetchTodos: immutability of the array it returns. React itself doesn't require that the data is immutable, but (to be opinionated) that's the sensible way to use React.

My aim here is to see if there is something I need to add to the blog post that would avoid misleading - although I should stress that people who are aren't bothered to read around and get informed are not my target audience :smile: Don't let anyone like that near your code!

What I originally put was:

The render function of a React component is a pure function that returns a tree of lightweight objects describing the desired UI. How pure is it? Very, even though it doesn't look that way at first because it has no formal parameters. There are strict rules about what data it can depend on: props and state. React re-renders automatically if either of those change. If render depended on anything else that might change, the system breaks down.

What I am effectively saying here is that a React component has to contain, embedded within it, a pure function of state and props, under the assumption that the values in the properties of those two objects are immutable. Would adding something like that make it any clearer?