Proposal: reverse application operator

Most functional languages have a reverse application operator often denoted as |>.
With it, instead of writing this:

var x = f(a, b, c)
var y = g(x, d)
var z = h(b, y)
return z

We could write something like:

return a
    |> f(_, b, c)
    |> g(_, d)
    |> h(b, _)

which is vertically aligned, and avoids to define tons of intermediate variables.

Two related issues: [Feature Request] piping · Issue #386 · modular/modular · GitHub and [Feature Request] Implement `map`, `filter` and `for_each` to enable function chaining (some code provided) · Issue #213 · modular/modular · GitHub

IMO, if Mojo has/will have good enough functionality related to extensions and method chaining, this won’t be necessary.

If you allow arbitrary nesting of code by making everything an expression, it surely won’t be abused.

Just flatten your code and define your variables!

I consider the piping proposal to be slightly better than iterator methods, though only by a very slim margin. This is because piping is consistent in making all code unreadable.

I can see how this could be useful in more functional style code. However, right now Mojo doesn’t really have currying (which is how I would want to implement this), and we’d need to debate how that feature would be implemented and how it would interact with function overloading.

My concern is that the ways we could implement this in Mojo might be far more verbose than using temporary variables.

You are justifying a simple syntactic feature that has a large impact on code readability with a more complicated feature that is extraordinarily difficult to explain and reason about, and that leads to generally worse error messages.

1 Like

My concern is that Mojo doesn’t have HM type inference, which means that “going backwards” is hard for it.

The most reasonable way I can think of for this to work is to first implement currying, which would require first decisions about whether the value is provided at comptime or at runtime, and then about what type the value is so that overload resolution can work.

Consider the following:

fn foo(a: Float32, b: Float32, c: Float32) -> Float32:
   """Lower precision variation."""
    return a + (b * c)

fn foo(a: Float64, b: Float32, c: Float32) -> Float64:
   """Higher precision variation."""
   return a + (Float64(b) * Float64(c))

If I were to curry this function, and do foo(_, b, c), then I first need to figure out whether b and c are being curried at comptime, in which case both functions get specialized with b and c constant folded, producing a fn(a: Float32) -> Float32 and a fn(a: Float64) -> Float64. However, if the currying happens at runtime, then currying these would produce closures, which are a different type, aren’t function pointers, and can’t be optimized as aggressively.

Mojo might be able to deal with it if it’s the comptime version, but if it’s the runtime version then we would need function overloading on closures. Even if it’s the compile-time version, Mojo would need to propagate, that, for example a is a Float64 through f, which set’s f’s return type, which may have similar knock-on effects in g and h. Based on what I know about how the parser works, the parser won’t be able to figure that out and you’ll lose type inference for values “downstream” of this construct. If the parser can’t figure it out, then you’ll need to do explicit casting or provide type hints, which very quickly makes this more verbose.

1 Like