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 recursive record assignment #1315

Open
6 tasks done
reinux opened this issue Sep 12, 2023 · 13 comments
Open
6 tasks done

Allow recursive record assignment #1315

reinux opened this issue Sep 12, 2023 · 13 comments

Comments

@reinux
Copy link

reinux commented Sep 12, 2023

I propose we allow fields assigned to a record to be referenced down the line, perhaps indicated by a rec keyword a la let rec:

rec {
  firstName = ...
  lastName = ...
  fullName = lastName + ", " + firstName
}

The existing way of approaching this problem in F# is to bind the value upfront, and then reference that, e.g.

let firstName, lastName = ..., ...
{ firstName = firstName
  lastName = lastName
  fullName = lastName + ", " + firstName
}

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:

  • This is not a question (e.g. like one you might ask on StackOverflow) and I have searched StackOverflow for discussions of this issue
  • This is a language change and not purely a tooling change (e.g. compiler bug, editor support, warning/error messages, new warning, non-breaking optimisation) belonging to the compiler and tooling repository
  • 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
  • I have searched both open and closed suggestions on this site and believe this is not a duplicate

Please tick all that apply:

  • This is not a breaking change to the F# language design
  • I or my company would be willing to help implement and/or test this

For Readers

If you would like to see this issue implemented, please click the 👍 emoji on this issue. These counts are used to generally order the suggestions by engagement.

@cartermp
Copy link
Member

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}"
}

@smoothdeveloper
Copy link
Contributor

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 }

@cartermp
Copy link
Member

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

@BentTranberg
Copy link

BentTranberg commented Sep 13, 2023

@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".

@smoothdeveloper
Copy link
Contributor

smoothdeveloper commented Sep 13, 2023

@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" }

@Tarmil
Copy link

Tarmil commented Sep 13, 2023

@BentTranberg The underscore syntax has already been implemented for F#8 as meaning fun x -> x.firstName, so unfortunately this would be ambiguous.

@BentTranberg
Copy link

BentTranberg commented Sep 13, 2023

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.

@smoothdeveloper
Copy link
Contributor

@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

@BentTranberg
Copy link

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.

@reinux
Copy link
Author

reinux commented Sep 13, 2023

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.

@smoothdeveloper
Copy link
Contributor

smoothdeveloper commented Sep 14, 2023

@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.

@BentTranberg
Copy link

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.

@charlesroddie
Copy link

  • Binding is fine and more explicit, so there is no good use case for this.
  • The example given is an example of badly designed type which inefficiently stores an additional field, whose inclusion creates possibilities of invalid data like { firstName = "John"; lastName = "Doe"; fullName = "MickeyMouse" }.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants