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 !
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
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
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
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
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:
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
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 5 • 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.