The "future-seeing-arrow" proposal.

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.

I can try to get more in details tomorrow, but this wouldn't work as a "superset" of wavy dot.

In particular, the "root" may not be always be a function call, bit can also be the result (handled promise) of a previous eventual send.

Aka you need to be able to write both:

const res = disk~.openDirectory('foo')
  ~.openFile('bar.txt')
  ~.read();

And:

const dir = disk~.openDirectory('foo')
const res = dir ~.openFile('bar.txt')
  ~.read();

Note also how disk is actually the target of the first wavy dot / eventual send, unlike in your rewrite which assumes it's a regular local method call.

In any case, making the first function call in a chain be special seem like a composability hazard.

In this version:

const dir = disk~.openDirectory('foo')
const res = dir ~.openFile('bar.txt')
  ~.read();

At what point are we sending out the request to the server? Is the read() function special, in that it's what's triggering the request to be sent?

This is somewhat similar, mechanical but not visually, to Type-safe builders | Kotlin Documentation which uses an implicit immediately invoked callback so values can be sent 'backwards' to the call above/before them

1 Like

The receiver may be able to handle/trap the ~., in this case disk and dir can both trap a wavy-dot "call". If the receiver isn't able to trap, the behavior falls back to performing Promise.resolve(disk).then(disk = > disk.openDirectory('foo')). As mentioned in the summary of the proposal, it's syntactic sugar for the eventual-send proposal.

The pipelined sends are performed as soon as the ~. is triggered (or however the trap implementation decides). There is no special knowledge or behavior for previous or future calls. It's basically the same as a regular call chain that synchronously returns values. It just happens these return values can handle wavy-dot traps.

Thanks for the info - I really should have researched this wavy-dot arrow proposal some more before posting :) - the idea I presented has been mulling around in my head for a little while, and recently I made the connection that it seemed to be a more powerful version of my previous (and incorrect) understanding of wavy-dot+eventual-send.

I am, however, still struggling to understand what the eventual-send proposal is actually providing. So, I'm sure I'll be back with some more clarifying questions on it, but I'm wanting to read through it a couple more times to try and get a better handle on what it's trying to convey.

And, maybe I'll move my questions to the eventual-send or wavy-dot repo, so the questions are more visible for anyone else to see, who may also have gaps in their knowledge.

Admittedly those proposals are not very active right now. We have however been using an eventual-send shim in production for some time now, with a helper to emulate wavy-dot:

const dir = E(disk).openDirectory('foo');
const res = E(E(dir).openFile('bar.txt')).read();

With pipes, it'd look a little better:

const res = E(disk).openDirectory('foo') 
  |> E(%).openFile('bar.txt')
  |> E(%).read();