Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for a pipeline operator |> #2585

Open
aviRon012 opened this issue Feb 7, 2023 · 5 comments
Open

Add support for a pipeline operator |> #2585

aviRon012 opened this issue Feb 7, 2023 · 5 comments
Labels
leads question A question for the leads team long term Issues expected to take over 90 days to resolve.

Comments

@aviRon012
Copy link
Contributor

aviRon012 commented Feb 7, 2023

Summary of issue:

Support a pipeline operator |> to pass the output of one function as parameters to another function,
This is a feature that exists in many languages, And it can improve code readability.

Details:

Examples

Say we have some functions:

fn scale(polygon: Polygon, factor: f32) -> Polygon;
fn rotate(polygon: Polygon, degrees: f32) -> Polygon;
fn translate(polygon: Polygon, x: f32, y: f32)) -> Polygon;

Now compare the difference in readability between the following two code examples:

let transformed_polygon: auto = polygon
    |> scale(5.0)
    |> rotate(90.0)
    |> translate(3.0, 5.0);
\\ equivalent to
let transformed_polygon: auto = translate(rotate(scale(polygon, 5.0), 90.0), 3.0, 5.0);

I think most would agree that the first option is more readable.
Another example is with iteration (loosely):

fn filter[T:! Iterate](iterable :T, my_predicate: Predicate(...)) -> IterateProxy;
fn map[T:! Iterate](iterable :T, my_map: Map_func(...)) -> IterateProxy;

