jonnifyelixir
F5 / F5.02 / structs

F5.02 · part 1 of 3

Structs & typespecs

An entity is a plain struct, and a good struct definition is three declarations working together. @enforce_keys lists the fields the entity cannot exist without; defstruct declares every field and gives the optional ones a default; and a @type t documents the shape so the compiler and Dialyzer can check it. The result is data with a fixed, known set of keys, where a malformed entity raises at construction rather than failing somewhere downstream. The branded-id fields carry their namespace, so a wrong reference is visible on sight.

The three parts of an Enrollment struct, and what each one guarantees. Select a part to see its job.

Three declarations

The three lines are not redundant. Enforced keys decide what must be present; the struct definition decides what fields exist at all and their defaults; the type decides what each field may hold. Together they make an illegal entity hard to build by accident. Select each to see what it enforces.

A struct definition · select a part

REQUIRED FIELDS @enforce_keys [:id, :user_id, :course_id] FIELDS + DEFAULTS defstruct [:id, :user_id, :course_id, enrolled_at: nil, progress: 0] THE SHAPE, CHECKED @type t :: %__MODULE__{progress: non_neg_integer(), ...}

declaration: @enforce_keys

guarantees: required fields

A struct is a map with a known shape. The three declarations decide what is present, what exists, and what each field may hold — so a broken entity fails loudly at the point it is built.

In code

Here is a full entity. Building one with all enforced keys succeeds; omitting one raises a KeyError at compile-checked construction; and the typespec gives Dialyzer enough to flag a field set to the wrong type. The id fields are branded strings, so their namespace makes the reference’s meaning obvious.

defmodule Portal.Learning.Enrollment do
  @enforce_keys [:id, :user_id, :course_id]
  defstruct [:id, :user_id, :course_id, enrolled_at: nil, progress: 0]

  @type t :: %__MODULE__{
          id: String.t(),            # "ENR0Nb..."
          user_id: String.t(),       # "USR0Nb..."  -> Accounts
          course_id: String.t(),     # "CRS0Nb..."  -> Catalog
          enrolled_at: DateTime.t() | nil,
          progress: non_neg_integer()
        }
end

# build one — all enforced keys present
%Enrollment{id: "ENR0Nb1", user_id: "USR0Nb2", course_id: "CRS0Nb3"}

%Enrollment{id: "ENR0Nb1"}   # ** (KeyError) the following keys must also be given: [:user_id, :course_id]

an entity

A thing the Portal tracks — an enrollment joining a learner to a course.

a typed struct

Enforced keys, defaults, and a @type t — data with a checked shape.

Next: bounded contexts — how these structs are grouped into modules that own them.

References

Primary sources for this lesson, and where it connects in the course.

Sources

Related in this course