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

Help people distinguish between methods and helpers #166

Closed
Rich-Harris opened this issue Dec 8, 2016 · 11 comments · Fixed by #473
Closed

Help people distinguish between methods and helpers #166

Rich-Harris opened this issue Dec 8, 2016 · 11 comments · Fixed by #473
Milestone

Comments

@Rich-Harris
Copy link
Member

It's not totally obvious what the distinction is between methods and helpers:

<button on:click='myMethod()'>{{myHelper(foo)}}</button>

<script>
  export default {
    methods: {
      myMethod () {
        alert( 'called a method' );
      }
    },

    helpers: {
      myHelper ( x ) {
        return x.toUpperCase();
      }
    }
  };
</script>

While we could do a better job of explaining in the docs (a helper is a pure function that transforms its inputs i.e. it returns something, whereas a method has side-effects i.e. it does something), we could do one better: we could identify cases where someone was trying to call a helper function in an event handler or trying to use a method in a {{tag}} and print the appropriate warning at compile time.

@aharpervc
Copy link

The guide says "Helpers are simple functions that are used in your template." If that's the only distinction between methods and helpers, why not merge the concepts? What is gained by treating them separately?

@Rich-Harris
Copy link
Member Author

They're conceptually very different things. Methods become part of the public API...

import MyComponent from './MyComponent.html';

var component = new MyComponent({...});
component.doCoolThing();

...whereas helpers are only available in templates, and behave differently (this has no value). The only thing they really have in common is that they're functions – the relationship between helpers and methods is basically the same as the relationship between helpers and lifecycle hooks (onrender and onteardown). Someone reading a component's source code will be able to understand what everything does more quickly if we retain the distinction.

@aharpervc
Copy link

Component methods that are part of the public api also are/could be functions (in both the sense that they are Functions and that they may possibly be implemented as a "pure" transformation, such as if they ignored their own this binding and only referenced their parameters). Therefore, I don't see why it is useful to partition them from helpers, and I'm not convinced that the distinction isn't arbitrary. I suspect you will counter that they need to be partitioned for implementation reasons, ie, you wouldn't want to make ALL component methods available in your templates for aesthetic, philosophical, or performance reasons.

I assume that you would think it's completely acceptable to implement helpers as component api methods, and then reference them as helpers, such as in your guide example for helpers;

 export default {
    helpers: {
      // from the guide
      leftPad,
      // hypothetically, a helper which is also a method
      doCoolThing,
    },
    methods: {
      doCoolThing: function() {
        return "cool thing";
      },
    }
    // etc
  }

Additionally, the guide example for event handlers kind of reinforces my point; doing <button on:click='select( category )'> where select is a component api method feels fine, but then I could apparently not do <button ...>{{ select("default") }}</button> without also setting up my select method as a helper? You're making me do bookkeeping that I shouldn't have to do. If someone has to set up select as a helper, at least make the compiler do it. Ie, don't warn about it, just set it up that way.

I'm not sure what lifecycle hooks have to do with this. Those are special because their names are reserved and they are typically not manually invoked. I'm certainly not asking to be able to manually invoke lifecycle hook methods from a template (I would also say, it'd be weird to invoke lifecycle hook methods manually from other normal component methods). The difference is, I implemented methods on my component to allow my component to do something, but in order to do that thing from a template, I have to either declare the method as an event handler function, or cross reference it from my helpers object.... and now I'm doing bookkeeping I feel is unnecessary.

I'm also trying to think about it from the other angle, where a function is a helper only and not a method. That's your guide example case for helpers, and I do see how it is useful to expose a utility pure function to a template without absorbing it into your component public api. I think that is your strongest argument to continue explicitly partitioning helpers. However, that doesn't feel like a dominant scenario. I could be wrong, especially if people are constantly pulling in helper utilities as your guide example suggests.

@Rich-Harris
Copy link
Member Author

Additionally, the guide example for event handlers kind of reinforces my point; doing <button on:click='select( category )'> where select is a component api method feels fine, but then I could apparently not do <button ...>{{ select("default") }}</button> without also setting up my select method as a helper?

This is as clear a demonstration of why we need the distinction as you could invent 😀

The click handler is doing something – when you click the button, you select the category. Or you could programmatically do...

component.select( 'someCategory' );

...in your app. {{ select("default") }} is absolutely not the same thing. It's an expression that returns some text – the text that is displayed on the button – and the point of a template-based framework like Svelte is that the framework calls that function whenever it suspects that it might need to update the DOM, rather than at specific predictable moments (i.e. when you call the method, or the click event takes place).

Because of that, it's important that the helper not have side-effects, whereas the whole point of methods is to have side-effects (or to return a value that's determined by the state of the object to which they belong). If helpers have side-effects, your app will behave unpredictably, which is why this warning appears in the docs:

screen shot 2016-12-09 at 6 27 26 pm

@fskreuz
Copy link

fskreuz commented Dec 9, 2016

a helper is a pure function that transforms its inputs i.e. it returns something

Just breezed through the thread. Isn't this a "computed property" in Ractive speak? I also view computed props as "a way to represent data in another way without creating another entity to represent it", should that be of help in clarifying.

In Angular, I believe they're called filters:

Filters format the value of an expression for display to the user.

https://docs.angularjs.org/guide/filter

Syntactically different, but sort of works the same way (value in, value out). Not sure if "filter" is a better name than helpers though. "filter" to me is like array.filter. But the pipe-style syntax does make it look familiar.


Helpers: Format data for display.

Methods: Mutate data, call events, call APIs, do rendering, operate the DOM, etc.


How about "transformers"?

My 2c 😄

@Rich-Harris
Copy link
Member Author

Helpers: Format data for display.

Methods: Mutate data, call events, call APIs, do rendering, operate the DOM, etc.

Yeah, we should have something like that in the docs – that's a good way to clarify the distinction.

Svelte has computed properties which are very similar to Ractive's computed properties (except that the dependencies are declared as function parameters and resolved statically, rather than at runtime using the this.get(...) dependency tracking machinery – means we can do a simple toposort and update computations very efficiently. The parameter thing is a little bit magical but the ergonomics and simplicity make it oh-so-worth-it 😀 ).

helpers is an extension of data, in a sense – it's separate because I found a lot of people were a bit weirded out by having functions on data in Ractive. If you're not one of those people then you can basically do without helpers altogether (put helpers on data), but I've been living with helpers for the last couple of weeks and I think I prefer it. It feels cleaner.

(BTW: I loathe the Angular filter syntax – {{ expression | filter:argument1:argument2:... }} is hot garbage!)

@proyb6
Copy link

proyb6 commented Dec 10, 2016

I would find expression: or magic: for {{ ... }} it sound clearer on first read.

@aharpervc
Copy link

the point of a template-based framework like Svelte is that the framework calls that function

I understand what you're going for now. In your jargon, "helpers" are code that, like lifecycle hooks, will be invoked by the framework internally, whereas component api methods will be invoked by external user events or other component code. I think coming at it from the invocation angle is more enlightening than coming at it from the "no no, they're pure functions" angle.

Even Ractive computed properties can have side effects. Probably shouldn't, but technically can. Similarly with Svelte, it sounds like helpers don't actually need to be pure. That's merely an architectural recommendation rather than a declaration on why helpers need to exist separately from component methods. Especially since helpers could be coming from external libs, where who knows what's going on.

However, even given this, I still believe there's room for improvement. Lifecycle methods are declared on the component, same as Ractive and it all works because people memorize what the right method names are. Why is it that lifecycle hooks okay to simply define on the component, but helpers need to be called out separately, when to you they're more similar than different? Again this just gets back to my original point that it feels like unnecessary user bookkeeping.

@Rich-Harris
Copy link
Member Author

Why is it that lifecycle hooks okay to simply define on the component, but helpers need to be called out separately, when to you they're more similar than different?

What I said was that helpers have the same relation to lifecycle hooks as they do to methods – i.e, none whatsoever! They're three very different things that just happen to all be functions 😀

Bunging everything together at the top level might sound like it makes your life as a component author easier, but I really think those gains are illusory and temporary. It's a higher priority for components to be easy to read than to write (because that's true of all code) – it's not book-keeping, it's organisation, just like keeping (say) components and events separate. Happily, a better-organised and easier-to-read component is also easier to statically analyse.

@Ryuno-Ki
Copy link

Hm, I wonder how helpers and methods compare in terms of optimisation. As far as I know, an engine interpreter will likely compile often called (or “hot”) functions to byte code/machine code. A pure function would be easier to highly optimise.

Since a template helper would be called often, it makes sense for it to be pure.

From a template authoring point of view I can tell, that it makes it easier if templates stay dumb and business logic is moved elsewhere (be it model/view/controllower or viewmodel etc).

@aharpervc
Copy link

Bunging everything together at the top level might sound like it makes your life as a component author easier, but I really think those gains are illusory and temporary. It's a higher priority for components to be easy to read than to write (because that's true of all code) – it's not book-keeping, it's organisation, just like keeping (say) components and events separate. Happily, a better-organised and easier-to-read component is also easier to statically analyse.

In that perspective, should lifecycle hooks be a separate top level key, like helpers?

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

Successfully merging a pull request may close this issue.

5 participants