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

Allow _.Property shorthand for accessor functions #506

Closed
5 of 6 tasks
wastaz opened this issue Nov 3, 2016 · 331 comments
Closed
5 of 6 tasks

Allow _.Property shorthand for accessor functions #506

wastaz opened this issue Nov 3, 2016 · 331 comments

Comments

@wastaz
Copy link

wastaz commented Nov 3, 2016

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

  • less unnecessary visual noise for a pretty common use case
  • improves on the experience when piping values
  • should not interfere with any existing code (at least I think)

The disadvantages of making this adjustment to F# are

  • introduces a special case in parsing code which might be hard to fit into the compiler in a nice way (Not sure about this, someone who knows more about this
  • may be seen as "magic" and be harder to understand?
  • two ways of doing the same thing

Affadavit (must be submitted)

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 would be willing to help implement and/or test this
  • I or my company would be willing to help crowdfund F# Software Foundation members to work on this
@rojepp
Copy link

rojepp commented Nov 3, 2016

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

wastaz commented Nov 3, 2016

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

@theprash
Copy link

theprash commented Nov 3, 2016

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

theprash commented Nov 3, 2016

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

wastaz commented Nov 3, 2016

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

cloudRoutine commented Nov 3, 2016

duplicate of #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
Copy link
Collaborator

dsyme commented Nov 3, 2016

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

dsyme commented Nov 3, 2016

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

wastaz commented Nov 3, 2016

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

dsyme commented Nov 3, 2016

@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
Copy link
Author

wastaz commented Nov 3, 2016

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

rojepp commented Nov 3, 2016

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

@cloudRoutine
Copy link

cloudRoutine commented Nov 3, 2016

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

dsyme commented Nov 3, 2016

@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
Copy link
Author

wastaz commented Nov 3, 2016

@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
Copy link
Collaborator

dsyme commented Nov 3, 2016

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

wastaz commented Nov 3, 2016

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

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

theprash commented Nov 3, 2016

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

@theprash
Copy link

theprash commented Nov 3, 2016

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

dsyme commented Nov 3, 2016

@theprash See also #186

@dsyme
Copy link
Collaborator

dsyme commented Nov 3, 2016

@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
Copy link
Collaborator

dsyme commented Nov 3, 2016

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

@vasily-kirichenko
Copy link

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

@vasily-kirichenko
Copy link

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 #506 (comment)

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

@vasily-kirichenko
Copy link

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

7sharp9 commented Nov 5, 2016

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

theprash commented Nov 5, 2016

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.

@adelarsq
Copy link

adelarsq commented Jun 4, 2021

@dsyme Would be possible to use two dots for the syntax (..)?

foo |> ..bar |> (+) 5

@Happypig375
Copy link
Contributor

That's too similar to the range operator, isn't it?

@adelarsq
Copy link

adelarsq commented Jun 4, 2021

That's too similar to the range operator, isn't it?

That's right. This will cause some confusion.

My others sugestions:

foo |> $bar |> (+) 5     # "$tatic"
foo |> ::bar |> (+) 5    # :: from the Rust use declarations https://doc.rust-lang.org/reference/items/use-declarations.html

The motivation to use like this is that remembers a static access.

@lucasteles
Copy link

I suggest we close this discussion and start an RFC with exactly this proposal.

agree, this is already useful enough

@wallymathieu
Copy link

Purescript uses the syntax:
https://github.com/purescript/documentation/blob/master/language/Syntax.md#property-accessors

@7sharp9
Copy link
Member

7sharp9 commented Aug 2, 2022 via email

@vzarytovskii
Copy link

Merged

@dlidstrom
Copy link

When will this feature be available to use?

@Happypig375
Copy link
Contributor

@dlidstrom when .NET 8 is released November.

@jwosty
Copy link
Contributor

jwosty commented Aug 26, 2023

I know this is the wrong place to ask this, but is there an easier way to try out preview features in my projects on non-Windows (so no VS preview) platforms, without having to build F# from source on your own machine?

@baronfel
Copy link
Contributor

In FSAC we have a 'nightly' branch where we try to incorporate the prerelease versions of the compiler library. We're currently working on the 8.0.100 previews (and in fact have identified a regression!) but we don't have an automated release system like is present for VS.

The current steps would be something like:

  • clone FSAC's nightly branch
  • build it
  • in Ionide, use the FSharp.fsac.netcoreDllPath setting to point to your nightly FSAC build

@jwosty
Copy link
Contributor

jwosty commented Aug 26, 2023

@baronfel Interesting, got a link to the regression?

@Lanayx
Copy link

Lanayx commented Oct 28, 2023

Was there an RFC for this suggestion?

@DedSec256
Copy link

DedSec256 commented Oct 29, 2023

@Lanayx, I found only
fsharp/fslang-design#710

@Lanayx
Copy link

Lanayx commented Oct 30, 2023

I think that one should better be finished and merged, otherwise the feature will be undocumented

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