Equals is an Assertion, not an Assignment

One of the most difficult mental shifts when going from imperative to functional programming comes when reading the ‘=’ operator. At least, I’ve found this to be true of me. For the benefit of anyone else with the same problem, I want to present a potentially new and hopefully helpful way of thinking about “assignment” in functional languages.

First off though, a note on scope: This article is about pattern-matching functional languages like Elixir, Erlang and Haskell. It is not applicable (as far as I know) to Lisps.

Here’s a line out of one of my Elixir programs.

{:ok, body} = fetch_feed(account)

Reading this causes immediate problems for the reader who is fresh off an imperative language. Naively translated to Ruby, it looks like:

[:ok, body] = fetch_feed(account)

…which makes no sense, and doesn’t compile, because you can’t assign a value to an array.

The reader reads up on pattern-matching, though, and begins to understand this code a bit better. She realizes it’s kind of like doing regex pattern-matching in Ruby.

match = /(\d{3}) - (.*)/.match("114 - Null Object")
match[1]                        # => "114"
match[2]                        # => "Null Object"

In the Elixir code a successful pattern match results in variables being bound. We can get a little closer to that in our regular-expression example using named captures.

match = /(?<number>\d{3}) - (?<name>.*)/.match("114 - Null Object")
match[:number]                  # => "114"
match[:name]                    # => "Null Object"

This is as far as most explanations of pattern-matching get, in my experience. But it doesn’t tell the whole story.

Let’s look at an alternative syntax for doing regular expression matches in Ruby.

/(?<number>\d{3}) - (?<name>.*)/ =~ "114 - Null Object"
number                          # => "114"
name                            # => "Null Object"

This (little-known) form of regex matching causes local variables to be set as a side effect of the match. (This might not seem quite so strange if you’re aware that Ruby also sets a bunch of Perl-style automatic variables like $&, $1, $2, and so on, as a side effect of a regex match).

This is starting to look a little closer to our original example of a functional “assignment”. But we’re not there yet. Here’s what I think a pattern-matching assignment is really equivalent to in Ruby regex-matching terms:

data = "114 - Null Object"
/(?<number>\d{3}) - (?<name>.*)/ =~ data or raise "No match!"
number                          # => "114"
name                            # => "Null Object"

(I’ve extracted the data to match on into a variable to keep the lines shorter)

See the addition of the assertion at the end of the matching operation? That’s the key piece that was missing before.

Because using the ‘=’ operator in a language like Elixir doesn’t just pattern-match. It asserts that the pattern match will be successful. If it doesn’t succeed, the code will fail at that point. This is the realization that I came to about pattern-matching languages: the ‘=’ operator isn’t assignment. It’s a pattern-matching assertion which may, as a side effect, bind variables.

Sometimes the pattern is very simple. A line like this will always succeed:

result = fetch_feed(account)

That’s because Elixir has a rule that says, in effect, “a pattern consisting of a bare variable name will match any value”. Oh, and coincidentally it will also assign that value to the variable name.

In effect, assigning to a bare variable is like matching on a universal regex:

data = "114 - Null Object"
/(?<result>.*)/ =~ data or raise "No match!"
result                          # => "114 - Null Object"

(Note that other languages handle bare variable names a little differently, and add the proviso that the variable must previously be un-assigned in order to match anything.)

So when you see code like this:

{:ok, body} = fetch_feed(account)

You can read it like this: “Try and match the result of fetch_feed(account) against a pattern consisting of a two-element tuple, where the first element is the atom :ok, and the second element can be anything. If the match succeeds, also assign the second element to the variable name body. If it fails, raise an exception immediately.”

I hope this helps someone.