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

Infix functions #772

Open
voronoipotato opened this issue Jul 17, 2019 · 21 comments

Comments

@voronoipotato
Copy link

commented Jul 17, 2019

Infix functions

I propose that we provide a way to make functions able to be run in an infix style through a symbol or single operator such as a ~f b where ~ modifies f to run like a infix operator. I'm not married to the specific keyword and I hope Don will lay down an edict of what symbol/syntax is used without any discussion so this doesn't get bike-shedded to death. I would like to politely request that you not debate what symbol or syntax we should use for this, and if you absolutely must do so please do it in the fsharp.org slack. Hopefully this will allow us reduce our dependency on foreign operators which cause conflicts with essentially the same operator for different objects. See >>= vs >>= , or more popularly >=> vs >=> .

a ~f b
// f here is a function made infix through the ~ symbol

The existing way of doing this is through @dsyme 's favorite butterfly operator |>f<|. F#+ uses </f/> which is a little more elegant but is still just as many characters. While this is a suitable workaround for the time being, it would be nice if there were a native approach to this. The other way this can be accomplished is computation expressions, however expanding CE's to include a function is a lot more effort. Apply is still being discussed for CE's for example.

let inline (</) x = (|>) x
let inline (/>) x = flip x

let getValidPassword : ResultT<_> =
    monad {
        let! s = liftAsync getLine
        if isValid s then return s
        else return! throw -1}
    </catch/>
        (fun s -> throw ("The error was: " + decodeError s))

Pros and Cons

The advantages of making this adjustment to F# are many. The first is that it gives a proper answer to the <> <!> >>= divide. Often we have scenarios where using |> does not work properly, for example with Seq.apply trying to use |> means we have to use <| or parentheses around the whole thing. <> <!> >>= while convenient in this regard are in many ways a hurdle for beginners, have to be defined per function or inline, and are limited in number. However if we can use a prefix to turn a function into an infix this becomes no longer a problem. This allows us to use existing and well named functions in a way that supports chaining without awkward back pipes.

add ~Seq.map [1..10] ~Seq.apply [1..10]

The disadvantages of making this adjustment to F# are ...
No more butterflies and another operator to end all operators. Less idiomatic haskell-y.

Extra information

S
Estimated cost (XS, S, M, L, XL, XXL):

Related suggestions: (put links to related suggestions here)

Affidavit (please submit!)

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 or my company would be willing to help implement and/or test this
@cartermp

This comment has been minimized.

Copy link
Member

commented Jul 17, 2019

Is there a different motivating example you could provide? Assuming an implementation like this then there's no need for extra parens:

module Seq =
    let inline apply (s: seq<'a>) (sf: seq<'a -> 'b>) = (s, sf) ||> Seq.map2 (fun x f -> f x)

[<EntryPoint>]
let main argv =
    let r =
        [1..10]
        |> Seq.map (+)
        |> Seq.apply [1..10]

    printfn "%A" r
@voronoipotato

This comment has been minimized.

Copy link
Author

commented Jul 17, 2019

It's a cool idea but wouldn't that go against our current convention of having the function first? The hope is to avoid flipping things since that's our current workaround. I think it could be useful really any time where the function isn't commutative, and you want to think about it from a left to right perspective and possibly chained with other functions.

[1;2;3;4] |> List.append [5;6;7;8] |> List.append [9;10;11;12]
//[9; 10; 11; 12; 5; 6; 7; 8; 1; 2; 3; 4]
[1;2;3;4] ~List.append [5;6;7;8] ~List.append [9;10;11;12]
//[1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 11; 12]

While it makes sense that 9 comes first with |>, it's probably not the most intuitive thing in every scenario.

@cartermp

This comment has been minimized.

Copy link
Member

commented Jul 18, 2019

It's a cool idea but wouldn't that go against our current convention of having the function first?

I don't know what you mean. There's no current convention of having the function first. It's quite idiomatic to have pipelines of data |> combinator function.

The hope is to avoid flipping things since that's our current workaround.

I don't quite understand this. In the example there's no flipping involved in my example. It's just working with the signature of the function as it was defined.

I think it could be useful really any time where the function isn't commutative

Could you add examples of this in the original issue text? This is what is a motivator; the text as-is doesn't seem to offer examples that obviate the need for specifying functions in an infix manner.

@voronoipotato

This comment has been minimized.

Copy link
Author

commented Jul 18, 2019

I thought I provided examples.

In your definition of apply you put the function after the data. So data |> combinator function would stop working. I gave a more trivial example of concat to make the problem a little more clear. Think of it as a pipe through instead of a pipe around. It's useful anywhere where you need the output to go in the first bit instead of the last bit. Sometimes when you put it at the end it makes it harder to reason about. Changing the signature of one function while maybe a good idea I don't know, doesn't fix the general problem of wanting to pipe through rather than around.

@cartermp

This comment has been minimized.

Copy link
Member

commented Jul 18, 2019

I understand the concept: I don't understand what you're saying, though.

You can use pipes without parenthesis as I demonstrated.

@voronoipotato

This comment has been minimized.

Copy link
Author

commented Jul 18, 2019

Are you saying that flipping the arguments in the definition for apply solves the general problem of wanting to pipe a result into the first parameter of a function? Flipping the arguments are what we are presently doing. Additionally this would break [1;2;3;4;5] |> Seq.apply [f1;f2;f3;f4] . Simply put, having the function as the first argument is convention in the same way that having the function as the first argument of map is convention. Doing so would break the consistency of pipes.

@cartermp

This comment has been minimized.

Copy link
Member

commented Jul 18, 2019

No, I'm saying that the example you provided is not really a motivating example for this feature. One can clearly solve the problem as-written in the issue without the use of backwards pipes or lots of parentheses. This is why I asked if there were some other motivating examples.

@voronoipotato

This comment has been minimized.

Copy link
Author

commented Jul 18, 2019

I gave a wide variety of other examples. I also disagree that flipping counts as a solution but clearly we're not going to convince each other on that.

@cartermp

This comment has been minimized.

Copy link
Member

commented Jul 18, 2019

I don't see a wide variety of examples here. Just a potential CE example and one that presupposes an implementation of apply (which I showed doesn't necessitate infix function calls).

I'll ask again: are there motivating examples that show this has a need?

@Happypig375

This comment has been minimized.

Copy link
Contributor

commented Jul 18, 2019

From #727,

I would also add as a distinct disadvantage that this would add yet another instance in F# where there are multiple equivalent ways to do exactly the same thing, making it harder to know what's idiomatic and harder to read code in general. I think F# already has more of these than many languages. This is made worse by being a late addition to the language, so even the most idiomatic code will never use this feature if it was written for earlier F# versions.

I wouldn't shocked if it were done differently had F# been first conceived today. However, I think this does break the following design principles:

  • education/learning paths and simplicity
  • does this give multiple ways to achieve the same thing
@voronoipotato

This comment has been minimized.

Copy link
Author

commented Jul 18, 2019

The other way to do this @Happypig375 is using the <| which has been stated as a bad thing to do and should be avoided according to @dsyme. By adding a "pipe through" we have a clean and obvious way to do this. By contrast the issue you were showing, while it adds cool feature the existing way to do it isn't a "discouraged behavior". So there really is no supported way to do this and the hope is that by using less operators we can have a simpler and easier to read experience for beginners.

@cartermp I provided how F#+ uses this for catch. Maybe it's not important enough to do I thought it would help avoid the operator creep that we currently see such as >=>. Operator creep can really cause grief for F# due to how different types treat operators differently. I personally thought this was closer to idiomatic F# than having a bunch of conflicting operators, but apparently you disagree to the point where you aren't even willing to consider them an example.

@gusty

This comment has been minimized.

Copy link

commented Jul 19, 2019

@cartermp Your piping example hilites the other workaround which is flipping functions in order to be able to forward pipe through them.

I think we all agree that having a second set of flipped functions is not a good deal, the alternative of using a flip function:

        [1..10]
        |> Seq.map (+)
        |> flip Seq.apply [1..10]

is considered evil by some people, including @dsyme

Of course, another way is to use Computation Expressions.

CEs looks nice for the imperative eye, but I worked in finance and F# shines there as business guys are really able to follow and validate the code if you take advantage of the syntax and write it like a formula:

let x = bs + a1 * S </safeDiv/> Sk

Write it as a CE or a forward pipe and they will see a program.

@charlesroddie

This comment has been minimized.

Copy link

commented Jul 19, 2019

@voronoipotato it would help avoid the operator creep that we currently see such as >=>

There is a certain style of F# that uses operators like this, and like </safeDiv/>. This usually cares about visual simplicity but not the complexity of type signatures. While this has some uses (showing to "business guys"), and people are welcome to create libraries with this preference, this style should not affect the F# language itself, since the intrinsic simplicity of constructs must take priority over their appearance.

I think it could be useful really any time where the function isn't commutative
[1;2;3;4] ~List.append [5;6;7;8] ~List.append [9;10;11;12]

I agree this is slightly preferable to List.append (List.append [1;2;3;4] [5;6;7;8]) [9;10;11;12], and so would work for non-commutative but transitive operations (transitive so you dont want to bother with bracketing). But for these situations you usually have sequence operations, as in this case: List.concat [[1;2;3;4]; [5;6;7;8]; [9;10;11;12]].

@voronoipotato

This comment has been minimized.

Copy link
Author

commented Jul 23, 2019

Here's the thing though, because of the lack of a unified standard we get things like this.

This will make all the HTTP method-, header-, and body-functions available. It will also import the operators:

(--) (alias for |> pipe forward)
(%%) (alias for <| pipe backward)
(.>) (synchronous request invocation)
(>.) (asynchronous request invocation)
The reason why there are aliases for |> and <| is the different precedence, that enables writing requests in a fluent way with less parenthesis.

Example

https://github.com/ronaldschlenker/FsHttp

Now I'm not trying to throw shade on FsHttp, I think the library is fantastic, however this is clearly another case where people resort to other things to avoid the butterfly of doom.

@ronaldschlenker

This comment has been minimized.

Copy link

commented Jul 23, 2019

I am with you Alan. It was a tradeoff between having custom, non-idiomatic elements (which I usually try to avoid because it might scare users) vs. convenience and expressiveness. Having a way of being convenient and idiomatic would be great.

@cartermp

This comment has been minimized.

Copy link
Member

commented Jul 23, 2019

I don't understand how this would help FsHttp. The CE example would be unchanged. The use of chained operators wouldn't be improved by this. It could allow for a third DSL using infix functions, if that's what is missing from the library.

@voronoipotato

This comment has been minimized.

Copy link
Author

commented Jul 24, 2019

Or you could use infix functions in place of both one shot CE and one shot operators because it's a solution that would be convenient and expressive while being approachable to newcomers. Your arguments are really lacking generosity here. I also don't see what the big deal is. It's a pipe that goes into the first argument rather than the last argument. Was there this much gnashing of teeth with the addition of |> ?

@cartermp

This comment has been minimized.

Copy link
Member

commented Jul 24, 2019

I'd appreciate that you don't accuse me of lacking generosity here. I'm a bit taken aback that you're unwilling to demonstrate more illustrative examples when asked to do so because it's a little unclear.

As for FsHttp, again, I don't see how this helps the way you use that library today.

@voronoipotato

This comment has been minimized.

Copy link
Author

commented Jul 24, 2019

Apologies. At the time it appeared ungenerous to me, however I was likely mistaken and I'm genuinely sorry for mischaracterizing you. Perhaps it would be helpful to see you elaborate what about this seems unhelpful? I have attempted to describe illustrative examples, so it's unclear what you're expecting me to do. The philosophical goal of this issue is that words are often clearer and easier for beginners than symbols, however operators are the only way to make something infix. When thinking from left to right it's useful to have an infix form. Sometimes you want to chain via the first argument and not via the last argument. It would be useful to have a pipe that goes into the first argument rather than having to write two versions of a function. The whole reason FsHttp has a %% operator is to facilitate this kind of left to right "fluent" style.

@ronaldschlenker

This comment has been minimized.

Copy link

commented Jul 24, 2019

The main reason why there are -- and %% in FsHttp is indeed readability for non-F# users (which I had a lot in one of my previous Projects).

Having Code like this scared some team members:

get <| buildSrvUrl "/orders"
|> acceptLanguage "de-DE"
|> authorization "Bearer 4943859034859m4..."
.> go

So I tried to alias pipe-left-to-right (with --) to achieve a more command line-ish style:

get <| buildSrvUrl "/orders"
-- acceptLanguage "de-DE"
-- authorization "Bearer 4943859034859m4..."

This doesn't work because of operator precedence, so I had to introduce %% as alias for pipe-right-to-left with different precedence:

get %% buildSrvUrl "/orders"
-- acceptLanguage "de-DE"
-- authorization "Bearer 4943859034859m4..."

I, too, don't see a strong connection to infix functions; no evil butterfly. The design choice was driven by a) visual appearence (might seem unimportant, but it attracted some non-F# team members), and b): It is a mess typing |> and <| on a German keyboard. It's a small thing, but if you write tons of queries a day, it becomes important.

But there are some (partially already mentioned) things that could improve the implementation or the usage of the library:

  • Having had infix functions (= alias for pipe-left-to-right into first argument) would have saved me writing 2 versions for each operation (one for CE, one for fluent API). But I don't know if that's very common.
  • Already mentioned: Learning a library like FParsec with a lot of operators could be easier having infix functions (too).
  • Editor support: Sometimes, I hear from C# developers that they can "dot" on IEnumerable and get LINQ intellisense. Of cource, in F# we have a lot of qualified access via modules. The argument then is: "I have to write '|> Seq.' instead of just '.'"?. Also, I use a lot FSI, and I have unqualified List/Seq/Array functions (this SRTP "hack" you propably know). Having infix functions, an editor could provide an intellisense list by looking up the functions having and their first parameter according to the value on the left side of the infix function:
// assuming you have a "reduce" function for lists in scope
let result = [1; 2; 3] ~reduce (+)
//                     ^
//                     |- typing "~" opens an intellisense list that provides "map"

As far as I know, in Haskell, you can predefine (or define ad hoc) a precedence for your functions when using infix. That "sounds" at least interesting - if it's of a big use, I'm not sure.

@charlesroddie

This comment has been minimized.

Copy link

commented Jul 25, 2019

get %% buildSrvUrl "/orders"
-- acceptLanguage "de-DE"
-- authorization "Bearer 4943859034859m4..."

@ronaldschlenker But you can get both the same visual structure and more essential simplicity by doing it the standard .Net way SrvUrl("/orders", AcceptLanguage = "de-DE", Authorization = "Bearer 4943859034859m4..."), using either optional constructor arguments or settable properties. You can also do .AcceptLanguage("de-DE") which I find awful but have seen in C# libs. Some F# libs take huge workarounds, at a cost of inherent complexity of types, to avoid defining .Net classes.

Learning a library like FParsec with a lot of operators could be easier having infix functions (too).

That's possible. @voronoipotato what this thread needs a concrete example where infix functions are the best option. Current syntax, simplest currently possible alternative syntax if current is not the best, what the syntax would be with infix functions.

Sometimes, I hear from C# developers that they can "dot" on IEnumerable and get LINQ intellisense. Of cource, in F# we have a lot of qualified access via modules.

FSharp.Core.Fluent is the answer to this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
6 participants
You can’t perform that action at this time.