Skip to content

Instantly share code, notes, and snippets.

@rootscript
Forked from yang-wei/destructuring.md
Last active December 15, 2021 20:44
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rootscript/52acc102e7d2c590dcb3d6fd064490e8 to your computer and use it in GitHub Desktop.
Save rootscript/52acc102e7d2c590dcb3d6fd064490e8 to your computer and use it in GitHub Desktop.
Elm Destructuring (or Pattern Matching) cheatsheet

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

Nested record

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

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

robertjlooby commented on Feb 17

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"

eskimoblood commented on Feb 18

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.


pancakeCaptain commented on Apr 26

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

mcampbell commented on Aug 5edited

@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.


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