rfcs
rfcs copied to clipboard
Request: Add pipeline operator
Pipe Operator
The pipe operator |>
passes the result of an expression as the first parameter of another expression.
The pipe operator |>>
passes the result of an expression as the last parameter of another expression.
Example
primitive Str
fun trim (s: String): String =>
...
fun upper (s: String): String =>
...
fun print(s: String, env: Env) =>
env.out.print(s)
primitive Trace
fun print(env: Env, s: String) =>
env.out.print(s)
actor Main
new create(env: Env) =>
let s = " I Love Pony "
s |> Str.trim |> Str.upper |> Str.print(env) //"I LOVE PONY"
s |>> Str.trim |>> Str.upper |>> Trace.print(env) //"I LOVE PONY"
Can you provide some motivation for why this would be desirable? Every RFC should contain some motivation as to why it is desirable. Even requests.
Please note in case you are aware. Opening an issue such as this is a request for another human (other than you) to write up the full RFC so the more details you provide them, the better.
What's the pipeline operators ?
The pipeline operator |>
and |>>
pipes the value of an expression into a function. This allows the creation of chained function calls in a readable manner. The result is syntactic sugar in which a function call with a single argument can be written like this:
let url = "%21" |> decodeURI()
The equivalent call in traditional syntax looks like this:
let url = decodeURI("%21")
The pipe operator |>
passes the result of an expression as the first parameter
of another expression.
The pipe operator |>>
passes the result of an expression as the last parameter
of another expression.
Why need pipeline operators ?
Pipe Operator allows a clear, concise expression of the programmer's intent. Example :
fun inc[A: (Real[A] val & Number) = I32](x: A): A => x+1
fun double[A: (Real[A] val & Number) = I32] (x: A): A => x*2
fun print[A: (Real[A] val & Number) = I32](x: A) => _env.out.print(x.string())
// without pipeline operator
let x: I32 = 3
let x'= inc(x)
let x''= double(x')
print(x'') //8
// with pipeline operator
3 |> inc() |> double() |> print() //8
// without pipeline operator
print(add(1,double(inc(double(double(5)))))) // 43
// with pipeline operator
5 |> double() |> double() |> inc() |> double() |>> add(1) |> print() // 43
// without pipeline operator
let arr: Array[I32] ref = [1;2;3;4;5]
let arr2 = Seqs.map(arr, Num.mul(2))
let arr3 = Seqs.reverse(arr2)
let arr4 =Seqs.join(arr3, "=")
lSeqs.trace(arr4)
//prints:
"10=8=6=4=2"
// with pipeline operator
[1;2;3;4;5]
|> Seqs.map(Num.mul(2))
|> Seqs.reverse()
|> Seqs.join("=")
|> Seqs.trace()
//prints:
"10=8=6=4=2"
// without pipeline operator
let str = "abc\ndef\nghi"
let arr = Str.lines(str) // ["abc"; "def"; "ghi"]
let arr2 = Seqs.map(arr, Str.upper) // ["ABC"; "DEF"; "GHI"]
let arr3 = Seqs.map(arr, Str.reverse) //["CBA"; "FED"; "IHG"]
let path = Seqs.join(arr3, "/") // "CBA/FED/IHG"
let path_win = Str.winpath(path) // "CBA\\FED\\IHG"
let dirname = Str.dirname(path_win) //"IHG"
// with pipeline operator
"abc\ndef\nghi"
|>Str.lines()
|>Seqs.map(Str.upper)
|>Seqs.map(Str.reverse)
|>Seqs.join("/")
|>Str.winpath
|>Str.dirname //prints: "IHG"
The return value of each function is used as the first argument of the next function in the pipe. It's a beautiful expression that makes the intent of the programmer clear.
How to implements pipeline operators ?
I do n’t have much say on how to achieve it, but only contribute a possible implementation method, just for reference:
primitive Num[A: (Real[A] val & Number) = I32]
fun inc(x: A): A => x+1
fun double (x: A): A => x*2
fun add(a: A, b: B): A => a+b
fun print(x: A, env: Env) => env.out.print(x.string())
fun trace(env: Env, x: A) => env.out.print(x.string())
Design the pipeline as a Partial Application
syntactic sugar, |>
will automatically wrap the first parameter|>>
automatically wrap the last parameter
3 |> Num~inc() |> Num~add(3) |>> Num~trace(env)
Here are some pipeline usage in other languages: Elixir pipeline: https://elixirschool.com/en/lessons/basics/pipe-operator/
Javascript Pipeline: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Pipeline_operator
React Pipeline: https://simon.lc/use-new-pipeline-operator-with-react-hocs
Clojure 's pipe : Threading macros
https://clojure.org/guides/threading_macros
Ruby 2.7: The Pipeline Operator https://dev.to/baweaver/ruby-2-7-the-pipeline-operator-1b2d
Emacs Lisp: https://github.com/magnars/dash.el#threading-macros
F# pipeline: https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/functions/#function-composition-and-pipelining https://github.com/fsharp/fsharp/blob/master/src/fsharp/FSharp.Core/prim-types.fs#L3412
ReasonML pipeline: https://reasonml.github.io/docs/en/pipe-first
TypeScript pipeline: https://github.com/Dabolus/typescript-pipeline-operator/blob/master/pipeline.ts
Python pipeline: https://pypi.org/project/pipe/
Haskell pipeline:
(|>) :: a -> (a -> b) -> b
(|>) x f = f x
Bash pipeline: https://www.gnu.org/software/bash/manual/html_node/Pipelines.html
Rust pipeline:
use std::ops::Shr;
struct Wrapped<T>(T);
impl<A, B, F> Shr<F> for Wrapped<A>
where
F: FnOnce(A) -> B,
{
type Output = Wrapped<B>;
fn shr(self, f: F) -> Wrapped<B> {
Wrapped(f(self.0))
}
}
fn main() {
let string = Wrapped(1) >> (|x| x + 1) >> (|x| 2 * x) >> (|x: i32| x.to_string());
println!("{}", string.0);
}
// prints `4`
Julia pipeline:
julia> Base.|>(xs::Tuple, f) = f(xs...)
|> (generic function with 7 methods)
julia> let
x = 1
y = 2
# just messing around...
(x, y) |> (x, y) -> (2x, 5y) |>
divrem |>
complex |>
x -> (x.re, x.im) |>
divrem |>
(x...) -> [x...] |>
sum |>
float
end
0.0
CommonLisp: https://github.com/nightfly19/cl-arrows#examples https://github.com/hipeta/arrow-macros#examples
C# http://www.mkmurray.com/blog/2010/09/06/f-pipeline-operator-in-c/
static class Extn {
public static U Then<T,U>(this T o, Func<T, U> f) { return f(o); }
public static U Then<T, U>(this T o, Func<T, Func<U>> fe) { return fe(o)(); }
}
… which lets you write the more-readable:
var blah = "576 "
.Then<string,string>(x => x.Trim)
.Then(Convert.ToInt32);
C++ pipeline: http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2013/n3534.html
C++20 pipeline: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p2011r0.html
auto dangerous_teams(std::string const& s) -> bool {
return s
| views::group_by(std::equal_to{})
| views::transform(ranges::distance)
| ranges::any_of([](std::size_t s){
return s >= 7;
});
}
I am in favor of this :+1:
I'm not in favor, but I'm also not super opposed. I'm mostly ¯_(ツ)_/¯.
We discussed this briefly in today's sync call.
We agreed that we want to continue discussing here in this issue ticket and in Zulip to try make sure that what we come up with is as useful as we can make it.
@SeanTAllen mentions that he is wanting us to try to come up with something that will solve problems more broadly than this current proposal does.
I mentioned that I'm generally in favor of things like this if they solve some problems and mostly stay out of the way when you don't need them. But of course if we can solve more problems with a different proposal, I'm likely in favor of that.
Others mentioned that they can remember wanting something like this in Pony at various points in time.
Discussed in today's sync call:
-
If we end up with multiple pipeline operators, we have to explicitly talk about operator precedence (Sylvan mentioned being against precedence differentiation, which would require parentheses, which I think would really break up the pipeline in undesirable ways) so that may be another reason to not prefer multiple pipeline operators, on top of Sean already not liking having multiple operators and wanting to find a better way to target position.
-
Sylvan suggested lambda-wrapping any pipeline steps that need to juggle the value to a different argument position.
I'm not in favor, but I'm also not super opposed. I'm mostly ¯_(ツ)_/¯.
@SeanTAllen, if you have 4½ minutes to spare, Dave Thomas makes a compelling argument for the pipeline operator in his presentation "Think Different".
Others mentioned that they can remember wanting something like this in Pony at various points in time.
@sylvanc suggested the addition of a pipeline operator in PR #4.
Let's get more comment on this and see where we are next week. If anybody is interested in writing an RFC (@damon-kwok, @jkankiewicz?) please feel free to do that. If you have any questions about doing that please ask for help in this issue.
a |> f
// equivalent to
f(a)
b |2> f(a) // or b |1> f(a) if starting at 0
// equivalent to
f(a, b)
b |b_arg> f(a)
// equivalent to
f(a, where b_arg = b)