fsharp / fslang-suggestions

The place to make suggestions, discuss and vote on F# language and core library features
344 stars 21 forks source link

Simplified syntax for lambda #634

Closed enricosada closed 2 years ago

enricosada commented 6 years ago

I propose we add a simplified syntax for lambda. this helps to cover some scenarios, not to completly replace the fun syntax

A scenario: F# allow easy value transformations (map, etc), who lots of time use simple rules, so short lambda

The existing way of approaching this problem in F# is like

xs |> List.map (fun x -> x.Name)
xs |> List.map (fun x -> x + 2)
xs |> List.map (fun x -> x.[1])
xs |> List.map (fun x -> Some 3, Very(Complex(x, (fun y -> x + y)))))

Current:

Two proposed solutions:

  1. xs |> List.map (x -> x.Length +1) drop the fun keyword
  2. xs |> List.map (x => x.Length +1) drop the fun keyword, use fat arrow
  3. xs |> List.map ( it.Length + 1 ) use new it in block

Already proposed variant, but who cannot use the value like (fun x -> x + 2) so discarted it for this proposal for a generic lambda (is discussed in #506),

1 Drop fun

This shorten just a bit the sintax

xs |> List.map (x -> x.Name)
xs |> List.map (x -> x * 2)
xs |> List.map (x -> x.[1])
xs |> List.map (x ->Some 3, Very(Complex(it, (y -> x + y)))))

2 Drop fun, use fat arrow

This shorten just a bit the sintax, and is familiar with other lang like c#.

xs |> List.map (x => x.Name)
xs |> List.map (x => x * 2)
xs |> List.map (x => x.[1])
xs |> List.map (x =>Some 3, Very(Complex(it, (y => x + y)))))

3 use a new keyword it like :

xs |> List.map ( it.Name )
xs |> List.map ( it * 2 )
xs |> List.map ( it.[1] )
xs |> List.map ( Some 3, Very(Complex(it, (fun y -> it + y)))))

Rules:

Some variations:

Variation 1

Variation 2

Variation A

xs |> List.map it.Property
xs |> List.map it.Property1.Property2    
xs |> List.map it.Method
xs |> List.map (it.Method arg)
xs |> List.map (it.Method(1,2))
xs |> List.map (it.Method().Property)
xs |> List.map (it.Method(1).Property)
xs |> List.map (it + 1)
xs |> List.map (it > 10)
xs |> List.map it.[1]
xs |> List.map it.[1..10]

Variation B

Pros and Cons

The advantages of making this adjustment to F# are to allow simplified lambda block, with a consistent sintax for both shortcuts like

The disadvantages of making this adjustment to F# are:

Extra information

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

Related suggestions:

More related work:

Affidavit (please submit!)

Please tick this by placing a cross in the box:

Please tick all that apply:

dsyme commented 6 years ago

I don't understand this rule:

every lambda has it's own it in scope, which shadows any outer lambda(s) it

My interpretation of your rules is that (...) introduces a new lambda if and only if there is a use of it within the parentheses (and not nested in other parentheses). But you show:

xs |> List.map ( Some 3, Very(Complex(it, (fun y -> it + y)))))

as one of your examples, where it is two nestings of parentheses above where you want the implied lambda to be. (My understanding is you are not proposing a type-directed rule for the location of the implied lambda)

Like most people I generally am uneasy with rules that are so sensitive to addition/removal of parentheses - F# already uses these in a few places but we've tried not to make it pervasive in the basic expression-oriented part of the language

gerardtoconnor commented 6 years ago

Thanks @enricosada

Like most people I generally am uneasy with rules that are so sensitive to addition/removal of parentheses - F# already uses these in a few places but we've tried not to make it pervasive in the basic expression-oriented part of the language

Agreed, there will be a lot of related bugs/issues raised when it's this sensitive, will frustrate more than help people.

theprash commented 6 years ago

Without an explicit lambda boundary even quite simple expressions become ambiguous:

(f2 (f1 it))

What about -> to mark the start of a lambda after an opening paren?

(-> f2 (f1 it))

Or with an underscore instead:

(-> f2 (f1 _))

I prefer the underscore because it stands out more as a language feature but wouldn't want it to clash with #506. Would the following lambda return the property or a function to get the property?

(-> _.Prop)
gerardtoconnor commented 6 years ago

@theprash I think you have nicely highlighted that as you try to remove ambiguity ... you end up nearly where you started, and for the sake of 1 less arg char/val is it worth it!?

