Open reinux opened 1 year ago
I would say that if we did this, I wouldn't want a keyword. I'd much prefer it to be a scoping rule that "works" in top-down order:
let doot = {
FirstName = "phillip"
LastName = "carter"
FullName = $"{FirstName} {LastName}"
}
isn't it going to introduce breaking change?
type Rec = { FirstName: string; LastName: string; FullName: string }
let FirstName = "gauthier"
let LastName = "segay"
let doot = {
FirstName = "phillip"
LastName = "carter"
FullName = $"{FirstName} {LastName}"
}
> val FirstName: string = "gauthier"
> val LastName: string = "segay"
> val doot: Rec = { FirstName = "phillip"
> LastName = "carter"
> FullName = "gauthier segay" }
I'm more keen if we introduce #653 / #24, I think it boils to almost the same as the suggestion:
let FirstName = "phil"
let LastName = "carter"
let FullName = $"{FirstName} {LastName}"
let phil = { FirstName; LastName; FullName }
Yes, it'd technically be a breaking change, but in ordering it'd be consistent with shadowing (most "recent" scope wins), so it's something I'd actually be in favor of introducing
@cartermp, I always thought not introducing breaking changes was a super high priority. Without some mechanism, e.g. a keyword, it would introduce massive breaking changes in my source.
How about just borrowing an idea I believe was recently used in another suggestion?
{
firstName = firstName
lastName = lastName
fullName = _.firstName + _.lastName
}
I know that some have argued the underscore should mean only "not used" or something like that, but the way I see it it's more like "no need to spell it out".
@BentTranberg I also considered _.
could be a good fit here, but I'm also wondering if this wouldn't be ambiguous in some contexts. I'd get a better sense of it when I'll start using that feature.
I also wonder about the ordering semantics, if the breaking change would be given a pass:
let FirstName = "gauthier"
let LastName = "segay"
let doot = {
FullName = $"{FirstName} {LastName}"
FirstName = "phillip"
LastName = "carter" }
@BentTranberg The underscore syntax has already been implemented for F#8 as meaning fun x -> x.firstName
, so unfortunately this would be ambiguous.
How about this?
type Person =
{
FirstName: string
LastName: string
FullName: string
}
let person1: Person =
{ as x
FirstName = "James"
LastName = "Bond"
FullName = $"{x.FirstName} {x.LastName}" // James Bond
}
let person2: Person =
{ person1 as x with
FirstName = "Lena"
FullName = $"{x.FirstName} {x.LastName}" // Lena Bond
}
edit: The last one - person2
- illustrates how as x
is used together with the existing syntax person1 with
.
@BentTranberg you are onto something, onto being one more fsharp language designer assistant :)
maybe this would be more natural:
let rec person1 =
{ FirstName = "James"
LastName = "Bond"
FullName = "{x.LastName}, {x.FirstName} {x.LastName}" } as x
as of now:
let rec person1 = {
FirstName = "James"
LastName = "Bond"
FullName = $"{person1.LastName}, {person1.FirstName} {person1.LastName}"
}
it currently gives:
let rec person1 = { FirstName = "James"; LastName = "Bond"; FullName = $"{person1.LastName}, {person1.FirstName} {person1.LastName}" };;
--------------------------------------------------------------------------^^^^^^^
warning FS0040: This and other recursive references to the object(s) being defined will be checked for initialization-soundness at runtime through the use of a delayed reference. This is because you are defining one or more recursive objects, rather than recursive functions. This warning may be suppressed by using '#nowarn "40"' or '--nowarn:40'.
let rec person1 = { FirstName = "James"; LastName = "Bond"; FullName = $"{person1.LastName}, {person1.FirstName} {person1.LastName}" };;
--------^^^^^^^
error FS0031: The value 'person1' will be evaluated as part of its own definition
With the last syntax I suggested, it should not be allowed to reference a field before it is assigned explicitly, if it is assigned explicitly. Without having given it much thought, I think that simple rule will be enough. I also think it feels quite F#'ish that fields are assigned before they can be used.
I think the learning overhead is a bit higher with that syntax, because as
is normally used after a pattern as opposed to within brackets. Maybe let (person1 as x): Person = ...
might be easier to make sense of?
Being able to copy-and-update from another record using a variable bound using as
is interesting, but I couldn't quite figure out what that means (or why you would do that) until I read the comment. Maybe that's just me, though, and I'm admittedly a little sleep deprived.
That said, I also take @cartermp 's point that not having too much additional syntax for this would be nice, which is kind of why I think rec
is a good compromise, even if it doesn't really add as much value as as
would.
@reinux you are right about as
location, just in case you miss the feature, which is supported:
type Foo() as this =
// ...
and #501 also uses the same for binding an interface.
I overall feel the suggestion isn't bringing enough, with record punning and using locals with the right name, it would become terse enough to not really bother for this edge case.
The feature value increases when the members are more complex expressions, but initializing the locals is very explicit and isn't much more code, no feature needed beyond punning, it doesn't even have to pollute the scope due to the flexible scoping for bindings.
let person =
let FirstName, LastName = "James", "Bond"
let FullName = $"{LastName}, {FirstName} {LastName}"
{ FirstName; LastName; FullName }
note: the code gen for the bindings of tuple above isn't great, it somehow initializes an array, but is a separate issue.
I'm not that happy either with the syntax I suggested. Especially the second one - person2
- I believe is bound to be misinterpreted by the reader all the time as meaning x
is an alias for the original person1
. It's easy enough to deduce the reasonable explanation if you stop and think for a while, but you shouldn't be required to stop and think for a while ... "oh, it must mean this, and not that, because why else is it there". No, no, no. It should be plainly understood immediately when you look at it, and I feel that doesn't happen.
{ firstName = "John"; lastName = "Doe"; fullName = "MickeyMouse" }
.
I propose we allow fields assigned to a record to be referenced down the line, perhaps indicated by a
rec
keyword a lalet rec
:The existing way of approaching this problem in F# is to bind the value upfront, and then reference that, e.g.
Pros and Cons
The advantages of making this adjustment to F# are quality of life, helps with legibility when working with large amounts of declarative code/configuration/etc.
The disadvantages of making this adjustment to F# are one more thing to learn, though its meaning can be intuited quite easily from
let rec
or with a simple code example as in here.Extra information
Estimated cost (XS, S, M, L, XL, XXL): S (or M if parsing records piecemeal is currently non-trivial)
Related suggestions: (put links to related suggestions here)
This syntax is available in the Nix language, a DSL for a functional Linux package manager and accompanying distro.
Affidavit (please submit!)
Please tick these items by placing a cross in the box:
Please tick all that apply:
For Readers
If you would like to see this issue implemented, please click the :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.