Skip to content

Nested field update syntax for records - field disambiguator #1456

@Happypig375

Description

@Happypig375

I propose we allow putting a dot before the record field in the with expression to explicitly denote it as a field instead of a type:

type SdlWindowConfig =
    { WindowTitle : string
      WindowX : int
      WindowY : int }
    static member defaultConfig =
        { WindowTitle = "Default Title"
          WindowX = 0
          WindowY = 0 }

type SdlConfig =
    { WindowConfig : SdlWindowConfig }
    static member defaultConfig =
        { WindowConfig = SdlWindowConfig.defaultConfig }

type WorldConfig =
    { Imperative : bool
      Accompanied : bool
      Advancing : bool
      FramePacing : bool
      ModeOpt : string option
      SdlConfig : SdlConfig }
    static member defaultConfig =
        { Imperative = true
          Accompanied = false
          Advancing = true
          FramePacing = false
          ModeOpt = None
          SdlConfig = SdlConfig.defaultConfig }
          
let worldConfig =
    { WorldConfig.defaultConfig with
        .SdlConfig.WindowConfig.WindowTitle = "My Title" } // <-

Without the dot, backwards compatibility dictates that SdlConfig be resolved as a type.

let worldConfig =
    { WorldConfig.defaultConfig with
        SdlConfig.WindowConfig.WindowTitle = "My Title" }
error FS0001: This expression was expected to have type
    'WorldConfig'    
but here has type
    'SdlConfig'

The existing ways of approaching this problem in F# include

  1. not using nested record field update (boilerplate)
let sdlWindowConfig = { SdlWindowConfig.defaultConfig with WindowTitle = "My Title" }
let sdlConfig = { SdlConfig.defaultConfig with WindowConfig = sdlWindowConfig }
let worldConfig = { WorldConfig.defaultConfig with SdlConfig = sdlConfig }
// or the pyramid...
let worldConfig =
    { WorldConfig.defaultConfig with
        SdlConfig =
            { SdlConfig.defaultConfig with
                WindowConfig =
                    { SdlWindowConfig.defaultConfig with
                        WindowTitle = "My Title" } } }
  1. prepending with type (unintuitive)
let worldConfig =
    { WorldConfig.defaultConfig with
        WorldConfig.SdlConfig.WindowConfig.WindowTitle = "My Title" } // need to repeat "WorldConfig" for every update in SdlConfig!

Pros and Cons

The advantages of making this adjustment to F# are

  1. Less noise (we already use type inference to hide types, why require them at nested record field updates?)
  2. Less repetition
  3. Less surprises (with a style that uses the dot everywhere)
  • this is similar to:
open System
type NotDU = Int32 // is a type abbreviation
type DU = | Int32 // unambiguously a DU so "it's a good practice to prepend |"

The disadvantage of making this adjustment to F# is another way to do the same thing.

Extra information

Estimated cost (XS, S, M, L, XL, XXL): S

Related suggestions:

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions