fsharp / fslang-suggestions

The place to make suggestions, discuss and vote on F# language and core library features
344 stars 20 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:

7sharp9 commented 7 years ago

Having tooling that works with currying would be nice rather than only tupled syntax.

Rickasaurus commented 7 years ago

The issue I see with the auto-convert module thing is what do you do about overloads? Even in that example, String.Split has overloads.

I'm really fond of the _.Property almost as a way to make property access nearly first class. It seems like a halfway solution though as this will only help with access and not updating records.

Maybe something like?

{ _ with x = newVal }

How might this interact with pattern matching? It would be ideal to do something like:

match x with 
| _.Foo.Bar.Baz as baz -> ...

Also, what about passing them in as active pattern parameters?

Just spit-balling here so don't take my syntax too seriously. I think though that if we're going to add something lens-like to the language we should consider these other use cases as well.

haf commented 7 years ago

@Rickasaurus I was just thinking that it could add a numeric let split s inst = ; let split2 or let split ...; let splitCharArray if you like it more verbose. The obstacle for me when I program F# is that I have to rewrap all instance methods in modules for every project so I can use point-free programming – but more importantly; use currying. My suggestion is not more verbose if you assign _ as the module name either, so VeryLongTypeName would not come into play.

Rickasaurus commented 7 years ago

@haf If this passing types into type provider thing works out then you could solve the problem that way. This would allow a lot of different experimentation for such a feature. I think this is a really hard problem to solve without a bunch of iteration.

dsyme commented 7 years ago

We should try to get completely away from having to 'dot into' objects at any point.

@varon Yes, @vasily-kirichenko is correct in this comment - this is a misunderstanding of the relationship between F# and various aspects of notation associated with object programming. F# embraces appropriate use of dot notation (though that's by no means the same thing as embracing or emphasizing all aspects of object-orientation). That embrace brings some dissonance, and this suggestion is really about resolving one remaining part of that dissonance.

varon commented 7 years ago

@dsyme Thanks for the clarification.

I'm genuinely surprised to hear that dot notation is still considered something to be embraced. While I'll totally defer to your expertise here, I'm really curious as to what the reasoning was behind this, and specifically why In general, in F#, the use of object-oriented programming is preferred as a software engineering device.

Where do you see F# as a language in a few years? Where does it aim to position itself? C# with ML syntax? Haskell.net? Something else?

dsyme commented 7 years ago

I'm genuinely surprised to hear that dot notation is still considered something to be embraced.

@varon Pretty much every F# book and F# sample introduces the use of the dot notation and type-directed name resolution, and always has done since F# 0.1. For one view, see Expert F# 4.0 ch 6.

It would clearly be odd to force the use of DateTime.getYear date for date.Year - 21 characters instead of 9 - and likewise for thousands and thousands of other properties in decently designed .NET types. DateTime has problems, but notation is not particularly one of them. And it is extremely hard to argue that a DateTime API that doesn't use properties is a better design according to any specific utilitarian metric. The question in F# coding is not whether dot notation should be used, but how much. The component guidelines give one perspective on where to draw the line on that.

varon commented 7 years ago

@dsyme Thanks for the detailed answer.

TIHan commented 7 years ago

I really like the suggestion made here #506.

