2 Diving In

2.5 Pattern Matching

2.5.1 What Are Patterns?

Pattern matching in Erlang is huge, and it has a proportional impact on LFE and what one can do with this dialect of Lisp. Pattern matching in LFE can be used in function clauses, let, case, receive and in the macros cond, lc, and bc. From the REPL, pattern matching may be done in set as well.

Pattern matching in LFE happens when an expression matches a given pattern, e.g.:

(... (<pattern> <expression>) ...)

where the <pattern> might be something like this:

(binary (f float (size 32))
        (b bitstring))

or this:

(tuple 'ok value)

or this:

(list a b c)

or this:

(cons h t)

and the <expression> is any legal LFE expression. Ideally, it will return data that will be matched by the pattern.

If the matching succeeds, any unbound variables in the pattern become bound. If the matching fails, a run-time error occurs. All of this is best understood through the examples given below. Each example is preceeded by the general form of pattern as used in the given context. This should help keep things clear, even when the examples get convoluted.

2.5.2 Patterns in Forms

2.5.2.1 let

Pattern matching in let has the following general form:

(let ((<pattern> <expression>)
      (<pattern> <expression>) ... )
  ... )

Examples:

> (let (((tuple len status data) #(8 ok "Trillian")))
       (list len status data))
(8 ok "Trillian")
>

In this example, we have a pattern of (tuple len status data) and this is getting matched against our expression which is some data of the form #(8 ok "Trillian"). The pattern expects a tuple, and a tuple is what we gave it. With the pattern's variables bound inside the let, we can return a list of the variables.

If our pattern was written to expect a list and the expression was a tuple, we'd get a badmatch error:

> (let (((list len status data) #(8 ok "Trillian")))
       (list len status data))
exception error: #(badmatch #(8 ok "Trillian"))

>

Whatever our expression is going to be needs to be matched in the pattern. If we had a list integers in the expression, we would need a pattern like (list i1 i2 i3 ...).

Here's a super-simplified version of a let with pattern matching:

> (let ((data "Trillian"))
       (list data))
("Trillian")
>

Here our pattern was simply the variable data and our expression was the string "Trillian". This, of course, is easily recognized as a standard variable assignment within a let.

Patterns can nest, though, and with this you can start to get a sense of the power they hold. Let's look at a more complicated example:

> (let (((tuple lens status data)
         #((8 43) #(err "msg too short") "Trillian")))
       (list lens status data))
("\b+" #(err "msg too short") "Trillian")
>

As you can see, we've nested our expression: length is a two-valued list and status is a two-valued tuple. Our pattern, however, is still simple. But this is going to change: we want to extract our data into more variables, and we do this by mirroring the expression data structure in the pattern itself:

> (let (((tuple (list len-data len-total) (tuple status-code status-msg) data)
         #((8 43) #(err "msg too short") "Trillian")))
       (list len-data len-total status-code status-msg data))
(8 43 err "msg too short" "Trillian")
>

As you can see, our nested pattern extracted the data into the pattern's variables. If all we cared about was the status message, we could make this simpler by using the "I don't care" variable (the underscore):

> (let (((tuple (list _ _) (tuple _ status-msg) _)
         #((8 43) #(err "msg too short") "Trillian")))
       (list status-msg))
("msg too short")

Having seen these examples, you are probably gaining some insight into the power of pattern matching in Erlang and LFE. There's more, though :-) See below for equally potent uses.

2.5.2.2 case

Pattern matching in case has the following general form:

(case <expression>
  (<pattern> <expression> ... )
  (<pattern> <expression> ... )
  ...)

Keep in mind that case may also be used (optionally) inside the try form. For more information on try, see section 5.2.

Let's take a look at case in action:

> (set data #(6 warn "Arthur"))
#(6 warn "Arthur")
> (case data
    ((tuple len 'ok msg)
      (io:format "~s seems good.~n" (list msg)))
    ((tuple len 'err msg)
      (io:format "There's a problem with ~s.~n" (list msg)))
    ((tuple len 'warn msg)
      (io:format "Be careful of ~s.~n" (list msg))))
Be careful of Arthur.
ok
>

The patterns we are using in this case example expect data of one particular format, differentiating by the second element of the provided tuple. With new data, we can exercise the other cases:

> (set data #(8 ok "Trillian"))
#(8 ok "Trillian")

We won't re-type the case example here; just hit the "up" arror until you get to the case entry and hit return:

> (case ...)
Trillian seems good.
ok
>

Similarly, we can test the remaining case:

> (set data #(6 err "Zaphod"))
#(6 err "Zaphod")
> (case ...)
There's a problem with Zaphod.
ok
>

2.5.2.3 receive

Pattern matching in receive has the following general form:

(receive
  (<pattern> ... )
  (<pattern> ... )
  ...
  (after timeout
    ... ))

There is a tutorial on working with Erlang's light weight processes in LFE, and several example usages of receive are given there. On the second page of that tutorial, we see that any message sent to receive is accepted and processed. In the example below, we replace the simple pattern of the whole data (i.e., msg) with a series of patterns that will print only if the message matches one of the provided patterns.

Save the following in a file named rcv-pttrn.lfe:

(defmodule rcv-pttrn
  (export (safety-check 0)))

(defun safety-check ()
  (receive
    ((tuple 'ok item)
      (io:format "~s is safe to approach.~n" (list item))
      (safety-check))
    ((tuple 'warn item)
      (io:format "Approach ~s with extreme caution.~n" (list item))
      (safety-check))
    ((tuple 'crit item)
      (io:format "Withdraw from ~s immediately!~n" (list item))
      (safety-check))))

Next, start up the LFE REPL, compile the module above, and start our safety server:

> (c "rcv-pttrn")
#(module rcv-pttrn)
> (set pid (spawn 'rcv-pttrn 'safety-check ()))
<0.34.0>
>

Now let's give our patterns a try by sending messages to the server process:

> (! pid #(ok "Earth"))
#(ok "Earth")
Earth is safe to approach.
> (! pid #(warn "Frogstar"))
#(warn "Frogstar")
Approach Frogstar with extreme caution.
> (! pid #(crit "Krikkit"))
#(crit "Krikkit")
Withdraw from Krikkit immediately!
>

As you can see, the receive patterns are working.

We can also see what happens when we send messages that don't match any of the defined patterns:

> (! pid #(noop "This won't be matched"))
#(noop "This won't be matched")
> (! pid "Neither will this"))
"Neither will this"
>

Absolutely nothing, that's what. Well, nothing from the process we spawned, that is... just the REPL doing its thang.

2.5.2.4 cond

Pattern matching in cond has the following general form:

(cond (<test> ... )
      ((?= <pattern> <expr>) ... )
      ... )

Typically, a cond looks like this:

(cond ((== a 1) (io:format "It's one!"))
      ((== a 2) (io:format "It's two!")))

In other words, a series of tests with conditional results. LFE extends the basic form with support for pattern matching, as seen in the general form above.

Here's an example of how one can do pattern matching in LFE with cond (starting with the setting of some data):

> (set data #(8 ok "Trillian"))
#(8 ok "Trillian")
> (cond ((?= (tuple len 'ok msg) data)
         (io:format "~s seems good.~n" (list msg)))
        ((?= (tuple len 'err msg) data)
         (io:format "There's a problem with ~s.~n" (list msg)))
        ((?= (tuple len 'warn msg) data)
         (io:format "Be careful of ~s.~n" (list msg))))
Trillian seems good.
ok
>

Note that this is a replacement of the case example above.

We can set the data variable differently to exercise the other code paths, and then enter the cond expression from above (elided below to save space):

> (set data #(6 warn "Arthur"))
#(6 warn "Arthur")
> (cond ... )
Be careful of Arthur.
ok
> (set data #(6 err "Zaphod"))
#(6 err "Zaphod")
> (cond ... )
There's a problem with Zaphod.
ok
>

2.5.3 Special Cases

2.5.3.1 set in the REPL

Using set in the REPL has the following general form:

(set <pattern> <expression>)

Note that set is only valid when running the LFE shell. Example usage:

> (set (tuple len status data)
       #(8 ok "Trillian"))
#(8 ok "Trillian")
> len
8
> status
ok
> data
"Trillian"
>

2.5.3.2 Aliases with =

Aliases are defined with the following general form:

( ... (= <pattern 1> <pattern 2>) ... )

Aliases can be used anywhere in a pattern. A quick example of this, updating the previous example with aliases:

> (set (= (tuple len status data) (tuple a b c))
       #(8 ok "Trillian"))
#(8 ok "Trillian")
>

The same variables that were bound in the previous example are bound in this one:

> len
8
> status
ok
> data
"Trillian"
>

In addition, however, we have aliased new variables to these:

> a
8
> b
ok
> c
"Trillian"

2.5.3.3 Arguments to defun

Pattern matching in functions has the following general form:

    (defun name
      ((argpat ...) ...)
      ...)

We haven't covered functions yet (that's coming up in Chapter 4), so this will be a short preview focusing just on the pattern usage in functions, with more detail coming later.

Proper functions can't be defined in the LFE REPL, so save the following to func-pttrn.lfe:

(defmodule func-pttrn
  (export (safety-check 2)))

(defun safety-check
  (('ok msg)
    (io:format "~s seems good.~n" (list msg)))
  (('warn msg)
    (io:format "There's a problem with ~s.~n" (list msg)))
  (('crit msg)
    (io:format "Be careful of ~s.~n" (list msg))))

As you can see, the usual function arguments have been replaced with a pattern. In particular, this function will accept any of three options with two arguments each: where the first argument is 'ok, or where it is 'warn, or where it is 'crit.

Let's compile our new module from the LFE REPL:

> (c "func-pttrn")
#(module func-pttrn)
>

Now let's step it through its paces:

> (func-pttrn:safety-check 'ok "Trillian")
Trillian seems good.
ok
> (func-pttrn:safety-check 'warn "Arthur")
There's a problem with Arthur.
ok
> (func-pttrn:safety-check 'crit "Zaphod")
Be careful of Zaphod.
ok
>

If a pattern is not matched in our example (which has no fallback pattern), an error is raised:

> (func-pttrn:safety-check 'oops "Eccentrica Gallumbits")
exception error: #(case_clause #(oops "Eccentrica Gallumbits"))
  in (func-pttrn safety-check 2)

2.5.3.4 Arguments to Anonymous Functions

One can use patterns in arguments with anonymous functions similarly to how one does with named functions, demonstrated above. In LFE, this is done with match-lambda. Here's an example done in the REPL:

> (set safety-check
    (match-lambda
      (('ok msg)
        (io:format "~s seems good.~n" (list msg)))
      (('warn msg)
        (io:format "There's a problem with ~s.~n" (list msg)))
      (('crit msg)
        (io:format "Be careful of ~s.~n" (list msg)))))
#Fun<lfe_eval.31.53503600>
>

Usage is similar as well:

> (funcall safety-check 'warn "Arthur")
There's a problem with Arthur.
ok
> (funcall safety-check 'oops "Eccentrica Gallumbits")
exception error: function_clause

>

2.5.3.5 Patterns in Comprehensions

List and binary comprehensions make use of patterns in a limited sense. They have the following general forms:

(<- pat guard list-expr)

and

(<= bin-pat guard binary-expr)

where the guard in both cases is optional.

You can read more about LFE comprehensions in section 3.3