Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
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

Some other cases:

List

You can match lists of definite length with the [] notation and match more than just the head element with multiple :::

listFn : List String -> String
listFn list =
  case list of
    [] -> "This list is empty!"
    [ _ ] -> "This list has one element"
    [ a, b ] -> "This list has two elements: " ++ a ++ " and " ++ b
    a::b::_ -> "This list has several elements. The first 2 are: " ++ a ++ " and " ++ b

General

You can destructure nested values in tagged union types:

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

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) :

f : Maybe Int -> String
f n =
  case n of
    Just 42 -> "You got it!"
    Just 41 -> "Almost!"
    Just _ -> "Nope!"
    Nothing -> "You have to at least try!"

The full value that is matched can be bound with the as keyword:

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"

Another case union types with only one member:

type MyThing
  = AString String


unionFn : MyThing -> String
unionFn  (AString a) = a

@eskimoblood that one is very useful for record-like opaque types as they don't get the magical accessor functions, especially as you can do partial matching:

type AThing = AThing { foo: String, bar: Int }

foo (AThing { foo }) = foo

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

It's not a community thing, as in many other functional language, _ is a wildcard match: it will match anything but importantly (and contrary to Python or Javascript for instance) it will not create a binding. Which means you can match to multiple _ in a single pattern.

mgold commented Feb 19, 2016

This looks great! Would you mind submitting it as a PR to elm-guides?

The latest version of Elm does support destructuring of nested types. Here's an example function I wrote to walk through an AST and return JavaScript code as a String.

format : ParseTree -> State -> State
format parseTree state =
  case parseTree of
    Node Feature (LeafNode (Description description) :: children) ->
      appendDesribe description children state

    Node Scenario (LeafNode (Description description) :: children) ->
      appendDesribe description children state

    Node Test (LeafNode (Description description) :: []) ->
      let
        open = tabs state ++ "it('" ++ description ++ "', function() {\n"
        close = tabs state ++ "});\n"
      in
        { state | output = state.output ++ open ++ close }

    _ ->
      state
Owner

yang-wei commented May 21, 2016

@pancakeCaptain nice to see that =)

mcampbell commented Aug 4, 2016 edited

@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 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 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 commented Mar 4, 2017 edited

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.

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 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 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:

Owner

yang-wei commented Jul 21, 2017

@gurdiga nice !!!

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