Settings

Theme

Method Combinators in CoffeeScript

github.com

64 points by grifaton 14 years ago · 25 comments

Reader

asolove 14 years ago

While the syntax here is CoffeeScript-specific, this is a perfectly fine pattern to apply in any JavaScript.

The key is to remember that, in JavaScript, you aren't really "defining instance methods" in the way you are in almost any other language. You're just passing anonymous functions as the value tied to some key in a dictionary, which happens to be the prototype of some set of objects. Once you get that into your mind and let it play around for a bit, you realize that you could be getting the anonymous function from anywhere!

From a function-factory, that takes some arguments and returns a function:

    render: renderWithTemplate("some_template")
From an anonymous function wrapped in something that controls its execution:

    onScroll: _.debounce(100, function(){ /* do something */ })
Or even by delegating to another object:

    viewInstance.onScroll = _.bind(somethingElse.render, somethingElse);
Thanks for the great post to help bend our minds around these possibilities.
  • Cushman 14 years ago

    > Or even by delegating to another object:

    > viewInstance.onScroll = somethingElse.render;

    This one is a bit dangerous, because as you mention the "methods" are just anonymous functions assigned to objects. If `render` is expecting to be called on a `somethingElse`, this might not work right without being bound.

    • Xcelerate 14 years ago

      That's why I never use "this" in Javascript. For me, dropping any pretense of OOP allows me to write more elegant programs.

      • notb 14 years ago

        I think that's a pretty poor practice. It's actually counter to central idea of this article; that you should think with the language features. In situations like the one above, if you dont want to lose the context of "this" you can explicitly bind an object to be this and return the new bound function. See underscore's bind(). Its a little more verbose and clunky but it saves you from tossing out modular code.

        • Xcelerate 14 years ago

          I'm not so sure it's poor practice. Rich Hickey makes an excellent point about not confounding data and functions. And Javascript provides excellent structures for avoiding mutability altogether (if one wishes to) thanks to first-class functions. Javascript was always an amalgam of C-syntax, object-oriented "ideas", and functional "ideas". To "think with the language features" is to tie to together two disparate programming paradigms that in my opinion perform better independently than in combination. So I just happen to focus more on the functional aspects of Javascript (and CoffeeScript provides syntactic support to make this much easier).

        • MatthewPhillips 14 years ago

          > See underscore's bind()

          Better, just use Function.prototype.bind, 0 bytes gzipped with Vanilla JS.

        • yesbabyyes 14 years ago

          And of course, in CoffeeScript you would just use the fat arrow:

              somethingElse =
                render: => # do some rendering
          • Cushman 14 years ago

            Careful, that's not the same as (->).bind(somethingElse). => binds to the current value of this at execution-- in that function, @ will compile to `var _this = this`, not `somethingElse` as we'd expect.

            I just tested and it looks like you can hack it by doing

              _this = somethingElse =
                render: => @renderMe # @ is somethingElse
            
            but that is definitely a bug.
            • raganwald 14 years ago

              I've written some transpilers, and I nearly always use a "magic variable" like __this_12345 just to prevent problems like--umm--this. If every single one you use has a quasi-random number or string appended, a lot of headaches melt away.

    • asolove 14 years ago

      Yeah, I apologize. I have changed it to read correctly.

goblin89 14 years ago

A little nitpick, which may be a misconception deserving clarification:

> Our decorators work just like Python method decorators, only we don't need any magic syntax for them because CoffeeScript, like JavaScript, already has this idea that functions can return functions and there's nothing particularly magic about defining a method, it's just an expression that evaluates to a function.

