Lesson 2: Pattern Matching - Erlang's Superpower
Master Erlang's most powerful feature - pattern matching - to elegantly handle data structures and build robust chat server components
Pattern Matching - Erlangโs Superpower
Pattern matching is what makes Erlang code so elegant and powerful. Instead of checking conditions with if statements, you describe the shape of data you expect and let Erlang do the matching for you.
Simple Pattern Matching Examples
Letโs start with the most basic examples to understand how pattern matching works:
% Start the shell and try these:1> MyList = [1, 2, 3].[1,2,3]
2> [A, B, C] = MyList.[1,2,3]
3> A.1
4> B.2
5> C.3
% Use underscore for values you don't care about6> [_, _, Z] = MyList.[1,2,3]
7> Z.3
What happened?
[A, B, C] = MyList
matches the pattern[A, B, C]
against[1, 2, 3]
- Erlang binds
A = 1
,B = 2
,C = 3
_
means โI donโt care about this valueโ
Pattern Matching in Function Heads
This is where pattern matching becomes really powerful:
-module(list_processor).-export([first_element/1, list_length/1, describe_list/1]).
% Get first element of a listfirst_element([Head | _]) -> Head;first_element([]) -> empty_list.
% Calculate list length recursivelylist_length([]) -> 0;list_length([_ | Tail]) -> 1 + list_length(Tail).
% Describe what kind of list we havedescribe_list([]) -> "Empty list";describe_list([_]) -> "Single item";describe_list([_, _]) -> "Two items";describe_list([_, _, _]) -> "Three items";describe_list(_) -> "Many items".
Key patterns:
[Head | Tail]
- Splits list into first element and rest[_]
- Matches list with exactly one element[]
- Matches empty list
Test It Out
1> c(list_processor).{ok,list_processor}
2> list_processor:first_element([1, 2, 3]).1
3> list_processor:list_length([a, b, c, d]).4
4> list_processor:describe_list([])."Empty list"
5> list_processor:describe_list([1, 2])."Two items"
Tuple Pattern Matching
Tuples are perfect for grouping related data:
-module(tuple_demo).-export([process_result/1, get_user_info/1, coordinate_distance/2]).
% Handle different result typesprocess_result({ok, Data}) -> io:format("Success: ~p~n", [Data]);process_result({error, Reason}) -> io:format("Error: ~p~n", [Reason]);process_result({warning, Message}) -> io:format("Warning: ~p~n", [Message]).
% Extract user informationget_user_info({user, Name, Age, Status}) -> io:format("User ~s is ~p years old and ~p~n", [Name, Age, Status]).
% Calculate distance between two pointscoordinate_distance({X1, Y1}, {X2, Y2}) -> DX = X2 - X1, DY = Y2 - Y1, math:sqrt(DX * DX + DY * DY).
Test Tuple Matching
1> c(tuple_demo).{ok,tuple_demo}
2> tuple_demo:process_result({ok, "Message sent"}).Success: "Message sent"ok
3> tuple_demo:get_user_info({user, "Alice", 25, online}).User Alice is 25 years old and onlineok
4> tuple_demo:coordinate_distance({0, 0}, {3, 4}).5.0
Building a Chat Command Parser
Letโs apply pattern matching to build a practical command parser for our chat server:
-module(chat_commands).-export([parse_command/1, handle_result/1]).
% Parse different chat commands using pattern matchingparse_command(<<"/join ", Room/binary>>) when byte_size(Room) > 0 -> {join_room, Room};parse_command(<<"/leave">>) -> leave_room;parse_command(<<"/users">>) -> list_users;parse_command(<<"/help">>) -> show_help;parse_command(<<"/nick ", NewName/binary>>) when byte_size(NewName) > 0 -> {change_nickname, NewName};parse_command(Message) when byte_size(Message) > 0 -> {chat_message, Message};parse_command(_) -> invalid_command.
% Handle different result typeshandle_result({ok, Value}) -> io:format("Success: ~p~n", [Value]);handle_result({error, Reason}) -> io:format("Error: ~p~n", [Reason]);handle_result({join_room, Room}) -> io:format("User wants to join room: ~s~n", [Room]);handle_result(leave_room) -> io:format("User wants to leave current room~n");handle_result({change_nickname, Name}) -> io:format("User wants to change name to: ~s~n", [Name]);handle_result(_) -> io:format("Unknown result~n").
Whatโs happening:
- Binary pattern matching extracts parts of binary strings
- Guards (
when
) add extra conditions - Different clauses handle different input patterns
- Underscore (
_
) matches anything we donโt care about
Test the Parser
1> c(chat_commands).{ok,chat_commands}
2> chat_commands:parse_command(<<"/join general">>).{join_room,<<"general">>}
3> chat_commands:parse_command(<<"Hello everyone!">>).{chat_message,<<"Hello everyone!">>}
4> chat_commands:handle_result({join_room, <<"general">>}).User wants to join room: generalok
Advanced List Patterns
Letโs explore more complex list patterns:
-module(list_patterns).-export([analyze_list/1, process_pairs/1, find_pattern/1]).
% Analyze different list structuresanalyze_list([]) -> empty;analyze_list([X]) -> {single, X};analyze_list([X, Y]) -> {pair, X, Y};analyze_list([X, Y, Z]) -> {triple, X, Y, Z};analyze_list([First | Rest]) -> {list, First, length(Rest)}.
% Process list of pairsprocess_pairs([]) -> [];process_pairs([{Key, Value} | Rest]) -> io:format("~p: ~p~n", [Key, Value]), process_pairs(Rest).
% Find specific patternsfind_pattern([1, 2, 3 | _]) -> "Starts with 1, 2, 3";find_pattern([X, X | _]) -> "First two elements are the same";find_pattern([X, Y | _]) when X > Y -> "First element is larger than second";find_pattern(_) -> "No special pattern found".
Case Expressions
Sometimes you need pattern matching inside a function:
-module(message_processor).-export([process_message/1, validate_user/1]).
process_message(Message) -> case chat_commands:parse_command(Message) of {join_room, Room} -> io:format("Joining room: ~s~n", [Room]), {ok, joined}; {chat_message, Text} -> io:format("Broadcasting: ~s~n", [Text]), {ok, sent}; leave_room -> io:format("Leaving current room~n"), {ok, left}; invalid_command -> {error, "Invalid command"} end.
validate_user(UserData) -> case UserData of {user, Name, Age} when is_binary(Name), is_integer(Age), Age > 0 -> {ok, valid_user}; {user, _, Age} when Age =< 0 -> {error, invalid_age}; {user, Name, _} when not is_binary(Name) -> {error, invalid_name}; _ -> {error, invalid_format} end.
Exercises - Practice Pattern Matching!
Exercise 1: List Operations
Create a module list_ops.erl
:
-module(list_ops).-export([second_element/1, last_two/1, remove_first/1]).
% Get second element of listsecond_element([_, Second | _]) -> {ok, Second};second_element(_) -> {error, not_enough_elements}.
% Get last two elementslast_two([A, B]) -> {ok, [A, B]};last_two([_ | Tail]) -> last_two(Tail);last_two(_) -> {error, not_enough_elements}.
% Remove first elementremove_first([_ | Tail]) -> Tail;remove_first([]) -> [].
Exercise 2: Message Validator
Create a validator for chat messages:
-module(message_validator).-export([validate_message/1]).
validate_message({message, User, Text, _Timestamp}) when is_binary(User), is_binary(Text), byte_size(User) > 0, byte_size(Text) > 0, byte_size(Text) =< 500 -> {ok, valid};validate_message({message, _User, Text, _}) when byte_size(Text) > 500 -> {error, message_too_long};validate_message({message, User, _Text, _}) when byte_size(User) =:= 0 -> {error, empty_username};validate_message(_) -> {error, invalid_format}.
Exercise 3: Advanced Pattern Matching
Create a number sequence analyzer:
-module(sequence_analyzer).-export([analyze_sequence/1]).
analyze_sequence([]) -> empty;analyze_sequence([X]) -> {single, X};analyze_sequence([X, X | _]) -> {starts_with_duplicate, X};analyze_sequence([X, Y | _]) when Y =:= X + 1 -> {ascending_sequence, X, Y};analyze_sequence([X, Y | _]) when Y =:= X - 1 -> {descending_sequence, X, Y};analyze_sequence([X, Y | _]) -> {different_numbers, X, Y}.
Key Takeaways
- Pattern matching describes the shape of data you expect
- Function heads use patterns to handle different cases
- Lists can be matched with
[Head | Tail]
or specific patterns - Tuples group related data:
{ok, Value}
,{error, Reason}
- Guards (
when
) add extra conditions to patterns - Case expressions allow pattern matching inside functions
- Underscore (
_
) matches values you donโt care about
Whatโs Next?
In Lesson 3, weโll explore recursion and higher-order functions to process data efficiently:
- Recursive functions for list processing
- Map, filter, and fold operations
- Building a message history system
- Performance considerations
Pattern matching combined with recursion makes Erlang incredibly powerful for data processing!
Test Your Understanding
Before moving on, make sure you can:
- Match simple patterns like
[A, B, C] = [1, 2, 3]
- Use pattern matching in function heads
- Extract data from tuples like
{ok, Value}
- Handle different list patterns (
[]
,[Head | Tail]
) - Use guards with
when
to add conditions - Write case expressions for pattern matching
If any of these feel unclear, review the examples and try the exercises again. Pattern matching is the foundation of elegant Erlang code!
Koans - Test Your Understanding
Fill in the blanks and press Enter or click Run to test your knowledge!
What value does A hold?
What list makes Second = 20?
What value does Third hold?
What pattern separates head from tail?
What tuple matches this pattern?
What is the value of Name?
This is part of the โLearn Erlang Step-By-Stepโ tutorial series. Each lesson builds on the previous ones, so make sure you complete the exercises before moving forward.
Finished this lesson?
Mark it as complete to track your progress
This open source tutorial is brought to you by Pennypack Software - we build reliable software systems.
Found an issue? Edit this page on GitHub or open an issue