fsharp / fslang-suggestions

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

Allow _.Property shorthand for accessor functions #506

Closed wastaz closed 10 months 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:

rojepp commented 7 years ago

I really like this suggestion, but it would probably have to be something other than a dot. List.map .baris the same as List.map.bar.

wastaz commented 7 years ago

Yea, I just realised the same thing and updated the proposal :)

theprash commented 7 years ago

Great suggestion. I love the way this works in Elm, but as already mentioned, this is complicated by F# ignoring whitespace between an identifier and the following dot.

Maybe these functions could be defined directly on the type and then always accessed with the type name prefix. This would also help with type inference by disambiguating between different record types with the same label (another problem Elm doesn't have due to structurally typed records). E.g.:

type Foo = { id : int }
type Bar = { id : int }

[{id = 1}] |> List.map Foo.id

The compiler already disallows defining an id member on Foo: The member 'id' can not be defined because the name 'id' clashes with the field 'id' in this type or module, so I think it should be possible to put something here without breaking changes.

Another advantage of this approach is that it doesn't require any syntax changes. It's slightly more verbose than what was originally asked for but seems to fit the general F# style. Maybe there are other pitfalls?

theprash commented 7 years ago

I've just realised that the expression Foo.id alone errors with Field 'id' is not static. So there is already something there that would need to be replaced without breaking it. I suppose some compiler magic would be needed to make it work.

wastaz commented 7 years ago

@theprash That could work, however I do believe that this should be possible to achieve even without having to declare the type (see the generic constraint function in my original post) which I think should be possible to work with in a nice way with the type inference. However, yes. The dot wont work, and it might be that the code is clearer by writing the typename instead of adding another symbol into the mix.

My hope was to be able to stick as close to the Elm implementation/sematics here as possible given that I really like it and it seems to be doable with the generic constraints (at least from my admittedly very limited point of view). But I can certainly see the point of doing it either way.

cloudRoutine commented 7 years ago

duplicate of https://github.com/fsharp/fslang-suggestions/issues/159, but this suggestion already has more detail so maybe we expand & extend this one instead?

I think an operator with non-standard semantics, similar to how the dynamic operator (?) can be used in a manner that lets you write unbound identifiers in the middle of an expression, e.g.

let inline (?) (src : 'a) (prop : string) : 'b =  src.GetType().GetProperty(prop).GetValue(src, null) :?> 'b
let x = "arg"?Length : int
> val x : int = 3

But what we'd want in this case is for the identifier following the accessor operator to be used in a static check against the members of the preceding type, like how it does in a SRTP member call.

I don't think this kind of feature should be limited to properties which are a subset of the more general problem of accessing the members of a type passed to a single argument lambda. As such it should support methods as well.

#bar is no good, it'll clash with preprocessors :bar is no good, it means the type of the preceding expression is bar

Potential Accessor Operators -

 ( @. )     @.Data  
 ( .@ )     .@Data  
 ( @| )     @|Data  
 ( |@ )     |@Data  
 ( =| )     =|Data  
 ( |= )     |=Data  
 ( |- )     |-Data  
 ( -| )     -|Data  
 ( ./ )     ./Data  
 ( /. )     /.Data  
 ( |. )     |.Data  
 ( .| )     .|Data  
 ( !. )     !.Data  
 ( *@ )     *@Data  
 ( @* )     @*Data  
 ( -@ )     -@Data  
 ( @- )     @-Data  
 ( |* )     |*Data  
 ( *| )     *|Data 
dsyme commented 7 years ago

This appears to be a duplicate of #159 or perhaps of #440 .

The original lengthy discussion of #159 on UV is worth taking a look at too.

It's an interesting suggestion to make .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.

dsyme commented 7 years ago

Since there is active discussion here, I'll close #159. I'd be grateful if someone could cherry-pick a summary of the original UV discussion into this thread or the suggestion description.

wastaz commented 7 years ago

Crap, I thought it was strange that no one had suggested this before (and of course someone had). :)

Just adding some opinions here again, of the options that @cloudRoutine had I'm quite fond of @.Data or .@Data, at least compared to the other ones. They are probably the ones who would seem the least weird if I found them in some random code somewhere :)

dsyme commented 7 years ago

@wastaz How about .bar with disambiguation by (.bar) where used in a long identifier? And a new warning on space-separated long identifiers a .b .c?

cheers don

wastaz commented 7 years ago

@dsyme I like warnings for space-separated long identifiers. However, would this only be for space-separated identifiers in that case?

I'm thinking if maybe it's confusing if

foo |> List.map .bar

is different than

List.map  
    .bar
    foo

In this case it's probably silly to structure it like that, but if I understand it correctly then the second example would be interpreted as List.map.bar foo which just feels very weird. Also, splitting lines like that is something that I would like to be able to do especially when I have to do interop with C# fluent-style interfaces.

So basically, I think that in the end that special case would be more confusing than helping?

rojepp commented 7 years ago

@cloudRoutine ./Data looks very nice, kind of like accessing a file system. Would it conflict with anything?

cloudRoutine commented 7 years ago

I agree with @wastaz, it'll lead to an explosion of warnings across code like

let builder = 
    StringBuilder()
        .Append('\t', indent)
        .Append(sprintf "%s(%s) = " block.BlockType block.ParenthesizedName)
        .AppendLine block.Value

which is just one of several fluent examples I could pick out the project I'm working on right now.

@rojepp None of the operators I listed conflict with existing operators

dsyme commented 7 years ago

@cloudRoutine The uses of .Foo in that example are not in long identifiers. But yes, you're right that it would apply to

let builder = 
    System.Console
        .WriteLine("abc")

Certainly List.map (.Bar) is a natural notation for F# with high comprehensibility and orthogonality given the rest of the syntax (c.f. active patterns, first-class uses of operators etc.). The question is how irritating the extra parentheses are, and how much that would reduce reasonable use of the feature.

wastaz commented 7 years ago

@dsyme I agree that List.map (.Bar) feels like a natural notation. However, I do wonder if it is easier to implement in the compiler with a "new" operator that is not used anywhere else instead? Since there are less disambiguation needed then. Though I'm way too much of a noob in the compiler code to be able to tell if this is a concern or not :)

dsyme commented 7 years ago

However, I do wonder if it is easier to implement in the compiler with a "new" operator that is not used anywhere else instead?

(.Bar) would be really very simple to implement

One concern is the clumsiness of x |> (.Bar) though I hope people would never use that and just do x.Bar instead. People might want to do (.Bar) >> (.Baz) >> (.Foo) though that's pretty readable, even if 6 characters longer.

Another concern is whether you could reasonably give autocomplete on xs |> List.map (. when the . is pressed. It's probably doable fairly easily though would need to bee put under test.

wastaz commented 7 years ago

@dsyme

I think that you might not do x |> (.Bar), however I could easily see myself doing something like

(sorry for the contrived example)

foo
|> convertToBar
|> doSomeAwesomeCalculation
|> (.Results)
|> List.map (.NumberOfChickens)
|> List.sum

...actually..typing that didn't really feel too bad, I'm not sure if it really would be that clumsy.

cloudRoutine commented 7 years ago

I hope this won't be limited to get_Prop(), I find myself writing

fun (str:string) -> str.Split ...

and many other similar lambdas to use instance methods far more often than I do to access a property.

theprash commented 7 years ago

@dsyme Do you have any thoughts on including the type name?

theprash commented 7 years ago

The original User Voice thread has a gem of an idea in it that I don't think anyone addressed:

luketopia: What if we allowed the underscore to represent missing arguments to a member (including the instance), in which case a function for applying those arguments would be produced? Then we could do the following:

customers 
|> Seq.map _.Name 
|> File.WriteAllLines(@"C:\CustomerList.txt", _)

This would allow us to partially apply ordinary CLR methods with more than one argument, something I have always wanted.

I've always wanted some short syntax for partial application in an arbitrary order but never realised it could help with record access too:

type Foo = { id: int }

[{id = 1}] |> List.map _.id

[1; 2; 3] |> List.map (String.replicate _ "x")

[1; 2; 3] |> List.map (1 - _)

// Multiple 'slots' allowed
(String.replicate _ _)  3 "x"

I'm not sure about the underscore but the idea is there. It may be difficult to integrate with current syntax so there may need to be a prefix symbol or keyword.

dsyme commented 7 years ago

@theprash See also #186

dsyme commented 7 years ago

@wastaz @theprash It would be good to come up with a list of examples that can be used to assess the syntax against. Can someone create gist containing the examples (and any others you care to add) above for variations _.Foo and (.Foo) and perhaps .Foo disambiguated by (.Foo)?

dsyme commented 7 years ago

@theprash I like the suggestion of _.Foo. Thanks for bringing it to our attention.

vasily-kirichenko commented 7 years ago

_.Foo is what used in Scala for same purposes. I like it more than other alternatives.

vasily-kirichenko commented 7 years ago

Also it's used in Nemerle for both substituting lambda arguments and partial application in exactly the same form as @theprash suggested, see https://github.com/rsdn/nemerle/wiki/Quick-guide#anonymous-functions-and-partial-applications. I like it very much as it makes .NET Frameworks and C#-oriented libraries interoperability much, much nicer.

Such form of partial application makes code more readable in some scenarios. For example, in Scala it look like this:

def foo(i: Int, s: String, d: Double, a: Any) : Unit = {}
val f = foo(1, _, 2.2, _)
val x = f("bar", null)

It'd be fantastic if

let getName = _.Name

can be automatically generalized to (fun (x: ^T) -> (^T : (member Name: 'U) x) as @dsyme suggested here https://github.com/fsharp/fslang-suggestions/issues/506#issuecomment-258126097

Nemerle cannot generalize such a function and infer type from first (local) usage. Scala does not allow such form at all.

vasily-kirichenko commented 7 years ago

A couple of examples

func.TryGetFullDisplayName() 
|> Option.map (fun fullDisplayName -> processIdents func.FullName (fullDisplayName.Split '.'))
|> Option.toList

func.TryGetFullDisplayName() 
|> Option.map (processIdents func.FullName (_.Split '.'))
|> Option.toList

func.TryGetFullDisplayName() 
|> Option.map (processIdents func.FullName (.Split '.'))
|> Option.toList

func.TryGetFullDisplayName() 
|> Option.map (processIdents func.FullName ((.Split) '.')))
|> Option.toList
uses
|> Seq.map (fun symbolUse -> (symbolUse.FileName, symbolUse))
|> Seq.groupBy (fst >> Path.GetFullPathSafe)
|> Seq.collect (fun (_, symbolUses) -> 
      symbolUses 
      |> Seq.map snd 
      |> Seq.distinctBy (fun s -> s.RangeAlternate))
|> Seq.toArray

uses
|> Seq.map (_.FileName, symbolUse)
|> Seq.groupBy (fst >> Path.GetFullPathSafe)
|> Seq.collect (fun (_, symbolUses) -> 
      symbolUses 
      |> Seq.map snd 
      |> Seq.distinctBy (_.RangeAlternate))
|> Seq.toArray

uses
|> Seq.map (.FileName, symbolUse)
|> Seq.groupBy (fst >> Path.GetFullPathSafe)
|> Seq.collect (fun (_, symbolUses) -> 
      symbolUses 
      |> Seq.map snd 
      |> Seq.distinctBy (.RangeAlternate)
|> Seq.toArray
7sharp9 commented 7 years ago

Im not sure _ makes all code easier to understand. As _ is used elsewhere to ignore things.

Im also unsure whether this language idea is for shorthand access to properties or a way to partially apply functions.

I think _.Foo works as great as syntactic shortcut but things like:

[1; 2; 3] |> List.map (String.replicate _ "x")

Seem a little obtuse, intension is too hidden

theprash commented 7 years ago

Using this for creating lambdas is definitely quite a big change to the language and would take some getting used to but it's potentially so powerful. It almost obviates the need for currying!

The compiler could see if a value expression (as opposed to a pattern expression) contains a _. If it does, then it can be treated as a function.

records |> List.map _.recordLabel
[1] |> List.map (_ - 1)
[Some 1] |> List.choose _   // `_` equivalent to `id`

This would be much more useful if you could refer to multiple parameters. But then there's also a difficulty in knowing which _ refers to which parameter and being forced to write your code in a way where the parameters appear from left to right in their function application order. This could be resolved by explicitly numbering any extra parameters, naming them _2, _3, etc.

// Instead of...
let flip f = fun a b -> f b a   // A helper somewhere
(flip String.replicate) "x" 3

// Simply...
(String.replicate _2 _) "x" 3

And then that also allows to referring to the same parameter twice:

// Takes one parameter
let square = _ * _

This could definitely be confusing, especially if there is a _ buried in a large expression:

let func x =
    let a = 1
    let b = 2
    a + _ + b + x
// func actually takes two parameters!

So probably quite dangerous to just throw into the language like this. But what if it required a prefix symbol? Let's try the above examples prefixing with \ as in a normal Haskell lambda:

records |> List.map \_.recordLabel
[1] |> List.map (\_ - 1)
[Some 1] |> List.choose \_

(\ String.replicate _2 _) "x" 3

let square = \ _ * _

let func x =
    let a = 1
    let b = 2
    \ a + _ + b + x

It's more explicit but less pretty, and probably confusing to someone used to \ being equivalent to fun. Maybe this could all be improved with a different choice of symbols. It doesn't have to be backslash and underscore.

And is this compatible with automatically generic record labels? Maybe not?

I've convinced myself that having this new lambda syntax, as opposed to just record label accessors, would take a lot more thought to be viable, if it is at all.

gusty commented 7 years ago

@theprash Regarding the confusion in the order of parameter there's no need to name them with a number, you can give them any name just use the normal lambda with the fun keyword and problem solved.

@7sharp9 for me the _ symbol rather than "ignore this" means "a value goes here" and that applies to the current use as well.

musheddev commented 7 years ago

@vasily-kirichenko In the last two examples with |> Seq.map (.FileName, symbolUse) symbolUse would not be defined and a full lambda will still need to be used. Even (_.FileName, _) would be come a two parameter lambda.

@7sharp9 I am inclined to agree with you. The scope of the original shorthand accessor (.Member) would be largely self evident and convenient but full _ shorthanded lambdas don't seem to add to readability.

wastaz commented 7 years ago

My original idea here was only for shorthand property access. I do think that a lot of the other ideas here on making it a more general way of creating lambdas are interesting - but it feels like that work does complicate things a bit. While using something like _.Foo for mere property access feels simple and non-weird, doing things like (\ String.replicate _2 _) "x" 3 feels like something that might be a good idea but probably requires a bit more thought both to implement and to figure out if it actually makes code more or less readable.

So I would like to propose maybe staggering this into two things to be released (or not) separately. First a shorthand for property access like _.Foo (or another of the options if we deem them to be better) and then later a better way of creating lambdas as discussed in this thread? This would give us a quick and clear win in eliminating the most "stupid" inline lambda cruft without having to finish the entire creating a lambda discussion first :)

7sharp9 commented 7 years ago

@wastaz I agree having multi-use syntactic features can make things really fuzzy, I look as some of the suggestions here and Im not instantly clear on whats happening whereas the use of (_.Foo) does seem clear.

Theres already some gnarly bits of F# syntax around, the last thing we want is to add yet more :-)

vasily-kirichenko commented 7 years ago

@wastaz why do you want it to be restricted by property access specifically? For me property access and method calls look the same: _.DoIt(25, ”bar"), (_.Foo 34) + _.Bar

wastaz commented 7 years ago

@vasily-kirichenko I dont actually want it to be restricted. What I was meaning to convey was that this suggestion seems to have branched into two parts. The first one is what we all seem to somewhat agree to as simple and natural, for example property access _.Foo and I can also see that something like this could be related "1,2,3" |> _.Split(",").

However when we start talking about a generalized concept like this (\ String.replicate _2 _) "x" 3 or similiar constructs I think we are veering off into solving a problem that is a lot harder (both to implement, and also to "get right" design-wise). While I would love to have a similar construct in the language (though maybe not with exactly that syntax) I see no reason to have to necessarily linking those two things together. We could implement the simpler restricted version first and release that and while that is going on continue discussion and evaluation of a more generic kind. Taking the easy win first.

vasily-kirichenko commented 7 years ago

I'm afraid you don't get my idea. I meant the feature should allow method invocation in the same way as it does for property accessors. In short, _ should substitute object self identifier 1. only in dot expressions and 2. only in lambdas.

varon commented 7 years ago

Quick mention to @haf here, as he's offered incredibly useful feedback on similar uservoice suggestions on this.

For 'accessor' functions, lenses offer a vastly more powerful and elegant solution to this problem. You can read more about them, and see Aether (the de-facto standard) here.

For adding the syntax, I have two major objections: a) Adding the feature would encourage adding more members to records, which would promote an overall a less functional style. b) It complicates the language. Scala offers a good argument as to why adding endless syntactic tricks can confuse a language. idiomatic F# remains very easy to read.

As mentioned in the original discussion on lenses on uservoice, this seems to go back to the root of our problem - we don't have higher-kinded types, which would allow us to build this at a library level, rather than needing even more syntax for this.

haf commented 7 years ago

I've read this thread; and instead of the above suggestion, I suggest you this:

module Strings = System.String

(* Becomes equivalent to
module Strings =
  // automatically camelCased, docs are copied;
  let split (input:string) (instance:String) = instance.Split(input)
  let length (input:string) (instance:String) = instance.Length
  let concat // ...
  // ...
*)

"1,2,3" |> Strings.split ","

instead of adding underscore as an object identifier. By letting us assign all object instance methods to a module and make the instance the last parameter, we'd get the ability to compose functions. Also, we'd make people move more towards functional programming, away from keeping functions and state together.

Another upside of this suggestion is that it doesn't require any large changes to the compiler as it's very similar to a type alias. That brings down the risk of introducing new bugs and makes the compiler easier to work with.

smoothdeveloper commented 7 years ago

@haf I like this idea but I believe it really deserves its own suggestion!

wastaz commented 7 years ago

@haf Cool idea!

So if I understand you right then property access would be similar to this?

type Foo = { 
    Bar : int
    Baz : string
}

let a = [ foo1; foo2; foo3 ] |> List.map Foo.Bar |> List.sum
let b = foo1 |> Foo.Bar  
cloudRoutine commented 7 years ago

@haf that seems like it could be better addressed by https://github.com/fsharp/fslang-design/issues/125 than a direct change to the compiler. It's basically the same as the StaticMemberProvider<...> in my comment 😉

toburger commented 7 years ago

@haf brilliant idea!

I like the automatic currying, but I can't imagine how this works if the method has multiple overloads. Any ideas?

dsyme commented 7 years ago

Well, that suggestion a long way from what is proposed here. I agree with @cloudRoutine that the feature feels much more like adhoc macro meta-programming and should be covered by #125 and related type provider suggestions.

The automatic currying is not aligned with existing elements of the F# design, and won't cope with interop features such as params-args, named arguments, optional arguments or method overloading. Also the point of the feature described here is succinctness, where Strings.split requires a type-name qualification at every use point.

haf commented 7 years ago

My beef with the feature as described in this thread is that it introduces syntactic sugar that you need to learn the semantics of. Instead, the assign to module way would let you explore the available functions and documentation whilst reaching the same goal – to make it quicker and more succinct to use instance methods. So I'm personally against this suggestion, because it increases the mental burden of the programmer to understand what _ refers to.

Like Don says, there are many edge cases due to how interop works which is why a type provider would probably not be enough – the compiler would be a good position to hide all that complexity and provide a unified naming standard for interop function suffixes to the functions.

If I could "spend time" on the compiler, I'd explore the stronger type system, compile-to-binary, making opt-in structural typing like F* has and making performance improvements to async, or providing guidance around system programming. Or making equals behave well with arrays and mutable data structures, like Vesa wrote about the other day.

Succinctness btw?

module _ = System.String

There we go! ;)

dsyme commented 7 years ago

I do sympathize with the argument of "yet more special syntax to learn". I could tolerate (.Property) - which feels like it is very easy to learn - and could probably tolerate _.Property. But the _ + 1 suggestion has very many corner-case rules to learn and offers many opportunities to write cryptic code, and that concerns me.

there are many edge cases due to how interop works which is why a type provider would probably not be enough - the compiler would be a good position to hide all that complexity

To be honest, these are also the reasons why we wouldn't put this in the compiler, and instead push it off it to a beefed-up type-provider/meta-programming mechanism, if at all. Auto-interop mechanisms are just too heuristic, too edgy, likely to change over time, and offers a second, largely idiosyncratic way to write F# code.

to make it quicker and more succinct to use instance methods.

I don't particularly see how the suggestion will make using instance methods more succinct (or even quicker). Typing

 expr |> LongTypeName.methodNameWithOverloadQualifier arg1 arg2

is clearly much less succinct and less quick than

expr.MethodName(arg1,arg2)

I'm assuming you are using an editor that offers type-based completion. Likewise

exprs |> List.map VeryLongTypeName.propertyName

is not more succinct or quicker than

exprs |> List.map (.PropertyName)

or

exprs |> List.map _.PropertyName
7sharp9 commented 7 years ago

I think (.Property) is a concise extension to the language that would help to make simple expression clearer, I see it similar in use to function not in behaviour but in generally comprising simple syntax removing unnecessary boilerplate. I don't think I would want tit to become any wider in usage.

The discussion on generating functional accessors for bcl wrapping could be done with type providers now if anyones comfortable with a bit of stringly reflection. Admittedly a bit of a powerup to type providers would make it a lot easier, even just finishing off unchecked quotations in providedtypes.fs would help there too.

varon commented 7 years ago

@7sharp9 The problem with (.Property) is that it encourages using the language in the "wrong" way. You're calling methods on objects instead of applying functions to arguments. If you wanted to program in an OO style, then why not just use C#?

7sharp9 commented 7 years ago

@varon using (.Prop) is shorthand for (fun p -> p.Prop) your just eliminating the boiler plate on such a function.

varon commented 7 years ago

I understand that, @7sharp9. I think you've either completely missed what I'm getting at here.

In the case of (.Prop) or (fun p -> p.Prop), it's obvious by inspection that they're semantically identical. We should try to get completely away from having to 'dot into' objects at any point. Adding syntactic sugar for this is a kludge fix for a problem we don't encounter at all if we access members by function instead of by methods. You would simply call the function prop on p to get the value of Prop from it.

vasily-kirichenko commented 7 years ago

@varon the proposed syntax simplifies interop with .net framework and with almost all the libraries available. What's more, F# records encourage using dot notation and it's not oop in any way.

smoothdeveloper commented 7 years ago

I can see the argument for trying to veer away from OO, although F# is designed to mix rather well with interop with OO.

I also like the idea of having free functions, although the tooling is not yet there (opposed to the dot-completion that really sunk deep in the developers workflow and is supported in all languages), that being said, I see that resharper is bringing post-fix completion to C++ (https://blog.jetbrains.com/dotnet/2016/11/01/resharper-ultimate-2016-3-eap-6/):

https://d3nmt5vlzunoa1.cloudfront.net/dotnet/files/2016/10/cpp_postfix_completion.gif

And I believe completion tooling should evolve to next step, with feature such as finding functions that can be called with nearby symbols and proposing functions / symbols that are nested.

In day to day F# code I write, it is true that having short hand access to members instead of full fledged lambda would reduce some amount of clutter, I hope that suggestion is going to get good proposal / RFC draft with community input.