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

Postfix operators #711

Closed
5 tasks done
Happypig375 opened this issue Dec 18, 2018 · 17 comments
Closed
5 tasks done

Postfix operators #711

Happypig375 opened this issue Dec 18, 2018 · 17 comments

Comments

@Happypig375
Copy link
Contributor

Happypig375 commented Dec 18, 2018

Postfix operators

I propose we allow $ at the start of custom operators, but the operator will be postfix.

Currently, the $ operator by itself is an infix operator, but multi-symbol operators starting with $ is not supported. I'd like to take this opportunity to enable defining postfix operators in F#, something like:

//With a type
type Factorial = Factorial of int with
  static member ($!) (Factorial f) =
    let rec fact = function
      | 0 | 1 -> 1
      | x when x > 1 -> x - 1 |> fact |> (*) x
      | _ -> failwith "negative number"
    fact f |> Factorial

(Factorial 4)! //val it : Factorial = Factorial 24

//Inline
let rec ($!) = function
    | 0 | 1 -> 1
    | x when x > 1 -> (x - 1)! * x
    | _ -> failwith "negative number"

The rules are simple: If the operator starts with a $ and is longer than one character, then the leading $ is stripped away when used.
So, static member ($$) would become a postfix operator $. Alternatively, it could become $$, in line with how ~ behaves currently.

The existing way of approaching this problem in F# is maybe using a prefix operator, but why limit the types of operators when F# can be used as a DSL?

Pros and Cons

The advantages of making this adjustment to F# are:

  1. Fill the missing hole in regards to types of operators
  2. Make F# more viable as a DSL
  3. Less restriction to what people can do - We can now use let people use define both prefix and postfix ++ and -- if they come from an imperative background
  4. Shorter code: 3! vs fact 3, f 3! vs fact 3 |> f

The disadvantages of making this adjustment to F# are

  1. Harder parsing inside the compiler
  2. A new symbol is used, leaving fewer choices for a new F# language-wise operator in the future (we could use combinations starting with ` though. Or we could make the postfix operator starting symbol `. Or even with starting with ' because a type parameter cannot appear in that place. Maybe even \ because it must not appear in operator usage as it is hard to read, but allowing it only as a operator-symbol-chain starting character seems reasonable.

Extra information

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

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
Copy link
Member

cartermp commented Jan 3, 2019

In theory I like this for ! since it matches the mathematical notation - are there other applications you have in mind?

A main concern is that most peoples' experience and expectations are around the postfix ++ and -- operators, which are inherently mutable things.

@abelbraaksma
Copy link

How would the compiler disambiguate between x! and !x? I'm not sure there will be a parsing problem, but operator declarations can return function items, i.e. they can take any number of arguments,not just one.

If that doesn't cause existing code to break, I'd be all for this, though, with similar flexibility as for prefix operators. I've had a few situations where postfix would have improved readability.

PS, can you update the title in your proposal? It still says 'Title of suggestion'.

@rmunn
Copy link

rmunn commented Feb 6, 2019

I can see a potential parsing problem. How should f x! y be interpreted?

  1. As f (x!) y, i.e. take x and apply the postfix ! operator to it, so f is called with two parameters, the value of x! and of y.
  2. As (f x) ! y, i.e. treat ! as an infix operator; calculate f x and then calculate the result of applying an infix ! operator to the values of f x and of y.
  3. As f x (!y), i.e. treat ! as a prefix operator, calling f with the values of x and of !y.

If multiple ! operators (one postfix, one prefix, and one infix) have been defined, how does the parser distinguish them? One way could be by whitespace: f x! y is a postfix operator and parses as f (x!) y, f x ! y is an infix operator and parses as (f x) ! y, and f x !y is a prefix operator and parses as f x (!y). But Don Syme has said elsewhere that a design principle for F# is that adding whitespace should not change the meaning of an expression (indentation excepted, of course). So I don't like the idea of making whitespace significant in determining how operators should parse. OTOH, there's the prefix - precedent to consider: f -3 means "call f with the parameter -3 (negative three)", while f - 3 means "subtract 3 from f". So maybe it wouldn't cause an issue to deal with postfix operators in the same way?

@abelbraaksma
Copy link

@rmunn Whitespace can actually currently change the meaning of an expression. Prefix operators have higher precedence than function arguments, but only when there is no whitespace. Otherwise it gets parsed as an infix operator.

Also, there are some rare cases where whitespace is problematic, for instance, when () is a proper argument to a function, and the preceding argument is itself a function item. And recently, I noticed parsing issues with operators of three or more characters in size.

These aren't necessarily bugs, just stating that some shift in meaning is apparently already baked in.

@jkone27
Copy link

jkone27 commented May 15, 2020

Can this feature be possibly useful to override the OCaml (;) operator and use it instead for |> ignore ?

this would be a great benefit for people transitioning from C# or javascript!

@abelbraaksma
Copy link

abelbraaksma commented May 15, 2020

@jkone27, I don't think you can override ;, it's special, it's part of the syntax, and it already means "end of statement" (like in C#), allowing multiple statements on a single line. You may designate a different operator, provided that that operator is allowed as unary operator (and provided that this feature is implemented).

However, I don't think it'll help you much, if anything, unary operators have some of the highest precedences, and if postfix were allowed, I think they'll also get high precedence. Meaning, you'll have to wrap your statement into parenthesis for your operator to work as you intend.

Having said all that, coming from C#, I think it's best that one of the first things to learn is to stop using semicolons. And each time you need ignore, and it isn't for interaction with the BCL, to think hard if it is actually correct. It may mean you are ignoring something you shouldn't ;).

More and more, I've come to find let _ = ... a better readable statement that signifies I'm not interested in the result. But that's my personal taste. It also pairs better with computation expressions.

@jkone27
Copy link

jkone27 commented May 16, 2020

@abelbraaksma i know, but just consider when using opinionated .net libs in F# like aspnetcore, and especially for people transitioning to the language, example from a sample repo I did for my company: https://github.com/jkone27/FsharpWebApi/blob/master/FSharp/Startup.fs

is the common usage of ; operator so common in F# code? as a frequency of usage?

this is aspnetcore (which would be like a big percentage of .netcore market share),
without the prefix ! operator i introduced for demo purposes, this would look quite ugly

member _.Configure(app: IApplicationBuilder, env: IWebHostEnvironment) =
        if env.IsDevelopment() then
            app.UseDeveloperExceptionPage() |> ignore

        app.UseHttpsRedirection() |> ignore
        app.UseRouting() |> ignore

        app.UseAuthorization() |> ignore

        app.UseEndpoints(fun endpoints ->
            endpoints.MapControllers() |> ignore
            endpoints.MapGet("/", fun context -> context.Response.WriteAsync("Welcome to F#!")) |> ignore
            ) |> ignore
 
        //needed for swagger
        app.UseSwagger() |> ignore
        app.UseSwaggerUI(fun c ->
            c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1")
        ) |> ignore

vs

member _.Configure(app: IApplicationBuilder, env: IWebHostEnvironment) =
        if env.IsDevelopment() then
            !app.UseDeveloperExceptionPage()

        !app.UseHttpsRedirection()
        !app.UseRouting()

        !app.UseAuthorization()

        !app.UseEndpoints(fun endpoints ->
            !endpoints.MapControllers()
            !endpoints.MapGet("/", fun context -> context.Response.WriteAsync("Welcome to F#!"))
            )

        //needed for swagger
        !app.UseSwagger()
        !app.UseSwaggerUI(fun c ->
            c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1")
        )

i know in ocaml ";" is chaning side-effect operations, but here it doesn't seem to work if i used it, as the compiler still shows me wriggles in the intelliense,
and that's why |> ignore had to be introduced I guess ? else if i had a;b;c;d;() --> inferred return type would be unit and all would be fine for compiler.

Probably i should open a separate issue for this :) but anyway also having ! as postfix custom already solves quite a lot of this visual "stress"

@Happypig375
Copy link
Contributor Author

Why don't you use fluent calls just like C#?

member _.Configure(app: IApplicationBuilder, env: IWebHostEnvironment) =
    (if env.IsDevelopment() then app.UseDeveloperExceptionPage() else app)
        .UseHttpsRedirection()
        .UseRouting()

        .UseAuthorization()

        .UseEndpoints(fun endpoints ->
            endpoints.MapControllers()
                .MapGet("/", fun context -> context.Response.WriteAsync("Welcome to F#!"))
            |> ignore
        )

        //needed for swagger
        .UseSwagger()
        .UseSwaggerUI(fun c ->
            c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1")
        )

@abelbraaksma
Copy link

abelbraaksma commented May 16, 2020

having ! as postfix custom already solves quite a lot of this visual "stress"

I disagree, I find your code quite readable. The ! operator I would localize, though, as it is defined by F# as well (unless you're sure you'd never use it in a different meaning).

It's quite common in F# to locally override !, I've seen it in many code bases.

I had the same thought as @Happypig375, you can just use method chaining. Another alternative is to use x.Foo() |> fun x.Bar() |> fun x.Whatever() (but this isn't necessarily cleaner).

Similar code can be seen with StringBuilder, it returns itself after each action. If such code is local to one file, you can use #nowarn with the warning you want to disable. That too is quite common.

is the common usage of ; operator so common in F# code? as a frequency of usage?

No, it is very rare, as in most cases it clutters the code. Prefix operators are obvious, postfix, esp at end of lines or in the middle, are not, you easily glance over them. In 250K lines in one of my projects, I see it only 12 times for short let... in doX(); doY() statements. I'll probably get rid of those, F# is best readable when statements are vertically separated (one line per function call or statement).

@jkone27
Copy link

jkone27 commented May 16, 2020

I agree with both of you, I just say ppl coming from C# would find it more useful to have a ; (postfix) operator as well as from JS, because it's so hard-wired in their brain 🧠 😆

  • copy pasting from C# code would compile in F# , in most of the cases, with minor additions

@abelbraaksma
Copy link

abelbraaksma commented May 16, 2020

copy pasting from C# code would compile in F# , in most of the cases, with minor additions

That's, unfortunately not going to work, even with such operator. In fact, you can leave ; at the end of each line anyway. Your real worries are that assignments are not allowed, var is not a keyword, and the order of declaration is inverse (type comes before the identifier in C#, and after, though usually absent, in F#). And then I didn't yet mention the whitespace rules ;), or te very powerful static typing and inlining, not available in C#. Often, when I transliterate code from C# to F# I find hidden bugs and overall the code becomes much smaller, and (suggestive) way more readable and easy to follow. But the learning curve is there, for sure.

But certainly, we've all been there, and indeed, when I switch between languages, I often forget about ; in C#, getting weird errors. The inverse is not true, habitually adding a ; after each line is a no-op in F#.

@jkone27
Copy link

jkone27 commented May 16, 2020

yeah I know F# (at least keywords) i am just struggling to make it friendly to C# people , it's not my "own" struggle :) , i believe i will keep for now ! prefix custom without nowarn, also i don't like wrapping stuff in parenthesis, it would also sounds "hacky" to C# people i believe. I was trying to make it look good to them 👍 thanks!

@abelbraaksma
Copy link

abelbraaksma commented May 16, 2020

@jkone27, I've been through the same struggle. For me, the biggest challenge was learning to think functional, and understanding functional domain modeling. Ultimately, letting go of classes and hierarchies was one of the best things that happened in my programming career. It subsequently improved my C# skills too.

Esp. in the early days on that path, I learned a great deal from this book by Thomas Petricek: https://www.amazon.co.uk/dp/1933988924, if you don't already have it, I can suggest it, also for your co-workers. On DU's and monadic style, I found the explanation with the adage "in the box, out of the box" from https://fsharpforfunandprofit.com/ very helpful to understand bind, apply, map and friends.

(though on the ! prefix, don't forget to tell people that method chaining works just like in C#, see @Happypig375's comments, it's the lowest bar to learn coming from C#, while overriding operators, on the other hand, is not really newby stuff, and in this particular scenario, just complicates things more)

@jkone27
Copy link

jkone27 commented May 24, 2020

@abelbraaksma @Happypig375 thanks, the "concern" was more for an educational/teaching perspective, thanks. I know many sources that you quoted here already, but I think the language should be as friction-less as possible for entry-level folks we need to "initiate" (considering myself a level2 entrylevel guy haha)

And the main reason was just that I rarely see usage of ";" operator in real code, that's why i thought it would be nice to be able to override it at will, for sure when you need the original you should just be able to use it. I also understand it's not possible to easily override ";" being in the core of the lang (coming from ocaml), and also that I still have no clue of the internals of the compiler unfortunately.

But thanks a lot! Have a nice day guys : ) F# is awesome

@secana
Copy link

secana commented Jun 29, 2020

Really nice idea. A use case could be Rust like error handling for Result types: https://doc.rust-lang.org/edition-guide/rust-2018/error-handling-and-panics/the-question-mark-operator-for-easier-error-handling.html

A post fix "?" operator would take a lot of noise from from F# code with many "return on error" cases.

@Happypig375
Copy link
Contributor Author

@secana I think the idiomatic way for that is computation expressions.

@dsyme
Copy link
Collaborator

dsyme commented Sep 9, 2021

I'm closing this as it is not planned to add further complexity to the set of available operators in F#, there is already too much unnecessary symbolic complexity (particularly which beginners are over-exposed to) and our aim will be to reduce it in various ways

@dsyme dsyme closed this as completed Sep 9, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants