Beginner

Operators in Elixir

Explore arithmetic, comparison, boolean, and functional operators in Elixir, including the pipe operator, match operator, and list operations

Operators in Elixir are not just syntactic sugar for arithmetic — they are the everyday vocabulary you use to compose functional pipelines, match values, and transform immutable data. As a functional language running on the BEAM, Elixir gives operators a distinct flavor: the equals sign is not assignment but pattern matching, the pipe operator threads values through transformations, and there are two flavors of boolean logic (strict and relaxed) depending on whether you want truthy semantics or strict boolean checks.

This tutorial walks through the operators you will reach for in nearly every Elixir program. We will look at arithmetic, comparison, boolean logic, string and list operators, and the two operators that define the Elixir style most strongly — the match operator (=) and the pipe operator (|>).

Because Elixir values are immutable, none of these operators mutate their operands. Each expression returns a new value, which is at the heart of how functional pipelines stay safe to reason about.

Arithmetic Operators

Elixir provides the familiar arithmetic operators plus a few distinctions that matter for integer math. The / operator always returns a float, even when both operands are integers. For integer division and remainder, Elixir uses the div/2 and rem/2 functions instead of a dedicated operator.

Create a file named arithmetic.exs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
a = 17
b = 5

IO.puts("a + b = #{a + b}")
IO.puts("a - b = #{a - b}")
IO.puts("a * b = #{a * b}")
IO.puts("a / b = #{a / b}")
IO.puts("div(a, b) = #{div(a, b)}")
IO.puts("rem(a, b) = #{rem(a, b)}")

# Unary minus
IO.puts("-a = #{-a}")

# Exponentiation via Erlang's :math.pow (always returns a float)
IO.puts("2 ** 10 = #{:math.pow(2, 10)}")

Notice that 17 / 5 produces 3.4, not 3. If you want integer division, reach for div/2. Elixir 1.13+ also has a ** operator for exponentiation, but :math.pow/2 from the underlying Erlang math module is the historically portable choice and always returns a float.

Comparison and Equality

Elixir has both relaxed equality (==) and strict equality (===). The relaxed form treats 1 and 1.0 as equal, while the strict form distinguishes integers from floats. Both forms have negation counterparts (!= and !==).

Create a file named comparison.exs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
IO.puts("1 == 1.0  => #{1 == 1.0}")
IO.puts("1 === 1.0 => #{1 === 1.0}")
IO.puts("1 != 2    => #{1 != 2}")
IO.puts("1 !== 1.0 => #{1 !== 1.0}")

IO.puts("3 < 5     => #{3 < 5}")
IO.puts("5 <= 5    => #{5 <= 5}")
IO.puts("\"abc\" < \"abd\" => #{"abc" < "abd"}")

# Elixir defines a total ordering across all types
IO.puts("1 < :atom    => #{1 < :atom}")
IO.puts(":atom < \"s\" => #{:atom < "s"}")

One quirk worth knowing: Elixir defines a total ordering across all types, so you can compare a number to an atom or a string without an error. The ordering is: number < atom < reference < function < port < pid < tuple < map < list < bitstring. You rarely rely on this directly, but it means sorts and comparisons never crash on mixed types.

Boolean Operators: Strict vs Relaxed

Elixir has two families of boolean operators. The strict forms — and, or, not — require their left operand to be a strict boolean (true or false). The relaxed forms — &&, ||, ! — accept any value, treating nil and false as falsy and everything else as truthy.

Create a file named booleans.exs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Strict boolean operators require true/false
IO.puts("true and false  => #{true and false}")
IO.puts("true or false   => #{true or false}")
IO.puts("not true        => #{not true}")

# Relaxed (short-circuit) operators accept any value
# They return the actual value, not just true/false
IO.inspect(nil || "default", label: "nil || \"default\"")
IO.inspect("first" || "second", label: "\"first\" || \"second\"")
IO.inspect(nil && "never", label: "nil && \"never\"")
IO.inspect("hello" && "world", label: "\"hello\" && \"world\"")
IO.inspect(!nil, label: "!nil")

The relaxed forms are how you express defaults and guards on potentially-nil values. They also short-circuit, so expensive_call() || fallback() will skip the fallback when the first call returns a truthy value.