I think (-> _.Prop) bit is ugly and better off sticking with #506 format of _.Prop : 'T -> 'Prop so it is just creating a function (needs input object to extract property), and no parenthesis needed for prop but needed on method call (with args) (_.Method())

enricosada commented 6 years ago

every lambda has it's own it in scope, which shadows any outer lambda(s) it

sry @dsyme i'll remove that. i liked the discussion about this in #506 and tried to summarize it, that rule was part of the discussion.

Later, while writing the nested usage example, i found the it to be ambiguous and bad ihmo.

in xs |> List.map ( Some 3, Very(Complex(it, ( it + it ))))) the it doesnt do what you can think, so is error prone ihmo. Is hard to see that it + it is not the other lambda it, but was an additional lambda equal to (fun y -> y + y) ) so i think was worth add a restriction to usage (no nested it)

Added Variation 1 and Variation 2 about nested

Like most people I generally am uneasy with rules that are so sensitive to addition/removal of parentheses

again, trying to write down the examples, making optional the parens may helps with some scenarios, but some of these can be better done with_.Prop sintax for example (#506 if this doesnt replace it, making that _.Prop a special case of it.Prop when is anonimous).

List.map _.Prop vs List.map it.Prop vs List.map ( it.Prop )

Anyway, allow the optional parens add lot of corner case to discuss like List.map it.[0], List.map it.Prop.ToUpper() who can be easier (and not too much verbose) just using the normal form with parens List.map ( it.[0] ) and List.map ( it.Prop.ToUpper() ).

You added some in the https://github.com/fsharp/fslang-suggestions/issues/506#issuecomment-346673643 about require require parentheses if the expression is non-atomic

xs |> List.map it.Property
xs |> List.map it.Property1.Property2    
xs |> List.map it.Method
xs |> List.map (it.Method arg)
xs |> List.map (it.Method(1,2))
xs |> List.map (it.Method().Property)
xs |> List.map (it.Method(1).Property)
xs |> List.map (it + 1)
xs |> List.map (it > 10)
xs |> List.map it.[1]
xs |> List.map it.[1..10]

@dsyme added Variation A and Variation B for parens

Rickasaurus commented 6 years ago

I'm really not a fan of it, I know it's just aesthetics but it looks really ugly to me.

gerardtoconnor commented 6 years ago

Yeah ... can we kick it?

gerardtoconnor commented 6 years ago

... oh ... no Tribe Called Quest fans in the audience ... 😒

voronoipotato commented 6 years ago

I think it also overlaps with the "_" style syntax being discussed on the other issue, vasily proposed it but he relented later, I don't like it,I'm sure it's great for Kotlin but, I think it's bad for F#. I'm a fan of either dropping fun, or dropping fun and using fat arrow. I think the fat arrow will be more clear to javascript and C# users, but I think someone had discussed that fat arrow was already being used by some F# projects. I've personally always found "fun" to be visually noisy. It doesn't provide any information that can't be gleaned from a few characters ahead.

rmunn commented 6 years ago

I would generally be in favor of removing fun, but I do have questions. For example, might there be some ambiguous situations involving other meanings of ->? For example, what happens if I have a match that needs to return a function?

match operation with
| Add n -> (+) n
| Sub n -> x -> x - n
| Collatz -> x -> if x % 2 = 0 then x / 2 else x * 3 + 1

Will the Collatz line parse? What about the Sub n line? And how hard will it be to tweak the parser to make those lines work?

For reference, here's how that match statement would be written with fun:

match operation with
| Add n -> (+) n
| Sub n -> (fun x -> x - n)
| Collatz -> (fun x -> if x % 2 = 0 then x / 2 else x * 3 + 1)
voronoipotato commented 6 years ago

-> is right associative, so I think it would parse out correctly but I nobody should rely on my word.

isaacabraham commented 6 years ago

Why wouldn't deconstruct be possible e.g. given

List.map(fun (a,b) -> a + b)

why would this not be doable

List.map((a,b) -> a + b)
xperiandri commented 6 years ago

In general it looks like a nice idea however the root issues is code generation absence.

Just add snippets support to Visual Studio for F# and it will be much more pleasant to write. Meanwhile the only solution to avoid waste typing is to use CodeRush with my templates https://github.com/xperiandri/CodeRushTemplates

You just type fun + Space Bar and have template expanded.

voronoipotato commented 6 years ago

@xperiandri no, the root issue is not code generation absence. You still have to read fun over and over despite it providing no useful information. It's cool that you wrote snippets that cut out some of the typing but it does not eliminate the reading.

wanton7 commented 6 years ago

I'm F# beginner, i've been writing C# professionally 12 years. "it"suggestion looks ugly as hell to me and i feel it would be a taint to this beatiful language. I've written lot and lot of lambdas in C# , but after using F# for a while and => feels very unnatural for F# because -> is already used with fun keyword. I think it would make language harder to read compared to just making fun keyword optional.

voronoipotato commented 6 years ago

@wanton7 yeah I don't really understand the "it" suggestion. I think some libraries already use => for a different meaning. I've not seen any reason why we can't just eliminate fun without consequence.

miegir commented 6 years ago

I don't like the 'it' version because it is too restrictive, 'it' syntax supports only 'fun it -> ...'. In fact, I think that 'function' can also be used instead of 'fun'. The 'function' can always be used instead of 'fun' and is more powerful. With 'funtion', the 'match' keyword is not needed, because you can rewrite 'match x with ...' as 'x |> function ...'. In other words, I would like if there will be a shorter syntax for 'function' lambda, one with '->' or one with '=>' such as xs |> List.map (0 -> "zero" | x -> string x)

voronoipotato commented 6 years ago

oooh interesting. That does sound even more useful.

miegir commented 6 years ago

Oh, my mistake was that fun can always be replaced with function. It is not correct when fun of more than one argument is needed. For example, xs |> List.mapi (fun i x -> ...) cannot be rewritten with shorter lambda syntax that expands to function. But in this case, continuing to using fun is just 4 symbols longer than possible syntax without keyword, and perhaps this case is rarer than case with one argument.

rmunn commented 6 years ago

That's an interesting question: would functions of arity 2 or higher* be given a shorthand syntax as well? I.e., would (fun a b -> if a > b then a - b else b - a) be shortened to (a b -> if a > b then a - b else b - a)? And would that cause any ambiguities?

My inclination is to say that this should not be allowed, since it's too ambiguous: a b without a leading fun looks too much like a function call, so (a b -> if a > b then a - b else b - a) looks like you're calling a with a lambda (fun b -> ...) as argument. I'd say that if you're wanting a simplified syntax for a two-argument function, you either have to write fun a b -> ... as before, or else you can use the simplified (and curried) syntax a -> b -> ....

So I'm proposing that fun a b -> ... could be converted to a -> b -> ..., but NOT to a b -> ....

* Of course, all functions in F# are curried so they're really of arity 1. But F# lets you pretend that they're arity 2 or higher via fun a b -> ..., and that's the syntax I'm interested in considering here.

ghost commented 6 years ago

I'm really not a fan of it.

Pros and Cons

The advantages of making this adjustment to F# are to allow simplified lambda block, with a consistent sintax for both shortcuts like

methods (fun x -> x.Name.ToUpper()) to ( it.Name.ToUpper() )

ToUpper should just be a function in my opinion. Seq.map String.ToUpper

As for the fun keyword. I really don't like having two different syntaxes for lambda. I personally don't mind fun but if I were to pick a replacement it would be \

voronoipotato commented 5 years ago

I think "it" is bad and we're all just agreeing that it's bad so I think we can agree to not do "it" and move on from that.

//works perfectly fine
let x = function Some t -> t * 2 | None -> 3
//This constructor is applied to 0 argument(s) but expects 1
//not sure why it's totally okay for function but not okay with fun
let y = fun Some t -> t * 2

Pondering about this more I'm starting to see the ways that things actually get ambiguous without the fun, even with multiple arrows. At the very least if we're going to keep fun, I would very much appreciate it if we deprecated functionand just copy over function's features to fun. We shouldn't have two sets of functionality that are slightly different and one is unexpectedly more robust, especially when the more verbose one is the more robust one and is only used because it has more features than fun. Barring that, maybe this new syntax like \ could be shorthand for function. I also agree that \ is fine shorthand. \ x y -> x * y or \ Some x -> x | None -> 0. It's not used anywhere that I know of but it's visually similar to a lambda which is nice. I think @dsyme already pointed out that he didn't want more parentheses sensitivity and I frankly can't think of a way to accomplish "no fun" lambdas without more parentheses sensitivity. Adding a short idiomatic keyword like \, (which I'll admit I'm not married to) that is sugar for function brings me everything I want. If we can't do that and we're going to keep fun, then we should merge it with function.

abelbraaksma commented 5 years ago

@voronoipotato:

let y = fun Some t -> t * 2

While the error you see can be improved, what it really means is that you are using a constructor (Some) where you probably mean to use a pattern match. I think you mean the following, which is perfectly legal (though a very bad idea, since you are not matching over None):

// legal (but a necessary warning is raised, as it should, for missing None)
let y = fun (Some t) -> t * 2

if we deprecated functionand just copy over function's features to fun.

I beg to disagree. function is not fun. The first is shorthand for a match-expression-as-a-function, the second defines any function. While I'd agree that function as a keyword might've been a poor choice and confuses newcomers, it certainly serves a purpose and I use it judiciously. Basically:

function X -> y | Y -> z

gets desugared to:

fun x -> match x with X -> y | Y -> z

Apart from its current frequent use (mainly in cases where you do not need to access the argument itself, but just need to match over it), I think making fun and function synonymous, or move one over to the other will lead inadvertently to backward compatibility issues, which is a no-go for any feature.

I also agree that \ is fine shorthand

You are right that it is currently not used, as it is an invalid operator character. Though personally, I don't like it, but that's just because I think going from fun to \ has little benefit and saves you two characters typing at the expense of readability.

On the original proposal:

2 Drop fun, use fat arrow

The fat arrow is currently a valid operator. This would also lead to backwards compatibility issues, as has already been mentioned.

I see much more in the proposals elsewhere (the link escapes me) where we allow to create implicit variables for access of properties-as-functions, something like _.Length would be desugared to fun x -> x.get_Length().

matthid commented 5 years ago

While removing the fun keyword would be my favorite I figured I might suggest something crazy to get some other ideas.

How about using some special quotes and an identifier beforehand:

xs |> List.map x`(x.Name)
xs |> List.map x`(x + 2)
xs |> List.map x`(x.[1])
xs |> List.map x`(Some 3, Very(Complex(x, (fun y -> x + y)))))

So <id>`( would be equivalent to (fun <id> -> Most of the time you probably need braces anyway so...

abelbraaksma commented 5 years ago

@matthid, I like where that leads to. But perhaps we can get rid of the declaration completely? Though admittedly, we'd get scoping issues for magic variables:

// using "_" for illustrative purposes, any other "magic" placeholder would probably be better, as "_" now means something else
xs |> List.map _.Name
xs |> List.map (_ + 2)
xs |> List.map _.[1]
xs |> List.map (Some 3, Very(Complex(_, (fun y -> _ + y))))  // scope issue?

Something along those lines would reduce clutter and lead to less parentheses. Whether doable / possible or whether it even gets traction in the community, I don't know, but I often find myself writing the same pattern again and again, esp. when just accessing a property of an instance of a class, or record.

miegir commented 5 years ago

As an idea (another crazy idea) for member access, we could consider it an operator. Very unusual operator though (postfix, consists of non-operator characters and so on). But we can enclose it in brackets to turn into the function:

xs |> List.map (.Name)
xs |> List.map (.[1])

Thus we could do without underscore sign here.

abelbraaksma commented 5 years ago

@miegir, interesting thought, though that'll only work where there's actual class member access needed. In F# it's very common to use module functions instead, and I don't see how this syntax could be expanded to such cases.

realvictorprm commented 5 years ago

The idea of @miegir could be seen as super shorthand SRTP expression.

voronoipotato commented 5 years ago

https://github.com/fsharp/fslang-suggestions/issues/506 is for discussion of the member access. This issue is for discussing how to simplify syntax for lambda. Lets not get derailed into discussing that issue here. @abelbraaksma could you elaborate on the kinds of backwards compatibility issues that merging fun and function could have? It seems to me that it only expands the available options for input.

function Some t -> t | None -> 0
//vs 
fun Some t -> t | None -> 0 
//proposed
\ Some t -> t | None -> 0

If this is certainly the case that it would cause backward compatibility issues then we should create the shorthand for function and deprecate fun. After all let f = function x -> x * 2 is completely valid F#, function is simply superior to fun.

matthid commented 5 years ago

@voronoipotato Honestly, I don't see both implemented. If one is implemented the other one probably gets probably-not tag ;)

voronoipotato commented 5 years ago

@matthid Either way discussing them both in the same place is a mess. I personally disagree since accessing members is definitely definitely not the same as a better syntax for lambdas. Yes you can torture one into the other but it's pretty visually obvious that it's torturing.

matthid commented 5 years ago

It's just that they pretty much solve the same problem. If one is accepted there is no good reason to take the other one. Personally I'd rather have better lambdas then property shortcut (that's why I personally don't see any need to rush #506)

voronoipotato commented 5 years ago

I would rather have both. The fact that you don't want both is fine but that's your opinion. You can state you don't really care for #506 on that issue, however it's in many ways an unrelated issue to this one. Yes you can interpret #506 such that you can write full lambdas with it but I think that's a gross over-expansion of #506 and fundamentally misses the intent of the feature which is simply to access members/properties.

//#634 is way more useful for the broader case but for member accessing....
l |> List.map (\ x -> x.Data)
//Desugars to 
l | >List.map (function x -> x.Data)
//#506 is visually cleaner and more obvious
l |> List.map _.Data
//Desugars to
l |> List.map (fun x -> x.Data)

Anything other than property / member access on the underscore feature is imho ugly as sin and lovecraftian. However property/member access specifically is nice and good, and completely does not step on the toes of actual lambdas.

To be clear I'm not referring to your proposal of x`(x + 2), which I view as a lambda. Since there is a clear declaration of parameters and function body. I'm not entirely sure how that would work with lambdas with multiple arguments since the quote is after the argument however.

abelbraaksma commented 5 years ago

@voronoipotato,

function is simply superior to fun.

I would back to disagree ;). Here are some arguments:

It is true that function shines when you want to match over multiple DU cases, but whenever you need to create a multi-param curried function, your only option is fun.

I think that either merging function in fun or the other way around would always create conflicts, in fact I can't see how it could possibly work without breaking existing code, but I've been wrong before, I just don't see it.

voronoipotato commented 5 years ago

I'm realizing if you wanted to have fun match (or have function accept more than one argument) you probably need to destructure in parentheses like you do in fun. I better understand however how this is out of scope for this issue. Frankly at this point I think if I'm not simplifying the language in any way, and I'm at best saving two characters here, I just don't see how this is worth it. If we could merge the matching function syntax with lambdas in a way that could also make any kind of lambda, and then also have a shorthand for it, that would be insanely useful. It would be clearer and more obvious to beginners since there would be one recommended path for lambdas and it would be aesthetically pleasing. However removing two to five characters from lambdas just seems like yak shaving. If we could create a lambda shaped like this...

\ (Some t) y -> t * y | None y -> y

Bringing the best of lambda and function with a more concise syntax I'm all for this issue. Then we can deprecate fun and function and have one simplified and direct way to create functions. fun of course would continue to exist for ocaml compatibility.

Otherwise I simply don't see a reasonable amount of value for what amounts to yet another way to declare lambdas. We should be clearing cruft and consolidating features where possible. Having multiple ways of doing the same thing can create pain points so it's important that if we add a feature like this, that we can "kill" two others. Creating one recommended path for this. With the release could include a script that safely auto-replaces fun/function with the syntax.

abelbraaksma commented 5 years ago

Then we can deprecate fun and function and have one simplified and direct way to create functions. fun of course would continue to exist for ocaml compatibility.

Note that deprecating anything is typically a no-go unless there are extremely strong reasons for it. I don't decide on language features, @dsyme does, and if the past is any indication, my guess is he wouldn't (ever?) deprecate something so fundamental as function and fun.

I think there's more chance for syntax like in the _.Property shorthand proposal (see https://github.com/fsharp/fslang-suggestions/issues/506) as a shorter alternative for fun. That proposal was already proposed "in principle". And if we look at @enricosada's original proposal here, there's quite some overlap.

voronoipotato commented 5 years ago

Yeah, fun to λ only saves 2 characters anyway. It's not like we can't create font ligatures that do this, because someone already is in the process of adding a fun to λ ligature into firacode. I think adding a third way of doing lambdas is bad. If deprecation can't meaningfully happen it's probably just not worth it.

jannesiera commented 5 years ago

I'd also like to see the 'fun' keyword be made optional. Seems like a pretty popular opinion here. Any chance this will actually make it in the language?

matthid commented 5 years ago

I'd also like to see the 'fun' keyword be made optional. Seems like a pretty popular opinion here. Any chance this will actually make it in the language?

I guess that depends on someone being able to find all the corner cases involved and doing the actual implementation ;)

dsyme commented 2 years ago

Overall this is covered by other issues now, e.g. #168, #506