.bar or (.bar) (or whatever syntax) be precisely shorthand for (fun (x: ^T) -> (^T : (member bar : 'U) x), so let inline f xs = List.map (.bar) xs would get a generalized generic type.

Once I read that, I'm beginning to be more in favor of the original suggestion as long as we can have the mentioned generalization. :)

dsyme commented 7 years ago

@TIHan Yes, it's a very simple addition

7sharp9 commented 7 years ago

Simple is good, one step at a time.

On 25 Nov 2016 12:04, "Don Syme" notifications@github.com wrote:

@TIHan https://github.com/TIHan Yes, it's a very simple addition

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

Rickasaurus commented 7 years ago

The List.map (.bar) xs example is nice looking but I worry that it might not work very well with the type inference. Currently even explicit record access often gets mucked up. A good example of this ambiguity would be something like List.map (.Length). Where would you put the type annotation?

cloudRoutine commented 7 years ago
List.map (.Length: 'a ->int)

same as

let inline length rekt = (^T : (member Length : int) rekt)
let inline fn ls = List.map (length:'a->int) ls

or just

let inline fn ls : int list = List.map length ls
let inline fn ls = List.map length ls : int list 

make it generic for fun

let inline length rekt = (^T : (member Length : 'r) rekt)
let inline fn ls = List.map length ls : 'b list 

I don't think there's much ambiguity, the type with either be inferred from the value you pass to it, or it's in an inline function so it'll be determined at site, or you explicitly label the type in one of the several places that are available.

Rickasaurus commented 7 years ago

List.map (.Length: 'a ->int)

Hrm, yeah. That's not bad at all.

dsyme commented 7 years ago

Adjusted the title to match what feels like the most likely versions of this proposal

smoothdeveloper commented 7 years ago

@dsyme could this suggestions also include other member (than property) access?

dsyme commented 7 years ago

@smoothdeveloper No, this suggestion doesn't cover that.

odytrice commented 7 years ago

I really like this exprs |> List.map _.PropertyName or we could use exprs |> List.map #PropertyName which aligns with Standard ML and reduces the context overload of . and _

Rickasaurus commented 7 years ago

No, this suggestion doesn't cover that.

It seems like it could be pretty sweet using this for member access, especially with collections of so called fluent api classes which are so popular in C# these days.

xp44mm commented 6 years ago

The new keyword is the easiest method to implement this functional. overload some structure (_.name, (.name) and so on ) will bring some problems that we may do not know. I have some new keywords to candidate:

books |> Array.map which.Author // longer, new keyword *which*
books |> Array.map __.Author // double _ (double underline)
books |> Array.map '.Author // after '. inteliSence will popup
books |> Array.map fun'.Author // prompt this is a fun
books |> Array.map lambda.Author

all the above candidates is context-free. and avoid the problem about white space. for example:

let singleAuthor = getSingleBook() |> which.Author

anywhere or/and everywhere:

which.Author // === `(fun x -> x.Author)`
which<Book>.Author // === `(fun (x:Book) -> x.Author)`

it is easier for compiler or human reader to read or understand.

by the way, inteliSence prompt list is very important. when you typed __., VS will popup a select list box to select a member property.

vasily-kirichenko commented 6 years ago

@xp44mm "which" is a long word. Kotlin uses "it" - short and readable.

Rickasaurus commented 6 years ago

Interesting. "it" already has a top level meaning in FSI as the previous value. I don't think it sticks out as much though which is a downside.

Tarmil commented 6 years ago

it is a perfectly valid identifier though, like most of @xp44mm's proposals, so either we turn it into a keyword (which is a breaking change) or we add to F# the concept of identifiers that have special meaning in certain contexts (the context being "there is no value called it in scope", which is extremely error-prone). Either way, I'm not a fan.

I just realized that fun.Prop is also a possibility, and a non-breaking one (since it is currently always invalid syntax). I think it expresses the intention fairly well.

21c-HK commented 6 years ago

If we translate _.property to an inline function with the property as an member constraint of a statically resolved type parameter as suggested by @dsyme in https://github.com/fsharp/fslang-suggestions/issues/506#issuecomment-258126097, then some one who has not seen the syntax before could hover of the ._property part and see the inline function definition in a tooltip and therefore immediately understand what this syntax does (assuming the reader is familiar with member constraints on statically resolved type parameters).

21c-HK commented 6 years ago

Note that the OP (@wastaz) was asking for first-class record fields like in Elm (http://elm-lang.org/docs/records), but Elm does not have objects, only records and unions and even in Elm .field is just a shorthand for \record -> record.field where \ maps to fun in F#. Elm introduced the .field syntax because it supports polymorphic records via row polymorphism, which F# does not, but it is equivalent to structural subtyping, which F# supports via member constraints on statically resolved type parameters.

I just wanted to give a bit of context why I think that translating _.property to an inline function with the property as an member constraint of a statically resolved type parameter as suggested by @dsyme in #506 (comment) solves the problem exactly the same way as Elm does.

dsyme commented 6 years ago

The proposal here seems to resolve towards adding

(.Prop)

or

_.Prop

to the language as a shorthand for

(fun (obj: ^T) -> (^T : (member Prop : 'U) obj)

I would still be interested in a straight-up vote between _.Prop and (.Prop) though. Please 👍 for (.Prop) and ❤️ for _.Prop, e.g.

👍for this:

xs |> List.map (.Prop) |> List.map (.AnotherProp) |> (.FinalProp)

❤️ for this:

xs |> List.map _.Prop |> List.map _.AnotherProp |> _.FinalProp
vasily-kirichenko commented 6 years ago

I'm for __.Prop.

.Prop looks too "unfinished" (i.e. my brain is expecting something before the dot)

vasily-kirichenko commented 6 years ago

@gusty why? It'd remove a lot of noise from code interacting with OO APIs.

dsyme commented 6 years ago

@vasily-kirichenko I think .Prop is not on the menu. Did you mean to write _.Prop or (.Prop)?

dsyme commented 6 years ago

@vasily-kirichenko I think that was a vote for _.Prop. Maybe I should change 👎 to ❤️

vasily-kirichenko commented 6 years ago

Oh, than I'm 100% for __.Prop.

dsyme commented 6 years ago

@gusty Changed to ❤️ , please adjust your vote :)

gusty commented 6 years ago

@vasily-kirichenko note that the proposed underscore is a single one, not double. Which I think makes sense.

vasily-kirichenko commented 6 years ago

Ah! Even better :) It's ok to steal good syntax from other languages :)

susl commented 6 years ago

Would _.Prop.ChildProp (or (.Prop.ChildProp)) also work?

xuanduc987 commented 6 years ago

@susl I don't think _.Prop.ChildProp would works, but (_.Prop >> _.ChildProp)

OnurGumus commented 6 years ago

Nemerle also uses _.Prop. I am all in for that.

vasily-kirichenko commented 6 years ago

I think _.Prop.ChildProp.GrandchildMethod().ExtensionProp.ExtensionMethod() should work.

isaacabraham commented 6 years ago

I normally don't like the use of _ but in this case it works quite nicely:

  1. We already use _ for the "nameless" binding.
  2. Don't have to use ( ).

Both are an improvement over what we have now, but I prefer the _. syntax.

chillitom commented 6 years ago

how about ..Prop or even ...Prop?

geirive commented 6 years ago

❤️

HoraceGonzalez commented 6 years ago

Isn't method already a reserved keyword? Eg. method.Prop

dsyme commented 6 years ago

❤️ wins the day

Rickasaurus commented 6 years ago

I vote for _.Prop as always having to wrap in parens makes the syntax different than most other stuff in F#.

mathias-brandewinder commented 6 years ago

Would that also handle indexers, ex: xs |> _.[1..] or xs |> (.[0]) ?

dsyme commented 6 years ago

Would that also handle indexers, ex: xs |> _.[1..] or xs |> (.[0]) ?

Very good question. I suppose not since as described in this suggestion it even doesn't handle method calls, and indexers are sugar for method calls.

TIHan commented 6 years ago

I'm for _.Prop :)

vasily-kirichenko commented 6 years ago

There are a lot hidden scala lovers here ;)

enricosada commented 6 years ago

Would that also handle indexers, ex: xs |> _.[1..] or xs |> (.[0]) ?

this is a really cryptic to read. please don allow that. seems code golf.

But this allow other method chaining?

kotlin has the nice it inside a block, so you can shorthand the whole (fun x -> x > 0 )

val positives = list.filter { it > 0 }
val names = list.filter { uppercait.Name > 0 }

so this proposal will work also for

xs |> List.map _.Prop.ToUpper()

or

xs |> List.filter (_.Age > 0)

or

xs |> List.map (uppercase _.Name)

?

realvictorprm commented 6 years ago

I agree on the cryptic access... annnd I like the it stuff too although without the brackets. Please no brackets 😄

EDIT: What was the disadvantage of using xs |> List.map fun.Name ? I prefer this one even more.