lvalue function = rvalue

I've fairly often found myself writing code sort of like:

for (let dog of dogs) {
  superlative[dog.breed][dog.age] = 
    Math.max(superlative[dog.breed][dog.age], dog.value);
}

which really is awfully repetitive, in a bug-attracting sort of way. It would be nice to write it as

for (let dog of dogs) {
  superlative[dog.breed][dog.age] Math.max= dog.value;
}

It's a generalization of += who's meaning should be clear to people reading the code even if they're not familiar with the feature.

It's basically syntactic sugar, except that if superlative has a getter with side effects, that will only be called once.

There is one little edge case where this could cause ambiguity: foo(bar)[baz]=qux if foo is a function and both bar and foo(bar) are objects with baz as a key. While we could add syntax to avoid this, I propose simply declaring that this unlikely construct is to be parse the existing way and if you really wanted the other, you could write foo((bar)[baz])=qux. Assuming you needed to keep bar in parenthesis in the first place.

If functions were allowed to return lvalues directly, the ambiguity issue would be worse, but I don't think anyone wants that.

One issue I see right off the bat is the fact that it only supports functions that takes two parameters, but what if your function took three? Or only one, and simply transformed the value?

Actually - if we think about the general problem you're trying to solve here, we're just trying to take a value, transform it somehow, and reassign it back where it came from, so finding a syntax that supports just a single parameter might make more sense (you can always create an arrow function to turn a multi-param function into a single-param one).

Here's an example syntax that could achieve the same objectives, but in a more flexible way:

const increment = x => x + 1

thing[x][y] updateWith increment
superlative[dog.breed][dog.age] updateWith x => Math.max(x, dog.value)
user.groups updateWith g => processGroups(g, 2, false)
myArray updateWith x => x.filter(y => y > 2).map(y => y + 1)

The value on the left of "updateWith" is passed to the function on the right, and the returned value is reassigned back to the left.

(I'm not necessarily a big fan on what I'm proposing, but I do think it's an improvement over the current idea)

I guess another alternative would be the "pipe assign" operator. Just like you have +=, -=, etc, you could also have pipe-equals (|>=), based off of the proposed pipeline operator.

// F#-style pipes
x.y |>= z => Math.max(z, 0)
// Hack-style pipes
x.y |>= Math.max(%, 0)

I didn't really consider nonbinary functions because in practice it's always been binary ones where I wanted this. Can you think of a case where non-binary functions would come up, and wouldn't be elegantly addressed with compact currying?

I think it's worth the loss of flexibility for the readibility advantage. I really think someone unfamiliar with the feature could read x max= y and understand it, whereas even with context I had trouble following your examples.

Change who's turn it is in a game:

app.game.state.whosTurn = (app.game.state.whosTurn + 1) % PLAYER_COUNT
// vs
app.game.state.whosTurn |>= x => (x + 1) % PLAYER_COUNT

toggle pause state

app.game.state.paused = !app.game.state.paused
// vs
app.game.state.paused |>= x => !x

etc.

That's debatable - it took me a little bit to figure out your syntax too - when I see something Math.max= somethingElse, I see the "Math.max=" part and automatically recognize that as assigning somethingElse to Math.max, but then I notice you've also got another term to the left of Math.max, and my mental model breaks. Without context, I probably would have no idea what was going on either :).

I also think there will be more ambiguity issues than you realize with this kind of syntax as well. From what I've seen, the committee has generally tried to stay away from assigning any meaning to anything of the form "<expression> <whitespace> <expression>", as it closes off a huge region of potential expansion for the language. For example, consider our now-not-so-new ability to declare variables with "let". Let isn't actually a reserved keyword (i.e. you can do var let = 2), but they were able to add it in anyways, because they were able to rely on the fact that let x = 2 would normally be a syntax error. If your proposal came first, then let x= 2 would already have meaning, and they wouldn't have been able to introduce let bindings in their current form.

I found that fairly understandable. Perhaps because I've learned to see operators as syntax sugar for functions. You know that a += b means a = a + b which is sugar for a = ADD(a, b). You can generalize to a BinOp= b means a = BinOp(a, b).

That being said, the grammar implications you described are huge. Not worth it, imo.

Absolutely. I've been counting on that becoming a thing when/if pipes are added. Whenever I see/write an unnecessarily verbose .reduce, I wish we already had |>= ;)

1 Like

Could be a use case for GitHub - rbuckton/proposal-refs: Ref declarations and expressions for ECMAScript combined with pipeline.

(ref superlative[dog.breed][dog.age])
  |> (%.value = Math.max(%.value, dog.value));