Pattern match on record field values

Hey there,

I’m not sure if this has been asked before (I’ve searched around and haven’t seen it), but has there been any conversation about the idea of pattern matching on record field values? For example:

case myRecord of
    { a = 1 } -> ...
    _ -> ...

or even with nested pattern matching:

case myRecord of
    { a = Just value, b = "" } -> ...
    _ -> ...

Currently, you could achieve the same functionality by:

case ( myRecord.a, myRecord.b ) of
    ( Just value, "" ) -> ...
    _ -> ...

This is something that some other languages, such as Ocaml, have. I know “other languages do it” is not necessary a good argument in and of itself. However, I’ve found it useful in other contexts and could see it being helpful in Elm too!

Have other thought about this and/or know where I could find a reference to that discussion?

4 Likes

I’ve definitely also wanted this from time to time. Usually it’s a sub-pattern, such as:

case mState of
   Just { isDisabled = True } ->
         ....

It’s similar to quite a few language additions that I can surely live without, but don’t see the harm in adding. However, that is ‘quite a few’ so it may be that each one individually doesn’t present any harm, but adding all would somehow over-complicate the language. Therefore it’s not necessarily a question of “should X be added to the language” but rather “which of the X, Y, Z,… should be added to the language” which is a much harder question because you have to figure out what 'Y, Z . …" are.

1 Like

I have found case to have an odd set of capabilities to be honest. It can do some very sophisticated things but then it doesn’t have other things which I would consider more “obvious”. For instance list destructuring strikes me as very sophisticated and also very non-obvious:

case myList of
    [] ->  -- Matches an empty list
    head :: remaining ->  -- Matches a list of length 1 or greater
                          -- and gives access to the head and remaining items

On the other hand, I can’t match multiple variants of a type on a single branch. An example from some code I was just writing which is displaying time-averaged pass-fail metrics:

type AcquisitionStatus value =
    Pending
    | Never
    | Partial value
    | Finished value Bool

view : AcquisitionStatus -> Element msg
view acqStatus =
    el
        [ Background.color (case acqStatus of
            -- I'd like something like this
            -- to be able to match two (or more) types so I don't repeat myself
            --
            -- The logic here is that we don't know if an unacquired
            -- value is "passing" or "failing" but also more than three colors
            -- is probably more confusing than helpful. So render Pending and
            -- Partial the same.
            Pending | Partial _ -> rgb255 0xD0 0xD0 0xD0  -- Gray

            Never -> rgb255 0xFF 0x90 0x80  -- Red

            Finished _ passed -> if passed then
                    rgb255 0xC0 0xFF 0x90  -- Green
                else
                    rgb255 0xFF 0x90 0x80  -- Red
        )
        ]
        ...

EDIT: clarified a code comment

I agree that collapsing multiple branches in pattern matching would be really convenient in some cases!

I wouldn’t call list pattern matching very sophisticated though. As previously said, “it depends on you background”.

I’ve wanted ‘or-patterns’ for a long time, it’s one of the “quite a few” that I mentioned above.

The challenge on matching multiple cases at once is what named values they bring into scope. It would take a rule that said something like, for a collection of cases to be treated together, those cases need to introduce the same named values and those named values all must have the same types.

That’s exactly how F# works. (Its error messages aren’t super good, it’s always just “The two sides of this ‘or’ pattern bind different sets of variables” with no help on if it’s just different names, different amounts, different types or all of those, but I’m sure Elm could do better.)

For reference, here’s an example of what Rust’s errors for these situations look like. I think people coming from Rust will likely have the same intuition as kmurph1271, since it doesn’t support list destructuring but does support matching multiple patterns with |, I did at least.

enum MyEnum {
    A { field: String },
    B { field: u32 },
    C { field: String, another: String },
}

fn main() {
    let a = MyEnum::B { field: 0 };
    match a {
        MyEnum::A { field } | MyEnum::B { field } => {}
        MyEnum::A { field } | MyEnum::C { field, another } => {}
    }
}

error[E0408]: variable `another` is not bound in all patterns
  --> src/main.rs:11:9
   |
11 |         MyEnum::A { field } | MyEnum::C { field, another } => {}
   |         ^^^^^^^^^^^^^^^^^^^                      ------- variable not in all patterns
   |         |
   |         pattern doesn't bind `another`

error[E0308]: mismatched types
  --> src/main.rs:10:43
   |
9  |     match a {
   |           - this expression has type `MyEnum`
10 |         MyEnum::A { field } | MyEnum::B { field } => {}
   |                     -----                 ^^^^^ expected struct `String`, found `u32`
   |                     |
   |                     first introduced with type `String` here
   |
   = note: in the same arm, a binding must have the same type in all alternatives
3 Likes

This works because lists in Elm are basically the custom type

type List a = Nil | Cons a (List a)

(these names for the constructors are conventional), only that there is syntactic sugar, namely [] for Nil, head :: rest for Cons head rest and list syntax [a, b, ...] for a :: b :: .... So the list destructuring is basically just

case myList of
    Nil -> -- empty list
    Cons head rest -> -- matching a list of length at least 1

which would be legal for the custom type above and which would give the same result.

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.