ponylang / rfcs

RFCs for changes to Pony
https://ponylang.io/
59 stars 48 forks source link

Request: Add pipeline operator #165

Open damon-kwok opened 4 years ago

damon-kwok commented 4 years ago

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"
SeanTAllen commented 4 years ago

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.

damon-kwok commented 4 years ago

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)
damon-kwok commented 4 years ago

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;
            });
}
jemc commented 4 years ago

I am in favor of this :+1:

SeanTAllen commented 4 years ago

I'm not in favor, but I'm also not super opposed. I'm mostly ¯_(ツ)_/¯.

jemc commented 4 years ago

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.

jemc commented 4 years ago

Discussed in today's sync call:

jkankiewicz commented 4 years ago

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".

jkankiewicz commented 4 years ago

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.

aturley commented 4 years ago

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.

igotfr commented 3 years ago
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)