carbon-language / carbon-lang

Carbon Language's main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)
http://docs.carbon-lang.dev/
Other
32.28k stars 1.48k forks source link

Add support for a pipeline operator |> #2585

Open aviRon012 opened 1 year ago

aviRon012 commented 1 year ago

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

Any other information that you want to share?

No response

zygoloid commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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.

zllangct commented 1 year ago

less symbol more readable

OliverKillane commented 1 year ago

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? }

3. If `|>` is a simple 'chain output to first parameter' What advantage is there in adding `|>` syntax over reusing `.`
```carbon
// 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)
  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