masak / alma

ALgoloid with MAcros -- a language with Algol-family syntax where macros take center stage
Artistic License 2.0
139 stars 15 forks source link

Implement Smalltalk-style method chaining syntax #221

Open masak opened 7 years ago

masak commented 7 years ago

Courtesy of @arnsholt. Slightly made-up but complete example:

my employee = {
    name: "Frank",
    age: 44,
    role: "Senior Janitor",
    printName() {
        say(employee.name);
    },
    printAge() {
        say(employee.age);
    },
    printRole() {
        say(employee.role);
    }
};

employee
    ..printName()
    ..printAge()
    ..printRole()
;

That is, even though those printMumble methods don't explicitly return the employee object (or some this equivalent), the .. call syntax allows them to be used as if they did.

Here's a possible implementation:

macro postfix:<..>(object_ast, identifier_ast, parameterlist_ast)
    is parsed(/ ".." <identifier> "(" <parameterlist> ")" / {

    return quasi {
        {{{object_ast}}}.{{{Q::Identifier @ identifier_ast}}({{{Q::ParameterList @ parameterlist_ast}}});
        {{{object_ast}}};
    };
}
arnsholt commented 7 years ago

Looks good to me. Only quibble I have is on the macro implementation: if object_ast is something that performs a destructive operation (say, getting the next item off a queue or something), wouldn't the macro end up performing many of them, rather than just one?

arnsholt commented 7 years ago

Oh, another thing. In ST invocant m1; m2; m3 has the return value of m3, not the invocant itself which is what we get with this macro.

masak commented 7 years ago

Only quibble I have is on the macro implementation: if object_ast is something that performs a destructive operation (say, getting the next item off a queue or something), wouldn't the macro end up performing many of them, rather than just one?

Yes, you're right. More generally, with quasis in macros, whenever one is using the same value multiple times, one always wants to capture, with no exception that I've found so far. (Later edit: This is now discussed in #234 and #479.)

Ok, here's a new implementation that does that:

return quasi {
    my object = {{{object_ast}}};
    object.{{{Q::Identifier @ identifier_ast}}({{{Q::ParameterList @ parameterlist_ast}}});
    object;
};
masak commented 7 years ago

Oh, another thing. In ST invocant m1; m2; m3 has the return value of m3, not the invocant itself which is what we get with this macro.

Hm, you're right. The last method shouldn't return the invocant, because that's clearly useless, just like it's useful in earlier places in the chain.

Let's see... postfixes nest like this: ((employee..printName())..printAge())..printRole(). The corresponding macros get called innermost-first, that is, from left to right. Which means that what a postfix:<..> macro should do is not codegen its own method call, but rather tweak the previous method call, if any, to return the invocant.

Under that scheme, ..printRole() which is called last ends up tweaking ..printAge(), but nothing ends up tweaking ..printRole() so it ends up returning what it usually does (None).

Yes, this scheme is more ambitious, but I like it better, I think.

vendethiel commented 7 years ago

Hm, you're right. The last method shouldn't return the invocant, because that's clearly useless, just like it's useful in earlier places in the chain.

Yup. In ST, if that's actually what you want, you can just call the identity method, i.e. invocant m1; m2; yourself

arnsholt commented 7 years ago

Yeah, for maximum manipulexity you want to be able to choose between the invocant and the returned value. In ST it's solved by the chaining operator returning the last return value and having a method that just returns the invocant (called yourself actually, not self. self is the reserved word containing the invocant of a method).

masak commented 7 years ago

Smalltalk's syntax is different than the one I propose in OP. It uses semicolons and periods.

I think I might have gotten the .. syntax from LiveScript cascades. Who knows how long ago I read that. :smile:

masak commented 3 months ago

Dart also uses ..:

String s = (new StringBuffer()
..add('Jenny ')
..add('I ')
..add('got ')
..add('your ')
..add('number')
).toString();

Additionally, as this example shows, the entire cascade does return the original invocant (in Dart).

I was going to write something about how the postfix .. syntax clearly clashes with the infix .. range syntax. (According to the current/growing spec, range .. is a built-in syntax... although this might change before the dust settles; it's clearly a candidate for throwing out into a module.) But then I thought, everything's fair if you predeclare, and the world is clearly large enough for two different features with conflicting syntax. Something similar has been discussed earlier in the issues with postfix ++ for destructive increment vs infix ++ for nondestructive array concatenation.