for (value: auto in my_iterable |> filter(my_predicate) |> map(my_map)) {
    //do stuff...
}
// equivalent to
for(value: auto in map(filter(my_iterable, my_predicate), my_map) {
    //do stuff...
}

The alternatives aren't great

Notice that if the above functions can be declared as methods of Polygon, then the above can be expressed in terms of chaining methods:

let transformed_polygon: auto = polygon
    .scale(5.0)
    .rotate(90.0)
    .translate(3.0, 5.0);

however this is not always appropriate to define functions as methods of classes.
Another way to solve this is with temporary values, the output of each function is saved in a temporary value and pass that value to the next function

let temp1: auto = func1(value);
let temp2: auto = func2(temp1);
let result: auto = func3(temp2);

This too is not great.

Alternative syntax

We can have a symbol such as % that will be placed where arguments are normally passed to a function, this has the advantage of being able to specify to which argument of next function will the output of the previous function go:

fucn1(args...) |> func2(arg1, %, arg2 ...);
// equivalent to
func2(arg1, fucn1(args...), arg2 ...);

Passing the components of a tuple as arguments to a function

Sometimes functions return multiple values as a tuple, It may be desirable to pass those multiple values as arguments to the next function, if so there needs to be a way to differentiate between passing the tuple and passing its components, perhaps with an operator ||> or |>> or something similar, or perhaps unpacking tuples can be solved by some other mechanism of the language, or if we go with the option of the % symbol perhaps we can use the following syntax:

returns_tup(args ...) |> next_func(arg1, %%, arg2 ...);
// equivalent to
let temp_tup: auto returns_tuple(args ...);
next_func(arg1, temp_tup[0], temp_tup[1],... ,arg2 ...);

or perhaps this syntax:

returns_tup(args ...) |> next_func(arg1, %[0], arg2, %[1], ...);
// equivalent to
let temp_tup: auto returns_tuple(args ...);
next_func(arg1, temp_tup[0], arg2, temp_tup[1], ...);

Other languages

  • there is currently a JavaScript proposal for something like this, I think this explains much better than I can the rational for this, and how it can work,
  • elixir,
  • f#,
    and obviously shell languages have a similar concept:
  • bash,
  • powershell
    and many others...

Any other information that you want to share?

No response

@aviRon012 aviRon012 added the leads question A question for the leads team label Feb 7, 2023
@zygoloid
Copy link
Contributor

zygoloid commented Feb 7, 2023

I'm cautiously optimistic about this direction. I don't think this is something we should push for in Carbon 0.1, but it seems like an interesting avenue to explore post-0.1.

Notice that if the above functions can be declared as methods of Polygon, then the above can be expressed in terms of chaining methods [...] however this is not always appropriate to define functions as methods of classes.

#1122 (extension methods) could help here, but isn't really great, because you only get to pick one parameter to be self, and the decision is made by the method not by the caller.

We could also allow functions to be treated as methods, with the % sigil used to indicate which parameter is treated as self:

let transformed_polygon: auto = polygon
    .(scale)(%, 5.0)
    .(rotate)(%, 90.0)
    .(translate)(%, 3.0, 5.0);

... but I don't find this syntax especially aesthetically pleasing, and it would be a special case rather than a natural consequence of our other rules.

Passing the components of a tuple as arguments to a function

The %[i] syntax here makes sense. To forward a tuple as function arguments, I think we may not need to invent anything new: returns_tuple() |> takes_multiple_args(..., [:]%) seems like it should do the right thing.

@aviRon012
Copy link
Contributor Author

aviRon012 commented Feb 8, 2023

If we do decide to implement this feature, a nice mental model for this is an analogy to a calculator.
With a calculator, you write a mathematical expression, and then you press =, and the result of the calculation is stored in a variable you can refer to as ans.
So, in Carbon you write an expression, then you write |> (like pressing =), then you write a proceeding expression using % (the same way you use ans).

@geoffromer
Copy link
Contributor

To forward a tuple as function arguments, I think we may not need to invent anything new: returns_tuple() |> takes_multiple_args(..., [:]%) seems like it should do the right thing.

For the benefit of other readers: ..., and [:] are part of the current early draft design for variadics. To vastly oversimplify, [:] transforms a tuple into a pack, and ..., turns a pack into a comma-separated list. This specific syntax is very much a work in progress, but I expect the final design for variadics to still need two separate operators to do this, and although their spelling and fixity may change, I don't expect them to get much more concise.

@aviRon012 aviRon012 changed the title Support a pipeline operator |> Add support for a pipeline operator |> Feb 23, 2023
@zllangct
Copy link

zllangct commented Mar 6, 2023

less symbol more readable

@OliverKillane
Copy link

Some thoughts: is this just syntactic sugar, or a lead into FP?

data Polygon = Poly

scale :: double -> Polygon -> Polygon
scale s p = undefined

rotate :: Double -> Polygon -> Polygon
rotate a p = undefined

translate :: Double -> Double -> Polygon -> Polygon
translate x y p = undefined

{- 
transformed_polygon: auto = polygon
  |> scale(5.0)
  |> rotate(90.0)
  |> translate(3.0, 5.0);
-}
-- either with composition
x1 = ((translate 3.0 5.0) . (rotate 90.0) . (scale 5.0)) Poly
-- or with right associative application
x2 = translate 3.0 5.0 $ rotate 90.0 $ scale 5.0 $ Poly

There are some inconsistencies that should be addressed

  1. Are we piping to a value? Or specifically a function name with some args set?
let x1 : auto = polygon |> scale(%, 1.2);
let x2 : auto = polygon |> fn (p){ return scale(p, 1.2)}; // some lambda instead of a function

let lambda = fn (p){ return scale(p, 1.2)};
let x3 : auto = polygon |> lambda  

fn getlambda() -> auto {return  fn (p){ return scale(p, 1.2)};}
let x4 : auto = polygon |> getlambda();

fn getpartial() -> auto {return scale(%, 1.2)};
let x5 : auto = polygon |> getpartial();

We also have some potential for ambiguity in this case:

fn foo(p: i32) -> i32 {return p * 2;}

fn example() {
  let foo = () { return 3;} // can currently give variable same name as a function
  let x = polygon |> foo  // ah shucks, which foo?
}
  1. If |> is a simple 'chain output to first parameter' What advantage is there in adding |> syntax over reusing .
// if "." is relaxed such that a.f is applies function f with a as first argument, then we need no more

// some struct with private internals
let x : PrivStruct

fn foo(y: Polygon) -> PrivStruct
fn bar(p: PrivStruct, x: i32) -> PrivStruct

polygon. scale(0.3).foo().privStructMethod(3).bar(4)
  • Note: Effectively allows users to extend interfaces, need to be careful about conflicts
  1. If |> is implemented as function application (necessitates support for partial application), then
    this feels like a proposal that would work better as a larger FP extension to the language.
  • Composition
  • Function & Closure Types
  • Piping/this prop
  • Lambdas
  • Comprehensions
  • Partial Application
    Tacking together functional-like syntactic sugar later in language development probably wont end up as nice as waiting and having a really good crack at taking the more practical parts paradigm in one consistent extension.
let trans : auto = scale(%, 0.5) => rotate(%, 90) => translate(%, 3, 2);
polygon |> trans |> printMe
let x: auto = [1,2,3,4]
let y: auto = [polygon.clone() for _ in 0..x.len()]
zip(y,x).map(scale) |> printMe

@zygoloid zygoloid added the long term Issues expected to take over 90 days to resolve. label Apr 11, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
leads question A question for the leads team long term Issues expected to take over 90 days to resolve.
Projects
None yet
Development

No branches or pull requests

5 participants