Skip to content

Instantly share code, notes, and snippets.

@yang-wei
Last active December 2, 2024 06:40
Show Gist options
  • Save yang-wei/4f563fbf81ff843e8b1e to your computer and use it in GitHub Desktop.
Save yang-wei/4f563fbf81ff843e8b1e to your computer and use it in GitHub Desktop.
Elm Destructuring (or Pattern Matching) cheatsheet

Should be work with 0.18

Destructuring(or pattern matching) is a way used to extract data from a data structure(tuple, list, record) that mirros the construction. Compare to other languages, Elm support much less destructuring but let's see what it got !

Tuple

myTuple = ("A", "B", "C")
myNestedTuple = ("A", "B", "C", ("X", "Y", "Z"))

let
  (a,b,c) = myTuple
in 
  a ++ b ++ c
-- "ABC" : String

let
  (a,b,c,(x,y,z)) = myNestedTuple
in
  a ++ b ++ c ++ x ++ y ++ z
-- "ABCXYZ" : String

Make sure to match every tuple(no more no less) or you will get an error like:

let
  (a,b) = myTuple
in
  a ++ b
-- TYPE MISMATCH :(

In Elm community, the underscore _ is commonly used to bind to unused element.

let
  (a,b,_) = myTuple
in 
  a ++ b
-- "AB" : String

It's also more elegant to decrale some constant of your app using destructuring.

-- with no destructuring
width = 200
height = 100

-- with destrcuturing
(width, height) = (200, 100)

Thanks to @robertjlooby, I learned that we can match exact value of comparable. This is useful when you want to explicitly renaming the variable in your branches of case .. of.

isOrdered : (String, String, String) -> String
isOrdered tuple =
 case tuple of
  ("A","B","C") as orderedTuple ->
    toString orderedTuple ++ " is an ordered tuple."
    
  (_,_,_) as unorderedTuple ->
    toString unorderedTuple ++ " is an unordered tuple."


isOrdered myTuple
-- "(\"A\",\"B\",\"C\") is an ordered tuple."

isOrdered ("B", "C", "A")
-- "(\"B\",\"C\",\"A\") is an unordered tuple."

Exact values of comparables can be used to match when destructuring (also works with String, Char, etc. and any Tuple/List/union type built up of them) - @robertjlooby

List

Compare to tuple, List almost do not support destructuring. One of the case is used to find the first element of a list by utilizing the cons operator, ie ::w

myList = ["a", "b", "c"]

first list =
  case list of
    f::_ -> Just f
    [] -> Nothing

first myList
-- Just "a"

This is much more cleaner than using List.head but at the same time increase codebase complexity. By stacking up the :: operator, we can also use it to match second or other value.

listDescription : List String -> String
listDescription list =
 case list of
    [] -> "Nothing here !"
    [_] -> "This list has one element"
    [a,b] -> "Wow we have 2 elements: " ++ a ++ " and " ++ b
    a::b::_ -> "A huge list !, The first 2 are: " ++ a ++ " and " ++ b

Record

myRecord = { x = 3, y = 4 }

sum record =
  let
    {x,y} = record
  in
    x + y

sum myRecord
-- 7

Or more cleaner:

sum {x,y} =
  x + y

Notice that the variable declared on the left side must match the key of record:

sum {a,b} =
  a + b

sum myRecord
-- The argument to function `sum` is causing a mismatch.

As long as our variable match one of the key of record, we can ignore other.

onlyX {x} =
  x

onlyX myRecord
-- 3 : number

I don't think Elm support destructuring in nested record (I tried) because Elm encourages sparse record

When destructuring a record, you do not have to declare all fields within the braces. Also, you can alias the whole record while destructuring it as a parameter. This is useful if you need shorthand access to some fields but also to the record as a whole.

  • nmk
myRecord = { x = 1, y = 2, z = 3}

computeSomething ({x, y} as wholeRecord) =
    -- x and y refer to the x and y fields of the passed in record
    -- wholeRecord is the complete record
    -- i.e. x and wholeRecord.x refer to the same field
    -- but z is only accessible as wholeRecord.z

Union Type

Again, thanks to @robertjlooby, we can even destruct the arguments of union type.

type MyThing
  = AString String
  | AnInt Int
  | ATuple (String, Int)

unionFn : MyThing -> String
unionFn thing =
  case thing of
    AString s -> "It was a string: " ++ s
    AnInt i -> "It was an int: " ++ toString i
    ATuple (s, i) -> "It was a string and an int: " ++ s ++ " and " ++ toString i
@mcampbell
Copy link

mcampbell commented Aug 4, 2016

@robertjlooby - thanks for the additional examples. Your last one (the binding to a var with as) is oh so slightly wrong though; any point (0, ) is on the y axis, not x (other than (0,0), of course). (, 0) are all along the x axis.

@kkruups
Copy link

kkruups commented Dec 9, 2016

Yangwei/RobertjLooby,

Slight correction (x axis should be y-axis for top pattern match, since points matched
are of the form (0,1), (0,2), (0,3) (0,4), assuming your x-axis is horizontal and y-axis is vertical):

f : (Int, Int) -> String
f point =
case point of
(0, _) as thePoint -> toString thePoint ++ " is on the x axis"
_ as thePoint-> toString thePoint ++ " is not on the x axis"

should be:

f : (Int, Int) -> String
f point =
case point of
(0, _) as thePoint -> toString thePoint ++ " is on the y axis"
_ as thePoint-> toString thePoint ++ " is not on the x axis"

@nmk
Copy link

nmk commented Jan 18, 2017

When destructuring a record, you do not have to declare all fields within the braces. Also, you can alias the whole record while destructuring it as a parameter. This is useful if you need shorthand access to some fields but also to the record as a whole.

myRecord = { x = 1, y = 2, z = 3}

computeSomething ({x, y} as wholeRecord) =
    -- x and y refer to the x and y fields of the passed in record
    -- wholeRecord is the complete record
    -- i.e. x and wholeRecord.x refer to the same field
    -- but z is only accessible as wholeRecord.z

@mbylstra
Copy link

mbylstra commented Mar 4, 2017

The "Nested Record" example seems mislabelled. It looks to be an example of pattern matching on nested union types. Also, giving the type definition of ParseTree would help enormously for readability of that example. I don't believe Elm supports destructuring of nested records.

Eg: This is not possible, but could reasonably be considered consistent with the syntax for destructuring the top level of a record:

someFunction ({ name, age, ({ city, country } as location) } as person) =
    ...

But this is possible:

someFunction ({ name, age, location} as person) =
    ...

To be extra clear, you can destructure the top level of a record that happens to be nested, you just can't destructure multiple levels in the one expression.

I'd love to be proven wrong if there's some syntax I'm missing.

@charles-cooper
Copy link

Is there any way to pattern match on a constructor ignoring all fields? Something like,

type FooBar = Foo Int | Bar String Int
constructorName : FooBar -> String
constructorName x = case x of
  Foo {} -> "Foo"
  Bar {} -> "Bar"

@ohanhi
Copy link

ohanhi commented Apr 19, 2017

@charles-cooper: Not exactly, but this should be close enough

type FooBar = Foo Int | Bar String Int

constructorName : FooBar -> String
constructorName x = case x of
  Foo _ -> "Foo"
  Bar _ _ -> "Bar"

@gurdiga
Copy link

gurdiga commented Jul 7, 2017

It looks like you can also destructure with a plain let assignment: 🤓

import Html exposing (text)

type Tag = Tag String

t : Tag
t = Tag "Elm destructurization"

main =
  let (Tag s) = t
  in text ("Hello, the " ++ s ++ " World!")

Hint: This is copy-paste-able into http://elm-lang.org/try. :neckbeard:

@yang-wei
Copy link
Author

@gurdiga nice !!!

@josephscottstevens
Copy link

@yang-wei

I updated the grammer a bit, see gist

@maciejsikora
Copy link

Custom type destructuring:

type Force = Force Float Float

addForces: Force -> Force -> Force
addForces (Force a1 b1) (Force a2 b2) = Force (a1 + a2) (b1 + b2)

@jamespeterschinner
Copy link

jamespeterschinner commented Oct 26, 2020

Are there some pattern matching for records fields into lambda arguments?

Yes!

 (\{a, b} -> (a, b)) {a=1, b=2} == (1,2) 

@samuelstevens
Copy link

Has anyone found a way to pattern match record fields that are also custom types?

For example:

type Id = Id int
type alias Channel = { id: Id }

getId : Channel -> Int
getId ( what to put here? ) =
    id -- as an integer

I thought about { (Id idAsInt) }, which doesn't work.

@jamespeterschinner
Copy link

jamespeterschinner commented Nov 5, 2020

Elm 0.19

Taken from elm-sortable-table

type Config data msg
    = Config
        { toId : data -> String
        , toMsg : State -> msg
        , columns : List (ColumnData data msg)
        , customizations : Customizations data msg
        }

view : Config data msg -> State -> List data -> Html msg
view ((Config { toId, toMsg, columns, customizations }) as conf) state data =
    ...

And another simpler example

type TwoInts = TwoInts Int Int

secondInt (TwoInts first second) = 
    second

And on the repl:

> someValue = TwoInts 1 2
TwoInts 1 2 : TwoInts
> secondInt someValue
2 : Int
>

@shybovycha
Copy link

shybovycha commented May 14, 2021

Used to have in my code spaghetti like this:

update msg state = case msg of
  (ChangePlayerName newPlayerName) -> case state of
    NotStarted _ gameId -> (NotStarted newPlayerName gameId, Cmd.none)
    _ -> (state, Cmd.none)

  (ChangeGameId newGameId) -> case state of
    NotStarted playerName _ -> (NotStarted playerName newGameId, Cmd.none)
    _ -> (state, Cmd.none)

  -- 10 more branches like above

  _ -> (state, Cmd.none)

Use case: only allow the message to be processed when the app is in a certain state. Otherwise - ignore the message (potentially pitfall?).

Then, with the help of GlobalWebIndex/cmd-extra package, it simplified a little bit to:

import Cmd.Extra exposing (pure)

update msg state = case msg of
  (ChangePlayerName newPlayerName) -> case state of
    NotStarted _ gameId -> NotStarted newPlayerName gameId |> pure
    _ -> pure state

  (ChangeGameId newGameId) -> case state of
    NotStarted playerName _ -> NotStarted playerName newGameId |> pure
    _ -> pure state

  -- 10 more branches like above

  _ -> pure state

Finally, with the simple pattern matching:

update msg state = case (msg, state) of
  (ChangePlayerName newPlayerName, NotStarted _ gameId) -> NotStarted newPlayerName gameId |> pure

  (ChangeGameId newGameId, NotStarted playerName _) -> NotStarted playerName newGameId |> pure

  _ -> pure state

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment