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
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
ExUnit— Elixir documentation — the test framework.ExUnit.DocTest— Elixir documentation — tests from documentation examples.