String, List, and the Pipe Operator

Elixir uses dedicated operators for concatenating strings (<>), concatenating lists (++), and subtracting list elements (--). The match operator (=) binds names to values via pattern matching, and the pipe operator (|>) feeds the result of one expression as the first argument to the next function call — the defining shape of idiomatic Elixir code.

Create a file named operators.exs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# String concatenation with <>
greeting = "Hello, " <> "World!"
IO.puts(greeting)

# List concatenation with ++ and subtraction with --
combined = [1, 2, 3] ++ [4, 5]
trimmed = [1, 2, 3, 4, 5] -- [2, 4]
IO.inspect(combined, label: "combined")
IO.inspect(trimmed, label: "trimmed")

# The match operator: = is pattern matching, not assignment
{x, y, z} = {10, 20, 30}
IO.puts("x=#{x}, y=#{y}, z=#{z}")

[head | tail] = [1, 2, 3, 4]
IO.puts("head=#{head}")
IO.inspect(tail, label: "tail")

# The pipe operator threads the value as the first argument
result =
  "  hello world  "
  |> String.trim()
  |> String.upcase()
  |> String.replace(" ", "-")

IO.puts("piped result: #{result}")

# Membership operator: in
IO.puts("3 in [1, 2, 3]  => #{3 in [1, 2, 3]}")
IO.puts("9 in 1..5       => #{9 in 1..5}")

The match operator is where Elixir most clearly departs from imperative languages. {x, y, z} = {10, 20, 30} does not assign three variables — it asserts that the left side matches the right side and binds any unbound variables in the pattern. If the shapes do not match, you get a MatchError.

The pipe operator is the workhorse of Elixir style. value |> f(arg) is equivalent to f(value, arg). This lets you read data transformations top-to-bottom rather than inside-out, which fits the functional emphasis on shaping data through composed functions.

Running with Docker

1
2
3
4
5
6
7
8
# Pull the Elixir image
docker pull elixir:1.17-alpine

# Run each example
docker run --rm -v $(pwd):/app -w /app elixir:1.17-alpine elixir arithmetic.exs
docker run --rm -v $(pwd):/app -w /app elixir:1.17-alpine elixir comparison.exs
docker run --rm -v $(pwd):/app -w /app elixir:1.17-alpine elixir booleans.exs
docker run --rm -v $(pwd):/app -w /app elixir:1.17-alpine elixir operators.exs

Expected Output

Running arithmetic.exs:

a + b = 22
a - b = 12
a * b = 85
a / b = 3.4
div(a, b) = 3
rem(a, b) = 2
-a = -17
2 ** 10 = 1024.0

Running comparison.exs:

1 == 1.0  => true
1 === 1.0 => false
1 != 2    => true
1 !== 1.0 => true
3 < 5     => true
5 <= 5    => true
"abc" < "abd" => true
1 < :atom    => true
:atom < "s" => true

Running booleans.exs:

true and false  => false
true or false   => true
not true        => false
nil || "default": "default"
"first" || "second": "first"
nil && "never": nil
"hello" && "world": "world"
!nil: true

Running operators.exs:

Hello, World!
combined: [1, 2, 3, 4, 5]
trimmed: [1, 3, 5]
x=10, y=20, z=30
head=1
tail: [2, 3, 4]
piped result: HELLO-WORLD
3 in [1, 2, 3]  => true
9 in 1..5       => false

Key Concepts

  • / always returns a float — use div/2 and rem/2 for integer division and remainder
  • Two equality operators== is relaxed (1 == 1.0 is true), === is strict (1 === 1.0 is false)
  • Strict vs relaxed booleansand/or/not require true booleans; &&/||/! accept any value and treat nil and false as falsy
  • Short-circuiting returns valuesnil || "default" returns "default", not just true
  • = is the match operator — it pattern-matches the right side against the left, binding unbound names
  • Pipe operator threads datavalue |> f(arg) becomes f(value, arg), enabling readable functional pipelines
  • Dedicated operators per type<> for strings, ++/-- for lists; mixing them up is a type error rather than implicit coercion
  • Total ordering across types — any two terms can be compared with </>, which keeps generic sorts safe on mixed data

Running Today

All examples can be run using Docker:

docker pull elixir:1.17-alpine
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining