Elixir is a dynamically typed, functional language where all data is immutable. Rather than assigning values to variables in the traditional sense, Elixir uses pattern matching with the = operator to bind values to names. Once bound, the underlying data cannot be changed—you create new values through transformations instead.
This approach eliminates entire categories of bugs related to shared mutable state, and it’s central to how Elixir achieves safe concurrency on the BEAM VM. Understanding how bindings and types work in Elixir is essential for thinking functionally.
In this tutorial, you’ll learn about Elixir’s basic data types, how variable binding and rebinding work, how pattern matching relates to variables, and how to inspect and convert between types.
Basic Types and Bindings
Elixir has a rich set of built-in types. Variables are bound using the match operator (=), and names follow snake_case convention.
Create a file named variables.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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| # Integers - arbitrary precision (no overflow)
age = 30
big_number = 1_000_000_000_000
hex_value = 0xFF
binary_value = 0b1010
IO.puts("age: #{age}")
IO.puts("big_number: #{big_number}")
IO.puts("hex_value: #{hex_value}")
IO.puts("binary_value: #{binary_value}")
# Floats - 64-bit double precision
pi = 3.14159
temperature = -40.0
scientific = 1.0e-3
IO.puts("pi: #{pi}")
IO.puts("temperature: #{temperature}")
IO.puts("scientific: #{scientific}")
# Booleans
is_active = true
is_deleted = false
IO.puts("is_active: #{is_active}")
IO.puts("is_deleted: #{is_deleted}")
# Atoms - named constants (their name is their value)
status = :ok
color = :red
IO.puts("status: #{status}")
IO.puts("color: #{color}")
# Strings - UTF-8 encoded binaries
greeting = "Hello, Elixir!"
multiline = "line one\nline two"
IO.puts("greeting: #{greeting}")
IO.puts("multiline: #{multiline}")
# Charlists - list of character code points (Erlang compatibility)
charlist = ~c"hello"
IO.puts("charlist: #{charlist}")
# Nil - absence of a value
nothing = nil
IO.puts("nothing: #{inspect(nothing)}")
# Checking types
IO.puts("\n--- Type Checks ---")
IO.puts("is_integer(age): #{is_integer(age)}")
IO.puts("is_float(pi): #{is_float(pi)}")
IO.puts("is_boolean(is_active): #{is_boolean(is_active)}")
IO.puts("is_atom(status): #{is_atom(status)}")
IO.puts("is_binary(greeting): #{is_binary(greeting)}")
IO.puts("is_nil(nothing): #{is_nil(nothing)}")
|
Pattern Matching and Rebinding
In Elixir, = is the match operator, not an assignment operator. While it looks like assignment for simple bindings, it becomes much more powerful with complex data structures.
Create a file named variables_patterns.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
31
32
33
34
35
36
37
38
39
40
| # Simple binding looks like assignment
x = 42
IO.puts("x = #{x}")
# Rebinding: the name can be bound to a new value
x = 100
IO.puts("x rebound to #{x}")
# Pattern matching with tuples
{a, b, c} = {1, "hello", :world}
IO.puts("a: #{a}, b: #{b}, c: #{c}")
# Pattern matching with lists
[head | tail] = [1, 2, 3, 4, 5]
IO.puts("head: #{head}")
IO.puts("tail: #{inspect(tail)}")
# Underscore ignores values you don't need
{_, second, _} = {:first, "I want this", :third}
IO.puts("second: #{second}")
# Pin operator (^) matches against existing value instead of rebinding
y = 10
{^y, z} = {10, 20}
IO.puts("y stayed: #{y}, z: #{z}")
# This would raise a MatchError because y is pinned to 10:
# {^y, z} = {99, 20} # ** (MatchError) no match of right hand side value
# Pattern matching in function heads
defmodule Classifier do
def describe(0), do: "zero"
def describe(n) when n > 0, do: "positive"
def describe(n) when n < 0, do: "negative"
end
IO.puts("\n--- Pattern Matching in Functions ---")
IO.puts("describe(0): #{Classifier.describe(0)}")
IO.puts("describe(42): #{Classifier.describe(42)}")
IO.puts("describe(-7): #{Classifier.describe(-7)}")
|
Collection Types
Elixir has several built-in collection types, each with different performance characteristics and use cases.
Create a file named variables_collections.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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
| # Lists - linked lists, good for prepending
numbers = [1, 2, 3, 4, 5]
mixed = [1, "two", :three, 4.0]
IO.puts("numbers: #{inspect(numbers)}")
IO.puts("mixed: #{inspect(mixed)}")
# List operations create new lists (immutability)
prepended = [0 | numbers]
concatenated = numbers ++ [6, 7]
IO.puts("prepended: #{inspect(prepended)}")
IO.puts("concatenated: #{inspect(concatenated)}")
# Tuples - fixed-size, contiguous memory, fast access by index
point = {10, 20}
rgb = {255, 128, 0}
IO.puts("point: #{inspect(point)}")
IO.puts("rgb element 1: #{elem(rgb, 1)}")
IO.puts("tuple size: #{tuple_size(rgb)}")
# Maps - key-value pairs
user = %{name: "Alice", age: 30, active: true}
IO.puts("user: #{inspect(user)}")
IO.puts("name: #{user.name}")
IO.puts("age: #{user[:age]}")
# Maps with non-atom keys
scores = %{"math" => 95, "science" => 88}
IO.puts("math score: #{scores["math"]}")
# Updating maps (creates a new map)
updated_user = %{user | age: 31}
IO.puts("updated user: #{inspect(updated_user)}")
# Keyword lists - ordered list of two-element tuples with atom keys
options = [timeout: 5000, retries: 3, verbose: true]
IO.puts("\noptions: #{inspect(options)}")
IO.puts("timeout: #{options[:timeout]}")
# Ranges
range = 1..10
IO.puts("range: #{inspect(range)}")
IO.puts("5 in range?: #{5 in range}")
# MapSets
set = MapSet.new([1, 2, 3, 2, 1])
IO.puts("set: #{inspect(set)}")
IO.puts("set has 2?: #{MapSet.member?(set, 2)}")
# Type conversions
IO.puts("\n--- Type Conversions ---")
IO.puts("String to integer: #{String.to_integer("42")}")
IO.puts("String to float: #{String.to_float("3.14")}")
IO.puts("Integer to string: #{Integer.to_string(42)}")
IO.puts("Integer to float: #{42 / 1}")
IO.puts("Float to integer (trunc): #{trunc(3.7)}")
IO.puts("Float to integer (round): #{round(3.7)}")
IO.puts("Atom to string: #{Atom.to_string(:hello)}")
IO.puts("String to atom: #{inspect(String.to_atom("hello"))}")
|
Running with Docker
1
2
3
4
5
6
7
8
9
10
11
| # Pull the official image
docker pull elixir:1.17-alpine
# Run the basic types example
docker run --rm -v $(pwd):/app -w /app elixir:1.17-alpine elixir variables.exs
# Run the pattern matching example
docker run --rm -v $(pwd):/app -w /app elixir:1.17-alpine elixir variables_patterns.exs
# Run the collections example
docker run --rm -v $(pwd):/app -w /app elixir:1.17-alpine elixir variables_collections.exs
|
Expected Output
Output from variables.exs:
age: 30
big_number: 1000000000000
hex_value: 255
binary_value: 10
pi: 3.14159
temperature: -40.0
scientific: 0.001
is_active: true
is_deleted: false
status: ok
color: red
greeting: Hello, Elixir!
multiline: line one
line two
charlist: hello
nothing: nil
--- Type Checks ---
is_integer(age): true
is_float(pi): true
is_boolean(is_active): true
is_atom(status): true
is_binary(greeting): true
is_nil(nothing): true
Output from variables_patterns.exs:
x = 42
x rebound to 100
a: 1, b: hello, c: world
head: 1
tail: [2, 3, 4, 5]
second: I want this
y stayed: 10, z: 20
--- Pattern Matching in Functions ---
describe(0): zero
describe(42): positive
describe(-7): negative
Output from variables_collections.exs:
numbers: [1, 2, 3, 4, 5]
mixed: [1, "two", :three, 4.0]
prepended: [0, 1, 2, 3, 4, 5]
concatenated: [1, 2, 3, 4, 5, 6, 7]
point: {10, 20}
rgb element 1: 128
tuple size: 3
user: %{active: true, name: "Alice", age: 30}
name: Alice
age: 30
math score: 95
updated user: %{active: true, name: "Alice", age: 31}
options: [timeout: 5000, retries: 3, verbose: true]
timeout: 5000
range: 1..10
5 in range?: true
set: MapSet.new([1, 2, 3])
set has 2?: true
--- Type Conversions ---
String to integer: 42
String to float: 3.14
Integer to string: 42
Integer to float: 42.0
Float to integer (trunc): 3
Float to integer (round): 4
Atom to string: hello
String to atom: :hello
Key Concepts
- Immutable data — All values in Elixir are immutable. Operations like updating a map or prepending to a list always create new data structures rather than modifying existing ones.
= is pattern matching — The = operator matches the left side against the right side and binds variables. It goes far beyond simple assignment, destructuring tuples, lists, and maps.- Atoms are constants — Atoms like
:ok, :error, and true are named constants where the name is the value. Booleans true and false are actually atoms. - Dynamic but strongly typed — Elixir won’t implicitly convert between types. You must use explicit conversion functions like
String.to_integer/1 or Integer.to_string/1. - Pin operator (
^) — Use ^variable in a pattern match to match against the variable’s current value instead of rebinding it. - Strings are binaries — Elixir strings are UTF-8 encoded binaries, which is why
is_binary/1 returns true for strings. Charlists (~c"hello") are a separate type for Erlang interop. - Multiple collection types — Lists (linked lists), tuples (contiguous memory), maps (key-value), and keyword lists (ordered key-value with atom keys) each serve different purposes.
- Arbitrary precision integers — Elixir integers have no fixed size limit, so you never need to worry about integer overflow.
Comments
Loading comments...
Leave a Comment