Python also has this idea (called ‘higher-order functions’). The difference is in syntax—function calls require parens in Python, and anonymous functions aren't that well supported. Therefore the need for special syntax construct to make decorator use convenient.

  • raganwald 14 years ago

    You're speaking to the first part of my claim, but not the second, namely that JavaScript and CoffeeScript don't separate the idea of a function and a method, which is what allows you to use first-class functional combinators as decorators.

    • masklinn 14 years ago

      > JavaScript and CoffeeScript don't separate the idea of a function and a method

      Neither does Python, at definition time. All the difference is in the processing performed by the class constructor (`type`) when the class object is created. Before that, it's a bog-standard function.

      > which is what allows you to use first-class functional combinators as decorators.

      Which is exactly like Python, the original decorators[0][1] predate the syntactic extension by several versions, the original syntax for applying them was:

          def some_method(cls):
              pass
          some_method = classmethod(some_method)
      
      And you could use the exact same syntax to define non-method functions (though not this decorator, of course, as it doesn't make sense for functions)

      [0] http://docs.python.org/library/functions.html?highlight=clas...

      [1] http://docs.python.org/library/functions.html?highlight=clas...

    • goblin89 14 years ago

      I only intended to comment on the first part. Decided against cutting the quote mid-sentence. (Which led to me learning something new from masklinn's comment. =))

raganwald 14 years ago

Jeremy Ashkenas provides the "tl;dr:"

Python decorators are a hack around the lack of proper lambda ;) Just pass the function: decorate -> … Where "decorate" is a fn

https://twitter.com/jashkenas/status/235012485009248256

  • jerf 14 years ago

    That makes no sense at all. The lambda complaint is mostly a syntax complaint, it is not a complaint about not having anonymous functions. It even has the ability to invoke __call__ on instances, allowing you to create your own "functions" that are actually objects, if you like, which is somewhat unusual.

    There's no point in having an anonymous function decorator. You'd simply inline it into the body itself right on the spot, anything else would simply be obfuscation for the sake of obfuscation. This only makes sense if you're using a decorator applied elsewhere, at which point all languages require you to have named it, so there's no point complaining about lack of anonymous functions here.

    Python has first-class functions. It even has first-class methods, with automatic instance binding. It just doesn't quite work how very functionally-oriented people want it to work. Part of the reason I don't like the kvetching that such people do is that it does seem to convince people that Python is lacking function references. Nope. It just doesn't spell them to everybody's taste (for instance, "it doesn't have Ruby blocks" translates to "it doesn't have block syntax", not "it can't do function references", and this is a taste issue not a fundamental capability issue), and the can rarely have moderately inconvenient scope rules if you want to write to outer scopes.

    Decorators are syntax sugar, not a new feature.

    • raganwald 14 years ago

      I don't know about those other complaints, I don't personally have any complaints about Python. It doesn't have multi-line lambdas, that's a design trade-off like an iPhone not having a slide-out keyboard. But it does have first-class functions.

      https://en.wikipedia.org/wiki/Python_syntax_and_semantics#Fi...

      What I was speaking to is the desire to write:

        class SomeExampleModel:
        
          def setHeavyweightProperty:
            triggers('cache:dirty')(
              lambda self, property, value:
                ...something...
      
      My understanding is that Python doesn't like two different things about this. First, the multi-line anonymous lambda being used as the target of the decorator. Second, a function being called with another function as its argument as an expression within an instance method definition.

      I'm open to reëducation.

      • masklinn 14 years ago

        > What I was speaking to is the desire to write:

            setHeavyweightProperty = triggers('cache:dirty')(
                lambda self, property, value: 'code')
        
        is the same thing as

            @triggers('cache:dirty')
            def setHeavyweightProperty(self, property, value):
                # code
        
        but with the limitations of lambdas (no statements allowed)

        > First, the multi-line anonymous lambda being used as the target of the decorator.

        Python has no issue with that, it's just hard to do it because Python's lambdas can only contain expressions which can be rather limited in a statements-heavy language.

        > Second, a function being called with another function as its argument as an expression within an instance method definition.

        That's because your "instance method definition" is not syntactically correct, the capability itself exists. A "method definition" is nothing more than a function defined within a class scope. In fact you can do things like that:

            >>> def method_outside(self):
            ...     return self.wedding
            ... 
            >>> class SomeType:
            ...     whose = method_outside
            ... 
            >>> s = SomeType()
            >>> s.wedding = "Amanda"
            >>> s.whose()
            'Amanda'
            >>> method_outside(s)
            'Amanda'
            >>> from collections import namedtuple
            >>> o = namedtuple('N', 'wedding')(wedding="Jim")
            >>> method_outside(o)
            'Jim'
            >>>
        • masklinn 14 years ago

          Can't edit, but "a function defined within a class scope" should actually be "a function assigned within a class scope", where and how the function object was defined does not actually matter (as the example shows), the only thing which matters is that the object is bound to a name in the class scope.

      • lost-theory 14 years ago

        First off, I really like your posts and books. I learned advice / AOP at a previous job and really enjoy using it and applying it to new things. Your writing on the subject is great!

        The only thing python does not like is multi-line lambdas. You can target a lambda with decorators, just like you can target any callable. I'm not sure if I correctly parsed what you mean by "a function being called with another function as its argument as an expression within an instance method definition", but I'm pretty sure python can do that. Here is some demonstration code (see RaganwaldWidgetViewThree):

        https://gist.github.com/3495990

        And finally here is the translation of the before/after/etc. method combinators:

        https://gist.github.com/3495985

        But I might have misunderstood waitLevel, can you explain what it is for?

    • asolove 14 years ago

      Just FYI, he's not talking about the decorator being anonymous, he's talking about the function the decorator is wrapping being anonymous. What he means is that, since python lambda's are one-line, you can't do something like:

          ifAuthorized("admin", lambda x:
            // more code here
      • masklinn 14 years ago

        > What he means is that, since python lambda's are one-line

        Technically, Python's lambdas are one-expression not one-line.

        Within that constraint you can create as complex lambdas as you want (you'll probably need to create helpers for things like loops, and you may be hindered by Python's limits on recursion, but FWIW Scheme's or Haskell's functions have the exact same limitation, the primary difference being they're nowhere near as statement-heavy as Python).

quarterto 14 years ago

In LiveScript: https://github.com/quarterto/homoiconic/blob/d6b78812c00eb4d...

  • ludicast 14 years ago

    Interesting, do like the "implicit currying". And the decorator motivations read better when they are expressed with the <| rather than when they are just "escaped line breaks".

Keyboard Shortcuts

j
Next item
k
Previous item
o / Enter
Open selected item
?
Show this help
Esc
Close modal / clear selection