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

Using the pipe operator to assign the output of a pipe to a variable #541

Closed
5 of 6 tasks
PerArneng opened this issue Feb 18, 2017 · 9 comments
Closed
5 of 6 tasks

Comments

@PerArneng
Copy link

PerArneng commented Feb 18, 2017

I propose we allow the output of a pipeline to be stored inside an existing mutable variable or create a new one. The natural flow of data is forward and it is just as natural to store the data inside a variable as it is to pass it in to a function.

The existing way of approaching this problem in F# is to declare the variable first and then let it consume the output of the pipleline by sending it from the end back to the declaration. The data flows forward and then jumps back to the declaration.

let minimum = [0..10] |> Seq.min
printfn "minimum: %d" minimum

An alternative (additional) approach would be to allow the data to be piped in to the variable.

[0..10] |> Seq.max |> minimum
printfn "minimum: %d" minimum

The variable minimum in the example above is created and the type is inferred by the type returned by the max function. If minimum already existed it should have been a mutable variable with the type int to compile correctly. When piping in to a variable the value is consumed by the variable and the resulting value of the whole pipeline is unit.

let mutable minimum
let otherminimum:int = [0..10] |> Seq.max |> minimum

The code above would fail stating that you can not assign unit to an int or something similar.

A captured value can always be continued sent forward as show below.

[0..10] |> Seq.min |> minimum |> (fun x -> x+1) |> minimumPlusOne
printfn "%d" minimum

If you want to specify a type on your variable for readability you can do so.

[0..10] |> Seq.min |> minimum:int

Side Notes

When creating this variable inside the pipeline i have omitted the let keyword on purpose since it can be implied by the construct. If you feel that it is needed it could also be added. Then an example would look like this.

[0..10] |> Seq.min |> let minimum:int

Other Languages

Similar behavior can be seen in bash when pipining the result of a pipeline in to a file.

$ echo "ss" | sed s/"s"/"x"/g  > myfile.txt

Pros and Cons

The advantages of making this adjustment to F# are that you get a consistent way of allowing data in a pipeline to naturally flow forward regardless of having a function or a variable as the target.

The disadvantages of making this adjustment to F# are that it is an alternative new way of looking at the result from a pipeline and that can cause confusion for people who are used to the old way of looking at the result of a pipeline. People who are not used to seeing this new pipeline might confuse the variable from being a function.

Extra Information

Estimated cost (XS, S, M, L, XL, XXL): (I don't have enough knowledge of the f# tools to give an estimate)

Related suggestions: N/A

Affadavit (must be submitted)

Please tick this by placing a cross in the box:

  • This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it.

Please tick all that apply:

  • This is not a breaking change to the F# language design
  • I would be willing to help implement and/or test this
  • I or my company would be willing to help crowdfund F# Software Foundation members to work on this
@rmunn
Copy link

rmunn commented Feb 20, 2017

While I like the idea in principle (especially the similarity to Bash where the pipeline gets written to a file at the end), I'm not a fan of any of the proposed syntax suggestions. They all have various drawbacks. For example, this one looks ugly to me:

[0..10] |> Seq.min |> let minimum

That doesn't read like English, whereas let minimum = [0..10] |> Seq.min does. (Or at least it reads a little bit like English).

Whereas the proposal of making the let be optional and inferred could lead to syntax errors not being caught:

let sshow n = sprintf "Result was %i" n  // Note the typo
// ... more code here ...
[0..10] |> Seq.min |> show

Currently, that would not compile, with the compiler complaining that "The value or constructor 'show' is not defined", and you'd probably catch the typo pretty quickly. But if the let syntax is optional, the above would become the equivalent of:

let sshow n = sprintf "Result was %i" n  // Note the typo
// ... more code here ...
let show = [0..10] |> Seq.min

And the compiler wouldn't end up informing you about your sshow typo. So that's not a great solution either.

Another possibility would be to create a new operator for the "assign to a variable at the end of the pipeline" operation. But the question of "which operator to create?" is a thorny one. Anything you pick will probably overlap with custom operators that have been created by existing libraries; for example, |>> would seem like a decent possibility since it reminds one of the > outputfile.txt operation in Bash scripting. But |>> is already used by FParsec, among others — so adding an |>> operator to the F# language would be a breaking change that would require a lot of existing code to be rewritten. That one's a non-starter, and the same problem would probably apply to just about any other operator you could come up with.

Actually, there is one syntax that I can think of that I wouldn't be opposed to, which would be to use the as keyword instead of let. So the original [0..10] |> Seq.min |> let minimum example would become:

[0..10] |> Seq.min |> as minimum
printf "Result was %i" minimum // Prints "Result was 0"

There's still a disadvantage to this suggestion, which is that now there are two different keywords to define a variable, let and as. But this use of as fits with how it's used in pattern matching, so it's at least understandable.

With the as syntax, I'm -0 on this suggestion: still leaning towards "not a fan" since I still don't like that syntax very much, but not totally opposed. With any other syntax, I'm probably 👎 on the suggestion, since any gains in clarity would be (IMHO) offset by the weirdness of how |> let variableName reads.

@cartermp
Copy link
Member

cartermp commented Feb 20, 2017

Another issue is conflicting names:

let minimum x y = if x > y then y else x
...
[0..10] |> Seq.min |> minimum

Because of partial application, it's perfectly valid to generate a function via a pipeline. Here's the FSI 4.1 output of doing this:

> let minimum x y = if x > y then y else x;;

val minimum : x:'a -> y:'a -> 'a when 'a : comparison

> let foo = [1..10] |> Seq.min |> minimum;;

val foo : (int -> int)

> foo 12;;
val it : int = 1

What should the behavior be here? Emit a warning that you're not getting the minimum that you may think you're getting? It's not quite clear to me.

@PerArneng
Copy link
Author

@cartermp : I also think that let should be ommitted

For the last example i think that it would be a problem. Since minimum is declared it would be used in this context. If it wasn't declared the assumption would be to create a variable called minimum.

In terms of English language that flows from left to right the following:

[0..10] |> Seq.min |> minimum

Would be translated to:

Create an array from 0 to 10 and then select the minimum value and store it in the new variable minimum.
let minimum = [0..10] |> Seq.min 

Would be translated to:

Let the new variable minimum have the value of the following expression: Create an array from 0 to 10 and select the minimum value .

I think that the "pipe in to variable" reads more easily since the flow follows the english language which is Left to Right.

It would also be possible to have a separate operator as suggested if that would be needed. Ex:

[0..10] |> Seq.min |=> minimum

or

[0..10] |> Seq.min |= minimum

All in all i think the best benefit of a change like this is that the whole language would be focused around the flow of data in pipes from left to right just like the English language.

1.80 |> length
72.0 |> weight
weight/length**2.0 |> printfn "bmi: %A"

@theprash
Copy link

theprash commented Feb 20, 2017

@PerArneng The |> operator is not actually syntax in F#. It's just a function defined here as:

let inline (|>) x f = f x

All it means is apply the value on the left to the function on the right. Changing the meaning of this would definitely be a significant and confusing change.

Implementing this suggestion would need new language syntax. Using |> or any operator that can be defined or overridden by the user, such as |=> and |=, would be a breaking change. Therefore it would have to be a reserved operator or keyword (e.g. as).

I don't really see the benefit of this and I don't think it's intuitive. The bash example doesn't actually do the same thing as your suggestion. And I can see downsides: it makes it harder to see where a name was bound, or that an expression is there to bind a name (vs perform a side-effect). Also it adds a second way to do exactly the same thing as an existing construct in a language which is already quite big as it straddles FP and OOP. I think it would be hard for existing F# users to read code making use of this.

An interesting idea, though. Got any more? :)

@kspeakman
Copy link

kspeakman commented Feb 27, 2017

For the cursory example given, a variable declaration is unnecessary anyway.

[0..10] |> Seq.min |> printfn "minimum: %d"

But you probably have more sophisticated cases in mind. In those cases, you could use fun to get a label on the output value.

[0..10]
|> Seq.min
|> fun min ->
    printfn "minimum: %d" min
    min + 1
|> fun minPlusOne ->
    printfn "minPlusOne: %d" minPlusOne
    minPlusOne

I think there are multiple additional ways to do this within the existing language features, depending on the particular problem you're solving.

@dsyme
Copy link
Collaborator

dsyme commented Mar 1, 2017

Thanks for the suggestion. Realistically we aren't going to do the suggestion as originally stated in F# - the reasons why are covered well in the discussion above

The suggestion of using as is of some interest

[0..10] |> Seq.min as minimum

but I still feel that this just doesn't bring enough to justify adding multiple ways of doing the same thing

@dsyme dsyme closed this as completed Mar 1, 2017
@hakanai
Copy link

hakanai commented Feb 7, 2023

Coming across this ticket when looking for a way to do similar... seeing it marked as "completed"... but reading the context, it's not clear to me anymore, was a syntax added or not for this?

@vzarytovskii
Copy link

Coming across this ticket when looking for a way to do similar... seeing it marked as "completed"... but reading the context, it's not clear to me anymore, was a syntax added or not for this?

No, since it was closed before GitHub had two separate reasons when closing an issue, it defaults to "completed".

@daveyostcom
Copy link
Contributor

daveyostcom commented Feb 23, 2023

The feature proposed in this issue will provide an obvious improvement in readability if a pleasing syntax can be found.

Instead of this, which does not read left to right,

let f inputs =
  let s1 =
      inputs
      |> Array.sort
  let s2 =
      s1
      |> Array.sum
  s1, s2

this would be much easier to read for the same effect

let f inputs =
  inputs
  |> Array.sort
  |-> s1
  |> Array.sum
  |-> s2
  s1, s2

|-> is my favorite candidate.

Regarding the objection to adding another way to do the same thing, |> itself fits that description, and this feature does improve readability fully in the spirit of |> by contributing to left-to-right readability.

[edited in response to the ‘confused’ reaction emojis]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants