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

exhaustive record pattern #247

Open
lue-bird opened this issue Jun 18, 2024 · 1 comment
Open

exhaustive record pattern #247

lue-bird opened this issue Jun 18, 2024 · 1 comment
Labels
accepted The issue has been reviewed and accepted for implementation language proposal proposal of a major feature to the language

Comments

@lue-bird
Copy link
Contributor

lue-bird commented Jun 18, 2024

Proposing syntax to make sure you've looked at all the fields of a record value. Adding or removing a field should in that case warn you that you need to explicitly ignore/use it.

Some common use cases

  • matching on info (from type aliases) in view/render functions, deciding what should be shown and what not (e.g. viewing a list of tasks which might get creation date, tags, completion etc in the future)
  • a declaration taking a record of arguments which should all be acknowledged (e.g. Parser.number checking all arguments)
  • encoding where not encoding a field should be an explicit choice (e.g. encoding an audio graph for a js library)
  • a let destructuring all of the values returned from a case-of or if

pattern syntax

  • { _ | command = commandValue } for inexhaustive match with explicit variable name (equivalent to current { command = commandValue })
  • { _ | command } for inexhaustive match with field value variable name inherited from field name (equivalent to current { command })
  • { model = modelValue, command = commandValue } for exhaustive match using all field values
  • { model = modelValue, command = _ } for exhaustive match ignoring a field value
  • { model = modelValue, command } for exhaustive match using all field values, with a field value variable name inherited from field name

This is consistent with {} being a match on a record with exactly 0 fields, instead of a record with whatever fields.

example code

update msg model =
    case msg of
        UserSelectedTrackColor { trackIndex = trackIndexToRecolor hue = selectedNewTrackHue } ->
            { model
               | tracks =
                    model.tracks
                        |> Array.update trackIndexToRecolor
                            (\track -> { track | hue = selectedNewTrackHue })
            }

type Msg =
    | UserSelectedTrackColor { trackIndex : Int, hue : Float }

if we then e.g. add

type Msg =
    | UserSelectedTrackColor { trackIndex : Int, hue : Float, lightness : Float }

the compiler will give an error about lightness not being handled in the update case branch.

Had this been an inexhaustive record match as in current gren, the user choice of lightness would simply be ignored, which is a pretty subtle bug!

similar mindset in existing features

  • using \{} -> instead of \_ -> for laziness to avoid accidentally ignoring future values. E.g. changing from Test.test to Test.fuzz should trigger a compiler error that you didn't use the fuzzed value
  • using the value { contextA = old.contextA, contextB = [] } instead of { old | contextB = [] } when you want to assure yourself you've considered for each field whether it needs changing or not
  • not using a catch-all case when adding variants could affect future logic

conflicts

The proposed exhaustive record pattern does not allow matching on a value of an extensible record type.

This prevents a coding style where functions like view take info as an extensible record (also slightly related: #240).

alternatives considered

  • (only partial solution) catch unused extensible record argument fields in the compiler (or a widely used static analysis tool). This might prevent type magicians from using some extra fields for type-level permissions, context etc

current workaround

Explicitly add a let declaration for the record with an explicit record fields type annotation which only has the fields you've considered.

discussion

This issue is mostly a summary of my points brought up in this discord thread

@robinheghan robinheghan added the language proposal proposal of a major feature to the language label Jun 18, 2024
@peteygao
Copy link

peteygao commented Oct 9, 2024

This is a brilliant addition in my opinion and catches a lot of oversight-induced logic errors. There's been too many times to count where I added a new field to an Elm Record expecting behavioural change, only for it to not show up and scratching my head. Then waste time debugging going over conditional logic (most likely source of bugs) but finding nothing. Then after some more handwringing, I discover that I forgot to include a field in a record update somewhere.

Yeah, there's some trauma there 😹

@robinheghan robinheghan added the accepted The issue has been reviewed and accepted for implementation label Oct 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
accepted The issue has been reviewed and accepted for implementation language proposal proposal of a major feature to the language
Projects
None yet
Development

No branches or pull requests

3 participants