Beginner

Variables and Types in Erlang

Learn about variables, data types, and type conversions in Erlang with practical Docker-ready examples

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.

Running Today

All examples can be run using Docker:

docker pull erlang:alpine
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining