Erlang is a dynamically typed, functional language where all data is immutable and variables are single-assignment. Once a variable is bound to a value, it cannot be changed within the same scope. This is fundamentally different from most languages where variables are containers you can update — in Erlang, = is a pattern matching operator, not an assignment operator.
This single-assignment property is central to Erlang’s ability to run millions of concurrent processes safely. When data can never be mutated, there are no race conditions, no locks, and no shared-state bugs. Understanding how bindings and types work is essential for thinking the Erlang way.
In this tutorial, you’ll learn about Erlang’s core data types, how single-assignment binding works, how pattern matching relates to variables, and how to convert between types.
Core Data Types
Erlang has a rich set of built-in data types. Variables must start with an uppercase letter or underscore, and are bound using the match operator (=).
Create a file named variables.erl:
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
59
60
61
62
63
64
65
| #!/usr/bin/env escript
main(_) ->
%% Integers - arbitrary precision (no overflow)
Age = 30,
BigNumber = 1000000000000,
HexValue = 16#FF,
BinaryValue = 2#1010,
io:format("Age: ~w~n", [Age]),
io:format("BigNumber: ~w~n", [BigNumber]),
io:format("HexValue: ~w~n", [HexValue]),
io:format("BinaryValue: ~w~n", [BinaryValue]),
%% Floats - 64-bit double precision
Pi = 3.14159,
Temperature = -40.0,
Scientific = 1.0e-3,
io:format("Pi: ~w~n", [Pi]),
io:format("Temperature: ~w~n", [Temperature]),
io:format("Scientific: ~w~n", [Scientific]),
%% Atoms - named constants (start with lowercase or are single-quoted)
Status = ok,
Color = red,
SpecialAtom = 'an atom with spaces',
io:format("Status: ~w~n", [Status]),
io:format("Color: ~w~n", [Color]),
io:format("SpecialAtom: ~w~n", [SpecialAtom]),
%% Booleans are just atoms
IsActive = true,
IsDeleted = false,
io:format("IsActive: ~w~n", [IsActive]),
io:format("IsDeleted: ~w~n", [IsDeleted]),
%% Strings are lists of character codes
Greeting = "Hello, Erlang!",
io:format("Greeting: ~s~n", [Greeting]),
io:format("Greeting as list: ~w~n", [Greeting]),
%% Binaries - efficient byte sequences (modern string handling)
BinGreeting = <<"Hello, binary!">>,
io:format("BinGreeting: ~s~n", [BinGreeting]),
%% Tuples - fixed-size containers
Point = {10, 20},
Person = {person, "Alice", 30},
io:format("Point: ~w~n", [Point]),
io:format("Person: ~w~n", [Person]),
%% Lists
Numbers = [1, 2, 3, 4, 5],
Mixed = [1, hello, 3.14, "text"],
io:format("Numbers: ~w~n", [Numbers]),
io:format("Mixed: ~w~n", [Mixed]),
%% Type checking with guard BIFs
io:format("~n--- Type Checks ---~n"),
io:format("is_integer(Age): ~w~n", [is_integer(Age)]),
io:format("is_float(Pi): ~w~n", [is_float(Pi)]),
io:format("is_atom(Status): ~w~n", [is_atom(Status)]),
io:format("is_boolean(IsActive): ~w~n", [is_boolean(IsActive)]),
io:format("is_list(Numbers): ~w~n", [is_list(Numbers)]),
io:format("is_tuple(Point): ~w~n", [is_tuple(Point)]),
io:format("is_binary(BinGreeting): ~w~n", [is_binary(BinGreeting)]),
ok.
|
Single Assignment and Pattern Matching
In Erlang, = is the pattern matching operator. A variable can only be bound once per scope. Attempting to rebind a variable to a different value causes a badmatch error. This is the most important distinction from most other languages.
Create a file named variables_patterns.erl:
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
| #!/usr/bin/env escript
main(_) ->
%% Basic binding
X = 42,
io:format("X = ~w~n", [X]),
%% Matching against an already-bound variable succeeds if the value is the same
X = 42,
io:format("X still matches 42~n"),
%% Rebinding X to a different value would crash:
%% X = 100, %% ** exception error: no match of right hand side value 100
%% Pattern matching with tuples
{A, B, C} = {1, hello, 3.14},
io:format("A: ~w, B: ~w, C: ~w~n", [A, B, C]),
%% Tagged tuples - common Erlang pattern
{ok, Value} = {ok, "success"},
io:format("Value: ~s~n", [Value]),
%% Underscore ignores values
{_, Second, _} = {first, "I want this", third},
io:format("Second: ~s~n", [Second]),
%% List pattern matching - head and tail
[Head | Tail] = [1, 2, 3, 4, 5],
io:format("Head: ~w~n", [Head]),
io:format("Tail: ~w~n", [Tail]),
%% Multiple elements from the head
[First, Second2 | Rest] = [10, 20, 30, 40],
io:format("First: ~w, Second2: ~w, Rest: ~w~n", [First, Second2, Rest]),
%% Nested pattern matching
{point, {PX, PY}} = {point, {100, 200}},
io:format("Point X: ~w, Point Y: ~w~n", [PX, PY]),
%% Using bound variables in patterns
Target = error,
{Target, Reason} = {error, "not found"},
io:format("Matched ~w with reason: ~s~n", [Target, Reason]),
%% Pattern matching in case expressions
Result = {ok, 42},
case Result of
{ok, Val} ->
io:format("~nCase matched ok: ~w~n", [Val]);
{error, Err} ->
io:format("~nCase matched error: ~w~n", [Err])
end,
ok.
|
Maps, Records, and Type Conversions
Erlang provides maps for key-value data, and several functions for converting between types.
Create a file named variables_advanced.erl:
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
| #!/usr/bin/env escript
main(_) ->
%% Maps - key-value pairs (introduced in Erlang/OTP 17)
User = #{name => "Alice", age => 30, active => true},
io:format("User: ~w~n", [User]),
%% Accessing map values
Name = maps:get(name, User),
Age = maps:get(age, User),
io:format("Name: ~s, Age: ~w~n", [Name, Age]),
%% Safe access with default
Email = maps:get(email, User, "none"),
io:format("Email: ~s~n", [Email]),
%% Updating maps (creates a new map)
UpdatedUser = User#{age := 31},
io:format("Updated User: ~w~n", [UpdatedUser]),
%% Adding new keys
ExtendedUser = User#{email => "[email protected]"},
io:format("Extended User: ~w~n", [ExtendedUser]),
%% Pattern matching on maps
#{name := MatchedName, age := MatchedAge} = User,
io:format("Matched Name: ~s, Age: ~w~n", [MatchedName, MatchedAge]),
%% Proplist (list of tuples) - traditional key-value before maps
Options = [{timeout, 5000}, {retries, 3}, {verbose, true}],
Timeout = proplists:get_value(timeout, Options),
io:format("~nTimeout: ~w~n", [Timeout]),
%% Type conversions
io:format("~n--- Type Conversions ---~n"),
%% Integer <-> Float
IntVal = 42,
FloatVal = float(IntVal),
BackToInt = trunc(3.7),
Rounded = round(3.7),
io:format("float(42): ~w~n", [FloatVal]),
io:format("trunc(3.7): ~w~n", [BackToInt]),
io:format("round(3.7): ~w~n", [Rounded]),
%% Integer <-> String (list)
NumStr = integer_to_list(42),
StrNum = list_to_integer("42"),
io:format("integer_to_list(42): ~s~n", [NumStr]),
io:format("list_to_integer(\"42\"): ~w~n", [StrNum]),
%% Float <-> String (list)
FloatStr = float_to_list(3.14, [{decimals, 2}]),
StrFloat = list_to_float("3.14"),
io:format("float_to_list(3.14): ~s~n", [FloatStr]),
io:format("list_to_float(\"3.14\"): ~w~n", [StrFloat]),
%% Atom <-> String (list)
AtomStr = atom_to_list(hello),
StrAtom = list_to_atom("hello"),
io:format("atom_to_list(hello): ~s~n", [AtomStr]),
io:format("list_to_atom(\"hello\"): ~w~n", [StrAtom]),
%% List <-> Tuple
MyList = [1, 2, 3],
MyTuple = list_to_tuple(MyList),
BackToList = tuple_to_list(MyTuple),
io:format("list_to_tuple([1,2,3]): ~w~n", [MyTuple]),
io:format("tuple_to_list({1,2,3}): ~w~n", [BackToList]),
%% Binary <-> List (string)
Bin = list_to_binary("hello"),
Lst = binary_to_list(<<"hello">>),
io:format("list_to_binary(\"hello\"): ~w~n", [Bin]),
io:format("binary_to_list(<<\"hello\">>): ~s~n", [Lst]),
ok.
|
Running with Docker
1
2
3
4
5
6
7
8
9
10
11
| # Pull the official image
docker pull erlang:alpine
# Run the basic types example
docker run --rm -v $(pwd):/app -w /app erlang:alpine escript variables.erl
# Run the pattern matching example
docker run --rm -v $(pwd):/app -w /app erlang:alpine escript variables_patterns.erl
# Run the advanced types example
docker run --rm -v $(pwd):/app -w /app erlang:alpine escript variables_advanced.erl
|
Expected Output
Output from variables.erl:
Age: 30
BigNumber: 1000000000000
HexValue: 255
BinaryValue: 10
Pi: 3.14159
Temperature: -40.0
Scientific: 0.001
Status: ok
Color: red
SpecialAtom: 'an atom with spaces'
IsActive: true
IsDeleted: false
Greeting: Hello, Erlang!
Greeting as list: [72,101,108,108,111,44,32,69,114,108,97,110,103,33]
BinGreeting: Hello, binary!
Point: {10,20}
Person: {person,[65,108,105,99,101],30}
Numbers: [1,2,3,4,5]
Mixed: [1,hello,3.14,[116,101,120,116]]
--- Type Checks ---
is_integer(Age): true
is_float(Pi): true
is_atom(Status): true
is_boolean(IsActive): true
is_list(Numbers): true
is_tuple(Point): true
is_binary(BinGreeting): true
Output from variables_patterns.erl:
X = 42
X still matches 42
A: 1, B: hello, C: 3.14
Value: success
Second: I want this
Head: 1
Tail: [2,3,4,5]
First: 10, Second2: 20, Rest: [30,40]
Point X: 100, Point Y: 200
Matched error with reason: not found
Case matched ok: 42
Output from variables_advanced.erl:
User: #{active => true,age => 30,name => [65,108,105,99,101]}
Name: Alice, Age: 30
Email: none
Updated User: #{active => true,age => 31,name => [65,108,105,99,101]}
Extended User: #{active => true,age => 30,email => [97,108,105,99,101,64,101,120,97,109,112,108,101,46,99,111,109],name => [65,108,105,99,101]}
Matched Name: Alice, Age: 30
Timeout: 5000
--- Type Conversions ---
float(42): 42.0
trunc(3.7): 3
round(3.7): 4
integer_to_list(42): 42
list_to_integer("42"): 42
float_to_list(3.14): 3.14
list_to_float("3.14"): 3.14
atom_to_list(hello): hello
list_to_atom("hello"): hello
list_to_tuple([1,2,3]): {1,2,3}
tuple_to_list({1,2,3}): [1,2,3]
list_to_binary("hello"): <<"hello">>
binary_to_list(<<"hello">>): hello
Key Concepts
- Single assignment — Variables in Erlang can only be bound once per scope. Attempting to rebind a variable to a different value causes a runtime error. This eliminates an entire class of bugs related to unintended mutation.
= is pattern matching — The = operator matches the left side against the right side and binds any unbound variables. If a variable is already bound, it asserts equality instead of rebinding.- Atoms are lightweight constants — Atoms like
ok, error, true, and false are named constants stored efficiently in a global atom table. Booleans true and false are simply atoms. - Strings are character lists — Erlang strings (
"hello") are actually lists of integer character codes. For efficient string handling, use binaries (<<"hello">>). - Tagged tuples — The convention
{ok, Value} or {error, Reason} is ubiquitous in Erlang for return values, enabling clean pattern matching on success and failure cases. - Dynamic but strongly typed — Erlang won’t implicitly convert between types. Use explicit conversion functions like
integer_to_list/1, list_to_atom/1, or float/1. - Maps for key-value data — Maps (
#{key => value}) provide modern key-value storage with pattern matching support. Use => for creation and adding keys, := for updating existing keys and pattern matching. - Arbitrary precision integers — Erlang integers have no fixed size limit, making them suitable for large number arithmetic without overflow concerns.
Comments
Loading comments...
Leave a Comment