Now that’s a nice ‘stache

dot-stacheThere was something that bugged me about Allen Wirfs-Brock‘s proposed “monocle-mustache” syntax for JS: it updates the properties of the object on the left-hand side, but it doesn’t look like it’s updating properties; it looks like some strange combination of creating a new object and accessing properties of the left-hand side:

this.{ foo: 17, bar: "hello", baz: true };

Then this week a couple things happened that got me thinking: Reg Braithwaite (aka @raganwald) posted his proposal for a CoffeeScript syntax to support the “fluent style” of programming, inspired by Smalltalk’s message cascades:

array
    .pop()
    .pop()
    .pop()

path
    .moveTo(10, 10)
    .stroke("red")
    .fill("blue")
    .ellipse(50, 50)

Next I saw Bob Nystrom‘s post about a cascade syntax proposal for Dart, which is a small variation on Allen’s monocle-mustache:

document.query('#myTable').{
    queryAll('.firstColumn').{
        style.{
            background = 'red',
            border = '2px solid black'
        },
        text = 'first column'
    },
    queryAll('.lastColumn').{
        style.background = 'blue',
        text = 'last column'
    }
};

This is really just a tiny tweak to Allen’s original syntax, but it makes a world of difference: it uses the = sign for assignment. Much clearer!

Now, assignments with commas don’t really look like JS. But since the point of cascades is to do imperative sequencing — i.e., to ignore the result of intermediate message sends and do each message send on the original object from the left-hand side — it makes perfect sense to use a statement-like syntax:

array.{
    pop();
    pop();
    pop();
};

path.{
    moveTo(10, 10);
    stroke("red");
    fill("blue");
    ellipse(50, 50);
};

this.{
    foo = 17;
    bar = "hello";
    baz = true;
};

Even sweeter, JavaScript’s automatic semicolon insertion kicks in and lets you do this very concisely:

array.{
    pop()
    pop()
    pop()
};

path.{
    moveTo(10, 10)
    stroke("red")
    fill("blue")
    ellipse(50, 50)
};

this.{
    foo = 17
    bar = "hello"
    baz = true
};

What’s so great about a cascade syntax is that when you want to do imperative programming on the same object, you don’t have to rely on the API creator to return this from every method. As the client of the API you don’t have to care what the method returns, you throw away the result anyway.

In fact, this is one of the things I don’t like about method chaining in JS today: you can’t actually tell whether the method is mutating the current object and returning itself or producing some entirely new object. And when you mix the two styles, it gets even blurrier. Look at Bob’s example from jQuery:

$('#myTable')
    .find('.firstColumn')
        .css({'background': 'red',
              'border': '2px solid black'})
        .text('first column')
    .end()
    .find('.lastColumn')
        .css('background','blue')
        .text('last column');

You just have to know that .find() produces a new object, and .css() and .text() modify it and produce the same object, but nothing about the syntax tells you this. (And personally, I always feld that .end() was where jQuery’s API jumps the shark.)

With a cascade syntax in addition to normal method calls, you can so much more easily distinguish when you’re doing something to the same object, and when you’re selecting new objects:

$('#myTable').{
    find('.firstColumn').{
        style.{
            background = 'red'
            border = '2px solid black'
        }
        text = 'first column'
    }
    find('.lastColumn').{
        style.background = 'blue'
        text = 'last column'
    }
};

Gist here. These are just my initial thoughts; I’ll have to work on fleshing out a full proposal, including the full grammar spec.

23 Responses to Now that’s a nice ‘stache

  1. Since there are places that semicolons are required, relying on automatic insertion is asking for trouble and shouldn’t be encouraged!

  2. In your final jQuery example (using your new syntax) you forgot that jQuery returns a collection so you would have to access `find(‘.firstColumn’)[0].{…}` also the element returned probably doesn’t have a `text` property.

  3. Might consider binding this on the RHS to the expression on the LHS of monocle-moustache.

  4. Jacob Swartwood

    My vote is for the syntax at the top of the article… I think comma and colon make it clear that you are adjusting properties directly on the object; using semicolons and calling methods immediately makes me think of creating a new implicit scope (it feels too much like the `with` operator).

    I worry about confusion in scenarios like:

    path.{
    x = 3
    y = 4
    paint(x)
    paint(line())
    };

    Should/would anyone assume that `x` or `line` is also an alias to `path.x` or `path.line`?

    Thinking of .{ as .extend({ and keeping syntax to match that makes the most sense to me.

  5. Steven Roussey

    Funny! What’s old and discarded is new again?

    this.{
    foo = 17;
    bar = “hello”;
    baz = true;
    };

    OR

    with(this){
    foo = 17;
    bar = “hello”;
    baz = true;
    };

  6. Yeah, sorry, that was kind of a mish-mash of jQuery and Bob’s examples from Dart.

    Anyway, IMO the important part is that when you’re doing something for side effect only, whether it’s a property assignment or a method that internally updates one or more objects, it gets separated by a semicolon, so it’s clear to the reader that it’s side-effecting. So it doesn’t really matter whether the example uses assignment or method call.

  7. @Steven:

    There’s a huge difference between the two. The with block is an arbitrary statement that adds this to the scope chain. The monocle-mustache is not a statement, just a bunch of fragments of property assignments, and the scope is unchanged.

    The whole reason with sucks is the dynamic scoping behavior.

  8. Interesting.

    There is not much gain when using method calls — obj.{ foo(); bar(); baz() } vs. obj.foo().bar().baz() — but it sure looks better with assignments.

    Reminds me of `with` statement. I guess we can think of it as pretty much `with`’s syntax sugar? (except that property lookup probably wouldn’t go beyond LHS object, unlike `with` which proceeds with scope chain after not finding anything in LHS object (and its prototype)) :)

    this.{
    foo = 17;
    bar = “hello”;
    baz = true;
    };

    //compare to:

    with (this) {
    foo = 17;
    bar = “hello”;
    baz = true;
    }

    Or is there more to it?

  9. Robbert Broersma

    @dherman: We’re talking syntax here, so there isn’t a huge difference. They’re remarkably similar.

    And do you really think Steven didn’t know about the scoping behavior? Appreciate those who care to comment.

  10. @kangax:

    Re: method calls: the big difference is that it works regardless of whether the API you’re using returns this. So you don’t have to rely on API’s being method-chaining-aware.

    Re: with: this is totally different because it puts an object on the scope chain and uses scope resolution. Compare:

    var foo = “hello”;
    this.{
    x = 17;
    y = foo;
    };

    and

    var foo = “hello”;
    with(this) {
    x = 17;
    y = foo; // whether this is foo above or this.foo depends on whether `”foo” in this`!
    }

    Also, your with code only works if this already has those properties. Otherwise it creates globals. The monocle-mustache just creates properties of the LHS object, no matter what. No scoping semantics involved at all.

  11. @Robbert:

    Well, we’re talking syntax and semantics, and it wasn’t clear whether Steven was criticizing the proposal or not, so I thought it was worth making that clear. I certainly didn’t intend to insult — I absolutely do appreciate comments. Keep ‘em coming!

  12. So if only property assignments are allowed in mustache blocks, then ConditionalExpression’s won’t cut it?

    this.{
    foo ? bar() : baz();
    boo ? foo() : qux();
    }

  13. I didn’t talk about this in the post, but I think it’s also worth considering computed properties as well, using a syntax analogous to the syntax we’re adding to literals.

    So computed property assignments would look like this:

    this.{
    [foo ? "bar" : "baz"] = 17;
    [boo ? "foo" : "qux"] = “hello”;
    };

    and maybe computed method names too:

    this.{
    [foo ? "bar" : "baz"]();
    [boo ? "foo" : "qux"]();
    };

    I’m not 100% sure what I think of these; I don’t think it’s necessary to support every possible bell and whistle. Monocle-mustache is intended as a convenience form, but it wouldn’t do anything you can’t already do. So I’d rather have it do what it does well, without getting too ornate.

  14. Sounds like it actually really wants to be with(); if you merge the first example and your proposed syntax, you get something unambiguous (though a little wordy):

    array.{
    .pop()
    .pop()
    .pop()
    }
    path.{
    .moveTo(10, 10)
    .stroke("red")
    .fill("blue")
    .ellipse(50, 50)
    }
    this.{
    .foo = 17
    .bar = "hello"
    .baz = true
    }

    (I’m assuming automatic semicolon insertion here to match your examples; I’d personally prefer to be explicit, though.) Yes, that’s basically copying the VB with syntax – but hey, if it manged to do something right, why not learn from it? This way we can use other variables too:

    this.{
    alert("dimensions are " + .width + " x " + .height);
    }

    (Let’s assume the deepest mono-stache wins when you nest them, or something…)

  15. @Mook:

    I suggested a reformed with using the prefix dot-notation like your suggestion a long time ago, and it has a really bad interaction with semicolon insertion: notice how in the assignments in your example, they end with an expression, and the next line starts with a dot? That means that there’s no inserted semicolon! Semicolons are only inserted if it can’t parse, and 17.bar and “hello”.baz parse just fine.

  16. Just came here to say hooray for nattybo! \o/

  17. This looks like it could be extended into a sane method for defining prototypes. Currently we can either do something like

    function foo(bar) {
    this._bar = bar;
    }
    foo.prototype = {
    neg: function neg() {
    this._bar = -this._bar;
    },
    get bar() {
    return this._bar;
    }
    };

    but that destroys properties on the existing prototype, such as foo.prototype.constructor, which is bad for potential inheritance, or

    function foo(bar) {
    this._bar = bar;
    }
    foo.prototype.neg = function neg() {
    this._bar = -this._bar;
    };
    foo.prototype.__defineGetter__("bar", function() {
    return this._bar;
    });

    which is longwinded despite using SpiderMonkey-specific syntax. Instead I would like to see this:

    function foo(bar) {
    this._bar = bar;
    }
    foo.prototype.{
    function neg() {
    this._bar = -this._bar;
    }
    get bar () {
    return this._bar;
    };
    };

  18. In my opinion this code looks really ugly :( I hope at least that backwords compatibility is kept if this code is implemented.

  19. @Neil:

    Definitely useful for prototypes. For regular methods, I would just use assignment instead of mixing it up with function declaration syntax:

    foo.prototype.{
    neg = function() { this._bar = -this._bar; }
    };

    I could see adding a get syntax. Interesting thoughts, definitely worth considering. Thanks!

  20. @allesio:

    I agree that it can be abused, but I feel the same way about excessive amounts of method chaining. IMO, this simply provides a clearer way of doing what people currently do, but distinguishing when you’re using method chaining for side effect (with a method that returns this), and when you’re using it to get a new value. But if you don’t like the style of lots of nested method chaining, you certainly don’t have to use it. Taste in programming style is always going to vary.

    As for backwards compatibility, you bet — we are always vigilant about compatibility.

  21. I did consider assignment syntax but I couldn’t see how that would extend to getters and setters.

  22. @Neil:

    Fair enough — good food for thought.

  23. Another good solution would be to FIX the ASI to automatically insert a semicolon EXCEPT if the statement on current line doesn’t make sense (there’s a parenthesis open, it ends with an operator, …) OR if the next line starts with a binary operator OR a dot.

    This is the way VB.NET does it, I really like it. I almost never have problems when using Autmatic Line Continuation (and, BTW, most statements are written in one single line).

    var a = function() { // doesn't make sense, continue to next line
    // ...
    } // makes sense; the next line doesn't start by operator or dot -> stop parsing statement and create a new one
    (a || b) // make sense, but next line starts with a dot
    .doSomething() // make sense, but next line starts with a dot
    .doSomethingElse()

    Current ASI behavior is broken. We seriously need to fix that a day or another.