Elixir Syntax Series

  1. Data Types and Collections
  2. Variables
  3. Control Flow
  4. Functions

Assignment

In imperative programming languages, variables are assigned to specific memory locations. This memory can be written to, modified, and ultimately reclaimed through garbage collection. Imperative programs pass pointers to data, mutating the data in place.

Binding

Functional programming languages work differently. They assign a value to a specific memory position and bind the variable to this location. The memory is immutable, meaning it can be allocated or reclaimed, but not altered. Functional programs can safely share memory because immutability prevents unintended changes.

Understanding Immutability

Most modern programming languages have strategies to enforce a kind of immutability, whether by convention, such as using all-caps in Python, or through built-in language features, such as final in Java and const in JavaScript.

const a = 1;
a = 2; // Error: you cannot change a const variable.
let b = 1;
b = 2; // b is successfully mutated to the value 2.

Here, JavaScript uses the keyword const to prevent the variable from being overwritten, providing a measure of safety. This restriction only applies to primitive values. Arrays and objects can be mutated even when declared with const.

Erlang treats variable bindings as permanent within a given scope, similar to JavaScript’s const:

A = 1.
A = 2. % Error: you cannot reassign a variable.

Once a variable is bound in Erlang, it cannot be rebound within the same scope. This mirrors the mathematical concept of a variable, which remains constant once defined.

In Elixir, however, variables can be rebound within their context, allowing for mutable-like behavior in terms of rebinding, but the underlying data remains immutable.

a = 1
a = 2

When a variable is rebound to another value, it does not overwrite the existing data. Instead, it points to a new memory location, preserving the immutability of the original value.

Pattern Matching

In Erlang and Elixir, the = operator functions differently from most programming languages where it typically signifies assignment. Instead, it is used for pattern matching.

If the left side is a variable name, it will bind (or rebind, in Elixir) to the value on the right. However, if it is a literal value, it must match the result of the expression on the right.

any_name = 2 + 3   # binds any_name to 5
2 = 1 + 1          # succeeds
3 = 1 + 1          # raises a match error

In the example above, the last line raises an error because the literal 3 does not match the result of 1 + 1.

Pattern matching plays a central role in how Elixir and Erlang implement control flow, especially in function type constraints and case expressions.

Decomposition

Lists

Erlang and Elixir leverage pattern matching to decompose lists:

[head | tail] = [1, 2, 3]

In the example above, head is bound to the value 1, and tail is bound to the list [2, 3]. This approach is particularly useful for recursively processing lists.

Maps

Erlang and Elixir use pattern matching to decompose maps:

alice = %{first: "Alice", last: "Smith", age: 28}

%{first: first_name} = alice

Here, first_name is bound to "Alice", extracting it directly from the map alice.

Tuples

One of the most common decomposition patterns involves tuples:

{status, value} = {:ok, 4}

In this example, status is bound to :ok, and value is bound to 4. This pattern is particularly useful for handling function outputs where operations can result in more than one outcome, such as success or failure, simplifying execution paths.

{:success, result}
{:error, reason}

Conclusion

Erlang and Elixir’s use of pattern matching simplifies variable assignment, making it easier to handle different data structures without extensive conditional logic. This approach improves code readability and allows for the decomposition of complex data directly within variable assignments.