Closed wastaz closed 1 year ago
Strong preference for _.Member
Where Member is either a property or a record’s member!!
❤️
I'm using underscores in lambdas (C#) all the time (persons.Where( => .Name.Length == 2)). Missed it in F#.
What about e.g fun x -> x > 10
. Could that be simplified to just _ > 10
?
❤️
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.
if you think of _
as "a value goes here", then I think that there is some symmetry and it seems quite natural.
Maybe instead of limiting it to _
, you could have _
or _foo
?
Please not the brackets. Closing bracket matching is ugly enough as it is without another set of forced brackets.
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 .
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 (_, _)
.
❤️
I liked _.Prop
more
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).
_.Property1.Property2
_,Method
_,Method(1)
_,Method(1,2)
_.Method().Property
_.Method(1).Property
_ + 1
_ > 10
_.[1]
_.[1..10]
_.Method(_2,_3,_4)
Some + Very(Complex(_1,_2, (fun _ -> _ + 1)))
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.
❤ .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.
_.Name
@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.
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.
That said _.X
would be awesome
@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.
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).
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)
.
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...
?
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.
@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?
@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.
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
inline
codeRecall 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.
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.
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)
+1 to @vasily-kirichenko 's proposal to treat it as shorthand/alias to (fun _ ->)
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
@odytrice write some real code in Scala to feel what such syntax is like before judging.
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
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.
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)
@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]
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.
@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...
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?
@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.
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.
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.
@cartermp I like it
as well. Again, I'm for full fledged alternative lambda syntax, that's all.
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
@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).
@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
}
}
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.
I mean a curly braced block is a lambda:
@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#?
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.
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.
This is a feature that I would love to steal from Elm :) In Elm, if I define a record as such
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
I can write code like this
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
Are method calls allowed
Is this 1-place placeholder syntax?
Is this multi-place placeholder syntax?
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: