Lesson 6: Records and Complex Data Structures

Learn to define and work with records, nested data structures, and type-safe data modeling for scalable chat server architecture

Edit on GitHub

Records and Complex Data Structures

After learning list comprehensions for data transformation, we need structured ways to organize complex data. Records provide a powerful mechanism for creating named, typed data structures that make our chat server code more maintainable and type-safe.

Understanding Records

Records in Erlang are compile-time constructs that provide named access to tuple elements. This means records don’t exist at runtime. The compiler transforms record syntax into regular tuple operations. When you write User#user.name, Erlang converts this to element extraction from a tuple, making records both convenient and efficient. They’re perfect for representing entities like users, messages, and chat rooms in our application.

1> -record(user, {id, name, email, status = offline}).
2> User = #user{id=1, name="Alice", email="alice@example.com"}.
#user{id=1,name="Alice",email="alice@example.com",status=offline}
3> User#user.name.
"Alice"
4> User#user.status.
offline

Records must be defined before use, typically in module headers or separate header files.

Defining Records

Record definitions use the -record directive with field specifications:

1> -record(message, {
1> id,
1> user_id,
1> room_id,
1> content,
1> timestamp,
1> type = text
1> }).
2> Msg = #message{
2> id=101,
2> user_id=1,
2> room_id=5,
2> content="Hello everyone!",
2> timestamp={{2024,7,31},{14,30,0}}
2> }.
#message{id=101,user_id=1,room_id=5,content="Hello everyone!",
timestamp={{2024,7,31},{14,30,0}},type=text}

Fields can have default values, and omitted fields get the atom undefined.

Record Pattern Matching

Records excel in pattern matching, making data extraction clean and readable:

1> -record(user, {id, name, status}).
2> process_user(#user{name=Name, status=online}) ->
2> {active_user, Name};
2> process_user(#user{name=Name, status=offline}) ->
2> {inactive_user, Name}.
3> Alice = #user{id=1, name="Alice", status=online}.
#user{id=1,name="Alice",status=online}
4> process_user(Alice).
{active_user,"Alice"}

Updating Records

Records are immutable, but you can create new records with updated fields:

1> -record(user, {id, name, status, last_seen}).
2> User = #user{id=1, name="Bob", status=offline}.
#user{id=1,name="Bob",status=offline,last_seen=undefined}
3> OnlineUser = User#user{status=online, last_seen=now}.
#user{id=1,name="Bob",status=online,last_seen=now}
4> User.
#user{id=1,name="Bob",status=offline,last_seen=undefined}

The original record remains unchanged - we create a new one with modified fields.

Nested Data Structures

Records can contain other records, creating rich data models:

1> -record(address, {street, city, country}).
2> -record(user_profile, {id, name, email, address}).
3> Address = #address{
3> street="123 Main St",
3> city="Springfield",
3> country="USA"
3> }.
#address{street="123 Main St",city="Springfield",country="USA"}
4> Profile = #user_profile{
4> id=1,
4> name="Charlie",
4> email="charlie@example.com",
4> address=Address
4> }.
#user_profile{id=1,name="Charlie",email="charlie@example.com",
address=#address{street="123 Main St",city="Springfield",
country="USA"}}
5> Profile#user_profile.address#address.city.
"Springfield"

Working with Lists of Records

Combining records with list comprehensions creates powerful data processing:

1> -record(user, {id, name, status, age}).
2> Users = [
2> #user{id=1, name="Alice", status=online, age=25},
2> #user{id=2, name="Bob", status=offline, age=30},
2> #user{id=3, name="Carol", status=online, age=28}
2> ].
3> OnlineUsers = [U#user.name || U <- Users, U#user.status =:= online].
["Alice","Carol"]
4> AverageAge = lists:sum([U#user.age || U <- Users]) / length(Users).
27.666666666666668

Record Guards

Records work seamlessly with guards for robust pattern matching:

1> -record(message, {id, content, priority}).
2> classify_message(#message{priority=P}) when P > 8 ->
2> urgent;
2> classify_message(#message{priority=P}) when P > 5 ->
2> important;
2> classify_message(#message{}) ->
2> normal.
3> Msg1 = #message{id=1, content="Emergency!", priority=9}.
#message{id=1,content="Emergency!",priority=9}
4> classify_message(Msg1).
urgent

Chat Server Data Models

For our chat server, we can define comprehensive data structures:

1> -record(chat_room, {
1> id,
1> name,
1> description,
1> created_by,
1> created_at,
1> users = [],
1> is_private = false
1> }).
2> -record(chat_message, {
2> id,
2> room_id,
2> user_id,
2> content,
2> timestamp,
2> message_type = text,
2> metadata = []
2> }).
3> Room = #chat_room{
3> id=100,
3> name="General",
3> description="General discussion",
3> created_by=1,
3> users=[1,2,3]
3> }.
#chat_room{id=100,name="General",description="General discussion",
created_by=1,created_at=undefined,users=[1,2,3],
is_private=false}

Exercises

Exercise 1: User Management Create a user record with validation functions. Include fields for id, username, email, and join_date.

-record(user, {id, username, email, join_date}).
validate_user(#user{username=Username, email=Email})
when length(Username) > 0, length(Email) > 5 ->
valid;
validate_user(_) ->
invalid.

Exercise 2: Message Threading Design a record structure for threaded messages that can reference parent messages.

-record(thread_message, {
id,
parent_id,
content,
author,
timestamp,
replies = []
}).
add_reply(ParentMsg, ReplyMsg) ->
Replies = ParentMsg#thread_message.replies,
ParentMsg#thread_message{replies = [ReplyMsg|Replies]}.

Exercise 3: Complex Data Filtering Process a list of chat rooms to find rooms with specific criteria.

find_active_rooms(Rooms) ->
[R || R <- Rooms,
R#chat_room.is_private =:= false,
length(R#chat_room.users) > 2].

Key Takeaways

  • Records provide structure to complex data with named field access
  • Pattern matching with records enables clean data extraction and processing
  • Record updates create new instances while preserving immutability
  • Nested records support complex domain modeling
  • Guards and comprehensions work naturally with record patterns
  • Default values make record creation more flexible
  • Type safety improves through structured data access patterns
  • Chat server entities benefit from well-designed record schemas

Interactive Koans

🧘

Koans - Test Your Understanding

Fill in the blanks and press Enter or click Run to test your knowledge!

What atom is used as the default value for undefined record fields?

Erlang Shell
1> -record(person, {name, age}).
1> P = #person{name="Alice"}.
1> P#person.age.

What value should be assigned to create a user record with id=1 and name="Bob"?

Erlang Shell
1> -record(user, {id, name, status}).
1> User = ___.
πŸ’‘ Hint
Use record syntax with field assignments

What expression extracts the name field from a user record?

Erlang Shell
1> -record(user, {id, name, email}).
1> User = #user{id=1, name="Charlie", email="charlie@test.com"}.
1> Name = ___.
πŸ’‘ Hint
Use record.field syntax

What expression updates a user's status to online?

Erlang Shell
1> -record(user, {id, name, status}).
1> User = #user{id=1, name="Dave", status=offline}.
1> OnlineUser = ___.
πŸ’‘ Hint
Use record update syntax

What pattern matches users with online status?

Erlang Shell
1> get_active_users(Users) ->
1> [Name || #user{name=Name, status=___} <- Users].

What expression accesses the city from a nested address record?

Erlang Shell
1> -record(address, {street, city}).
1> -record(person, {name, address}).
1> P = #person{name="Eve", address=#address{street="Oak St", city="Boston"}}.
1> City = ___.
πŸ’‘ Hint
Chain record field access

What field should be used to filter users by age greater than 21?

Erlang Shell
1> -record(user, {name, age, status}).
1> Users = [#user{name="Frank", age=25}, #user{name="Grace", age=19}].
1> Adults = [U#user.name || U <- Users, U#user.___ > 21].

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