Skip to content

Instantly share code, notes, and snippets.

@yang-wei
Last active February 20, 2024 04: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
@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