fsharp / fslang-suggestions

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

Allow _.Property shorthand for accessor functions #506

Closed wastaz closed 1 year ago

wastaz commented 7 years ago

This is a feature that I would love to steal from Elm :) In Elm, if I define a record as such

type Foo = {
   Bar : int
   Baz : int
}

I then automatically get functions such as .bar and .baz that I can use as getters for this record. So instead of writing code like this

foos |> List.map (fun f -> f.Bar)

I can write code like this

foos |> List.map _.Bar

This does make things a lot nicer in larger chains, for example if I have a list that I want to map over

fooList |> List.map _.Bar |> List.max

Questions

Are indexers allowed

xs |> List.map _[3]

Are method calls allowed

xs |> List.map _.M(3)

Is this 1-place placeholder syntax?

xs |> List.map Math.Sin(_)

Is this multi-place placeholder syntax?

xs |> List.mapi (fun i x -> i + x)
becomes
xs |> List.mapi (_1 + _2)

Pros and Cons

The advantages of making this adjustment to F# are

The disadvantages of making this adjustment to F# are

Affadavit (must be submitted)

Please tick this by placing a cross in the box:

Please tick all that apply:

johnazariah commented 6 years ago

Strong preference for _.Member

Where Member is either a property or a record’s member!!

❤️

MarcusKohnert commented 6 years ago

I'm using underscores in lambdas (C#) all the time (persons.Where( => .Name.Length == 2)). Missed it in F#.

isaacabraham commented 6 years ago

What about e.g fun x -> x > 10. Could that be simplified to just _ > 10?

Savelenko commented 6 years ago

❤️

Rickasaurus commented 6 years ago

One of my favorite things about F# is the focus on symmetries, like with value decomposition/composition. This feature seems a little bit useful and may make some code slightly more pretty, but also kind of tacked on to the language with support in patterns and updating records. I hope we can find a way to fill this out in the future.

nosami commented 6 years ago

if you think of _ as "a value goes here", then I think that there is some symmetry and it seems quite natural.

nosami commented 6 years ago

Maybe instead of limiting it to _, you could have _ or _foo ?

claruspeter commented 6 years ago

Please not the brackets. Closing bracket matching is ugly enough as it is without another set of forced brackets.

realvictorprm commented 6 years ago

Hm I can think of good naming then ... _subQueryResult.Name. However I can imagine that this would break existing syntax.

Am 16.11.2017 9:25 nachm. schrieb "Jason Imison" notifications@github.com:

maybe instead limiting it to , you could have or _foo ?

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/fsharp/fslang-suggestions/issues/506#issuecomment-345051634, or mute the thread https://github.com/notifications/unsubscribe-auth/AYPM8FPWMXv8qUNUlPcFgzZB-oA5kBv0ks5s3JocgaJpZM4KoICL .

gusty commented 6 years ago

I think if we use the _ we can later decide to generalize it to handle positional lambda parameters, so it will allow us to write stuff like fun x y -> (x, y) as (_, _) .

fwaris commented 6 years ago

❤️

celsojr commented 6 years ago

I liked _.Prop more

dsyme commented 6 years ago

What about e.g fun x -> x > 10. Could that be simplified to just _ > 10?

The proposal in this suggestion is limited to _.Property. There are various comments advocating for extensions, I've cataloged them below. But they are not yet considered part of the suggestion (and I'm not convinced they should be).

Any of these extensions have quite significant technical ramifications. For example, consider just _.Method. The exact current proposal is that _.P is a shorthand for (fun (obj: ^T) -> (^T : (member P: 'U) obj) and the exact resolution of how P is resolved is determined by existing resolution of statically resolved member constraints. This would mean it could resolve to

This approach has the advantage that it plays nicely with extra generalization available via inlining, e.g.

let inline mapProps xs = xs |> List.map _.P

and here mapProps could be safely used with lists of any type T that has a property/field P.

Now, the static resolution of (member P : ty) constraints could in theory be extended to also resolve to methods (overload resolution would be applied based on the context-inferred ty). That would allow the syntax to be used with _,Method. But care needs to be taken. For example, given

member x.M(y)

does _.M really become (fun x y -> x.M(y))? But then _M x y becomes legitimate syntax for method application instead of x.M(y). Some people might actually love that, and end up writing it often, but it makes me feel a bit odd to have a new way of writing method application at this stage in the language evolution.

Likewise, even with the current suggestion _.P x becomes a syntax for applying a property getter. That's a consequence that certainly gives "two ways to do things" that we haven't had before.

thinkbeforecoding commented 6 years ago

.property fits well with the overall meaning : A placeholder for some value. Let's start with properties now and extend it further where it makes sense.

SergeyZa commented 6 years ago

_.Name

vasily-kirichenko commented 6 years ago

@dsyme SRTP are known to be complilable slow (it’s better in 4.1, but still slow afaik). It's a kind of feature that everyone will use very widely and it will lead to slow compilation in general, which should be avoided at all costs considering the current (not great) compiler speed.

Could _.Xxx be replaced by an equivalent real lambda (fun x -> x.Xxx) on early stage, and be compiled as usual? I.e. anything with shape ( one or more _.xxx constracts ) are immediately converted to a lambda and we are done. It would guarantee no speed regressions and would allow all of the constructs you are mentioned (methods, chaining, etc. List.map (_.Prop1 + _.Prop2 + (_.Method("foo")). That is, _ inside parent would be x in equivalent fun x -> .... The current proposal looks very limited (and easy to implement :)) and surprising for newcomers (like "why the hell it does not work for methods, like in scala or other languages?)

What's more, I don't like how xs |> List.map __.Prop looks. It's too sparse, like xs |> List.map ^ fun x -> x.Prop - both are a bit hard to read because it's harder to see where the lambda starts and ends.

I think we should implement a full blown syntax or not implement it at all.

qmcoetzee commented 6 years ago

One of the things I've enjoyed about Clojures lambda short hand over Scalas is the ability to use the arguments to a function in what ever order you like #(-> [%2 %1]) . I know that wasn't one of the options but it's kinda neat.

qmcoetzee commented 6 years ago

That said _.X would be awesome

isaacabraham commented 6 years ago

@dsyme I'm sort of inclined to agree with @vasily-kirichenko here - one of the nice things about F# is that many of the solutions are generalised and work for more than a specific, single case e.g. computation expressions rather than just async / await etc. etc. I can imagine people immediately trying this _. syntax on all members, not just fields or properties.

I have no clue what would be involved in a more far-reaching solution - it sounds like it would be more complex than just properties (and probably would have more corner cases and likelihood of unintended effects). If the syntax wouldn't change (from a user point of view) between supporting just properties and moving to all members, I'd be happy with a "properties-first" approach. But if there's a risk of having some hybrid syntax or solution that differs when working with all members and just properties, I would rather wait to get a "complete" solution.

vasily-kirichenko commented 6 years ago

I don't think that implementing "full" solution is harder. Just add (_.xxx) syntax as an alternative lambda one on syntax level. Done (maybe I'm wrong, I'm not fluent at the parser internals).

alfonsogarciacaro commented 6 years ago

As as side note, STRP (BasicPatterns.TraitCall in F# AST) are also very difficult to deal with in Fable (specially overload resolution because TraitCall only provides the member name as a plain string), so I'm with @vasily-kirichenko in that I would much prefer _.Prop to be just syntax sugar for (fun x -> x.Prop).

gerardtoconnor commented 6 years ago

I'd favor syntactic sugar approach of taking expression, wrapping lambda, and substituting in the argument placeholder

_.Property1.Property2     >>  fun x -> x.Property1.Property2       // 'x -> 'T
_.Method                  >>  fun x -> x.Method                    // 'x -> unit -> 'T
_.Method(1,2)             >>  fun x -> x.Method(1,2)               // 'x -> (int * int) -> 'T
_.Method().Property       >>  fun x -> x.Method().Property         // 'x -> 'T
_.Method(1).Property      >>  fun x -> x.Method(1).Property        // 'x -> 'T
_ + 1                     >>  fun x -> x + 1                       // int -> int
_ > 10                    >>  fun x -> x > 10                      // int -> bool
_.[1]                     >>  fun x -> x.[10]                      // 'T seq -> 'T 
_.[1..10]                 >>  fun x -> x.[1..10]                      // 'T seq -> 'T 
_.Method(_2,_3,_4)
Some + Very(Complex(_1,_2, (fun _ -> _ + 1)))

Only issue with this is compiler service feedback as ultimately it will need to assess if the value being applied to the expression has the member being used similar to how SRTPs do already ... unless it will just do a wrap/replace operation when being parsed so errors show up as full signature of fun x -> x...?

gusty commented 6 years ago

Regarding replacing the SRTP call with a lambda:

. Type inference will be different, and it will fail in cases like: List.map _.Length ["a"; "aa"] with:

error FS0072: Lookup on object of indeterminate type based on information prior to this program point.
A type annotation may be needed prior to this program point to constrain the type of the object.
This may allow the lookup to be resolved.

unless we move the list to the left side and use the forward pipe. But this is not the case with STRP, it will succeed, at least with the current implementation of the type inference.

. The pressure on the constraint solver will depend on the call site, I mean if it's being called from an inline function with no additional type information (in the above example the list of strings) it will propagate the constraints, but if it's not it will be forced to resolve locally.

theprash commented 6 years ago

@gusty Indeed, if it's not an SRTP function we may also want the ability to annotate the type:

List.map (_:_ list).Length ["a"; "aa"]

I think the SRTP version would be quite useful but I've not run into projects that are very slow to compile. Would very widespread usage actually be a performance concern if it's mainly within non-inline functions?

gusty commented 6 years ago

@theprash I don't like how it looks when annotating, I would prefer to use the standard syntax if full annotations are needed. But that's my personal taste.

Would very widespread usage actually be a performance concern if it's mainly within non-inline functions?

if you talk about run-time performance there won't be any impact in either scenario. Only compile time might be affected, but I think if functions are not inline it will be minimal. In my experience the constraint solver slows down the compilation when there is propagation of constraints, and this a big issue in F# versions previous to F# 4.1.

I've seen these days many people complaining about slow compilation time and blaming the constraint solver but in most cases I think the issue is that F# type inference itself is the issue, even removing the SRTP would not help.

dsyme commented 6 years ago

I'm enjoying the discussion. Just a couple of small responses:

SRTP are known to be complilable slow (it’s better in 4.1, but still slow afaik).

Just to say my belief is that this is not a blocking concern for

Recall that a + b is compiled using SRTP in F#, along with many other operations, and no one has complained about it.

Could _.Xxx be replaced by an equivalent real lambda (fun x -> x.Xxx) on early stage, and be compiled as usual?

Yes, that is an option. @gusty indicates some of the ramifications correctly.

Re these:

_.Method(1,2)             
_.Method().Property   
_.Method(1).Property 
_ + 1                    
_ > 10                  
_.[1]                    
_.[1..10]               
_1.Method(_2,_3,_4)
Some + Very(Complex(_1,_2, (fun _ -> _ + 1)))

My gut feeling is to be reluctant to admit any of these into F# as a language (I suppose it's my job to be reluctant :)). I initially suggested (.Prop) partly to avoid the slippery slope in this direction.

vasily-kirichenko commented 6 years ago

Yes, I thought about + and others. OK, the perf is not a concern, but I still prefer this feature to be an alternative syntax for lambdas, no more, no less. About _ placeholder, I personally don't care if it will be it, for example as it works great in Kotlin, but it could be a breaking change.

realvictorprm commented 6 years ago

OK I came to the conclusion that I'm not fine about the proposed syntax yet. I would prefer to keep more consistence and rather use some keyword like it or something similar which expresses that we try to operate on an object with the . .

Moreover I go with others which highly suggest to not limit usage to properties. That would be very confusing.

Writing this stuff feels nice:

personList
|> List.groupBy (it.name)
et1975 commented 6 years ago

+1 to @vasily-kirichenko 's proposal to treat it as shorthand/alias to (fun _ ->)

odytrice commented 6 years ago

I think this feature should only be a shorthand for property accessors. Anything else would become complicated to think about like List.map (_ + 1) in this case, writing it in full is clearer List.map (fun x -> x + 1) The underscore by itself is ambiguous however with a dot and a property, Its pretty clear the focus is on the property e.g. List.map _.Name so in conclusion I think _.Prop should simply be a shorthand for (fun x -> x.Prop) and nothing more

vasily-kirichenko commented 6 years ago

@odytrice write some real code in Scala to feel what such syntax is like before judging.

odytrice commented 6 years ago

I haven't coded in scala before so I can't say anything about how it's implemented there. However, from the very little I know in SML they use map #lab to refer to map (fun x -> x.lab) which is precisely what we want to do. So I saw _.lab as an "F# Version" of the above.

That said maybe if I played around with the idea of having a standalone _ it might not seem as weird to me. but in my mind _ means that its a placeholder for a value like let _,y = method() or in match so having it mean a "function" placeholder would be confusing to me. That's my opinion anyway

vasily-kirichenko commented 6 years ago

I'm just saying that it's very hard to judge a syntax not using it for a while. For example, I found a very strange to using it in Kotlin for the same purpose, but after writing some real code for a week it became very elegant and low noisy for me. Anyway, I'm for full fledged _ / it / .xxx whatever.

Thorium commented 6 years ago

I have to say I'm not in favour of giving existing code multiple meanings.

This is working already:

type MyClass() =
    member _.Name = fun x -> x % 2

    member _.Test =
        [1;2;3] |> List.map _.Name

let runMe = MyClass().Test

...and for that reason I would go for more cryptic List.map (.Name)

ReedCopsey commented 6 years ago

@Thorium That doesn't compile - you need double underscores for it to work.

The code you posted results in error FS0010: Unexpected symbol '.' in member definition. Expected 'with', '=' or other token. at _.Name.

Using _.Prop should be safe - you needed __.Prop before to compile.

[Not suggesting that you shouldn't prefer (.Prop) if that's your preference, but I don't think that argument is necessarily the best reason for it]

Thorium commented 6 years ago

Did work on my fsi. You know how Javascript this-keyword is broken and all the consequences. I wish f# underscore won't became the same.

ReedCopsey commented 6 years ago

@Thorium Curious what version of FSI - I tried it in a couple of versions, all of which fail. As far as I'm aware, member _.Name = should be a syntax error, and not valid...

dsyme commented 6 years ago

I'm just saying that it's very hard to judge a syntax not using it for a while.

What about when reading other people's code?

vasily-kirichenko commented 6 years ago

@dsyme From my experience, any language, which you are not familiar with, looks cryptic and ugly and hard to read (and yes, F# looked very ugly for me at the beginning, especially so loved by everyone the pipe operator). This reminds me discussion on C# new features, which mostly looks funny for any F# dev.

0x53A commented 6 years ago

I have to agree with @vasily-kirichenko.

If you don't know F# (or at least any ML), then it is already basically unreadable. Which isn't actually a bad thing IMO, but just a fact.

cartermp commented 6 years ago

I personally love the syntax for it and found it very easy to read and understand in 2014 when I was first learning Kotlin.

After re-reading examples here, I'm finding myself having trouble parsing all of the underscores. I like them for defining members in F# classes, but I think that's because each one is usually on its own line.

vasily-kirichenko commented 6 years ago

@cartermp I like it as well. Again, I'm for full fledged alternative lambda syntax, that's all.

dsyme commented 6 years ago

I'll admit I had hoped to iterate this suggestion without biting into the "fully-fledged implicit binding lambda syntax" apple :) But the success of the feature in Kotlin et al certainly makes it relevant.

The feature deserves a suggestion of its own as it has quite different tradeoffs (I may have closed such a suggestion already, I can't recall). Among them is the fact that it is not a reserved keyword, though that isn't a dramatic problem since we could just add a warning where it is used today. We would probably also deprecate its use in F# scripting code, I never liked that feature in any case.

As an aside, I think it's safe to assume that a "fully-fledged implicit binding lambda syntax" would require parentheses if the expression is non-atomic, e.g.

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]
xs |> List.map (Some 3, Very(Complex(it, (fun x -> x + 1))))

c.f. today where

xs |> List.map C,M(3)

is not allowed and

xs |> List.map (C,M(3))

is required instead

dsyme commented 6 years ago

@vasily-kirichenko Can I ask, what rule scope do you suggest be used to determine the scope of the it binding? For example in

xs |> List.map (it, ys |> List.map (it + 1))

is that necessarily the same it or not? Can there be multiple it variables in scope at any point, or is there always at most one? Within what scope? For example, in the following what makes these it variables different? Do the enclosing parentheses determine the scope where the implied fun it -> is inserted?

xs |> List.map (it + 1) |> List.map (it + 2)

I suppose it goes without saying that I find the idea of adding an implicit binding of any name into F# code disturbing, it's something we've very long avoided (c.f. no implicit binding of this or self, something I'm very happy we didn't give in to).

vasily-kirichenko commented 6 years ago

@dsyme Every lambda has it's own it in scope, which shadows any outer lambda(s) it. In Kotlin it's simpler because its syntax for lambdas is just a curly braces block: { }. So:

val x: (Int) -> Unit = {
    val y = it

    val z: (String) -> Unit = {
        val z = it
    }

    val h: (Double, Int) -> Unit = { d, i ->
        val `we had "it" dispite the parameters are explicit` = it
    }
}

image

To be honest, I find { x, y, z -> ...use "it" if you like here... } super lightweight and easy to read compared to any other lambda syntax I've seen.

vasily-kirichenko commented 6 years ago

I mean a curly braced block is a lambda:

image

vasily-kirichenko commented 6 years ago

An example of real code https://github.com/intellij-rust/intellij-rust/blob/master/src/main/kotlin/org/rust/cargo/project/workspace/CargoWorkspace.kt#L123-L137

dsyme commented 6 years ago

@vasily-kirichenko Right, though I'm after your a more detailed description of the proposed rules for the F# feature you have in mind, not so much a description of similar features in other languages. I presume you're not proposing curly-brace-is-a-lambda for F#?

vasily-kirichenko commented 6 years ago

I’ll think about it. For instance, anything at a function position surrounded by parens should be threaten as lambda. So, if we infer that an expression must have a function type and enclosed in parens, this is a lambda.

vasily-kirichenko commented 6 years ago

Curly braces as a lambda looks interesting too. Instead of

let result, elapsed = 
    time <| fun _ ->
        let x = ...
        res

we could write

let result, elapsed =
    time {
        let x = ...
        res
    }

and instead of

xs |> List.map (fun x -> x % 2 = 0)

we could write

xs  |> List.map { it % 2 = 0 }

I understand it's a big new syntax though and not everyone would appreciate it.