The idea is to provide a new ->
"operator", that conceptually serves a similar purpose as a "." in a fluent-API, however, it's given a special super-power. The initial function in the chain will be allowed to see everything that happens in the chain of ->
, and is in charge of executing everything in the chain. In reality, the ->
syntax is just a simple syntax sugar that decomposes into an array of tuples.
Here's an example.
// With the `->` operator
let data = beginAnimation({ duration: '10ms' })
-> translateY(obj1, { from: -100, to: 0, })
-> translateY(obj2, { from: -100, to: 0 })
-> grow(obj3, { factor: 2, duration: '20ms' });
// The arrows get transpiled down to a list of tuples, like this:
let data = beginAnimation({ duration: '10ms' }, [
['translateY', (obj1, { from: -100, to: 0, })],
['translateY', (obj2, { from: -100, to: 0 })],
['grow', (obj3, { factor: 2, duration: '20ms' })],
]);
Use Cases
First and foremost, this proposal would be capable of replacing the wavy dot proposal, with a more general-purpose operator. This is the primary reason I'm proposing this - because, to me, it seems like it accomplishes the same primary objective as wavy-dot, but with additional, useful perks. (unless I'm mistaken - the information on that proposal is very slim, so it's a little difficult to fully understand what they're trying to achieve and why).
The wavy-dot proposal allows you to write out a series of async operations, one after another, and then allow some underlying machinery to examine those operations and send them off in parallel, instead of in series, when its possible to do so.
Here's an example of what you might do in the wavy-dot proposal, taken from this issue:
disk~.openDirectory('foo')~.openFile('bar.txt')~.read();
These look like operations that happen, one after another, but in reality, all three operations are being sent to the server in parallel, asking for the server to try and run the whole chain at once.
The same can be accomplished with the future-seeing-arrow, like this:
await disk.openDirectory('foo')
-> openFile('bar.txt')
-> read();
As a more concrete example, let's take the transaction operation from this Redis package. It allows you to build up a chain of commands that get sent to the Redis DB in one go, for Redis to execute the whole thing as one atomic action.
const [setKeyReply, otherKeyValue] = await client.multi()
.set('key', 'value')
.get('another-key')
.exec();
This seems to be the exact use-case that wavy-dot is built for.
const [setKeyReply, otherKeyValue] = await client.multi()
~.set('key', 'value')
~.get('another-key')
But you could just as easily use ->
.
const [setKeyReply, otherKeyValue] = await client.multi()
-> set('key', 'value')
-> get('another-key')
The problem with wavy-dot, is that it's restricted to a very narrow use-case, which is the use-case of bundling async operations. If we broaden it, so it can also work with sync operations, we open up many more possibilities.
Here's an example use-case, where we're building up a regular expression (I borrowed this example from the block params proposal, and tweaked it to use the future-seeing arrow instead).
let re = regex()
-> start()
-> then("a")
-> then(2, "letters")
-> maybe("#")
-> oneof("a", "b")
-> between([2, 4], "a")
-> insensitively()
-> end();
Or, say you want to perform a number of different operations, but if you can tell from the start that one of them will fail, you don't want any of them to execute.
// Doesn't do anything if one of these actions will cause
// the robot to hit a wall.
const success = moveRobot()
-> forward()
-> forward()
-> turnRight()
-> forward();
Or, think about .flatMap()
. One large reason this function exists, is because it's more optimized than a .map()
followed by a .flat()
. If only the .map()
could see the future and see that a .flat()
would happen next...
array.map(x => ...)
-> flat()
Why, there's all sorts of extra optimizations we could do on arrays, if those functions could see the future. For example, we could perform the following chain of actions, without ever creating intermediate arrays.
array.map(x => ...)
-> filter(x => ...)
-> filter(x => ...)
-> map(x => ...);
Alternatives
There are alternative patterns that could be used today. For example, we could simply use the transpiled list-of-tuples in any of these scenarios. But, there's a reason we don't see APIs built around that sort of pattern often - it's ugly to use. I mean, which would you prefer?
// Fluent API
$('#myId')
.attr('x', 'y')
.attr('a', 'b');
// List of tuples of commands
$('#myId', [
['attr', 'x', 'y']
['attr', 'a', 'b']
]);
There's also the option of using a fluent-API anyways, letting the fluent API build up a list of commands, and then wait for a .end()
/.exec()
before executing them all. This is a fairly common pattern - just be careful you don't forget the .exec()
though.
await disk.openDirectory('foo')
.openFile('bar.txt')
.read()
.exec();
I think there's a reason the wavy-dot proposal exists, and it's because developers need something a little more user-friendly to use than either of the above solutions.