Elixir basics · Lesson 23 (open contents)
Write down the data contract
Describe public input and output shapes with `@type` and `@spec`.
Run this first
Run it once, change one input, and compare the new result.
# Describe both result branches before running them
defmodule Positive do
@type result(a) :: {:ok, a} | {:error, :not_positive}
@spec check(integer()) :: result(integer())
def check(number) when number > 0, do: {:ok, number}
def check(_number), do: {:error, :not_positive}
end
{Positive.check(3), Positive.check(0)}- The result is
{{:ok, 3}, {:error, :not_positive}}.
Read from the first line down
- Read
@type result(a): Name a reusable tagged-result shape. - Read
@spec: Declare a function's input and output contract. - Read
::: Separate a name or expression from its type.
Symbols are not secret signs
@type result(a)Name a reusable tagged-result shape.
@specDeclare a function's input and output contract.
::Separate a name or expression from its type.
Match each name to its meaning
type alias
@type gives a readable name to a term shape.
function spec
@spec documents accepted arguments and possible return values.
Dialyzer
Dialyzer compares inferred success types with specs to find suspicious paths.
Why these forms are useful
Run the example first. Predict one result, then change one input and run it again.
@type gives a readable name to a term shape.
@spec documents accepted arguments and possible return values.
Why this shape?
Why write specs if the code can run without them?
A spec leaves a function contract for people and Dialyzer. It is good at finding calls that cannot succeed; it does not guard every runtime value.
A very broad spec says little, and an incorrect spec does not become runtime validation. Parse and validate data at system boundaries.
Coming from Java, Python, or JavaScript
Java
- Familiar starting point
- The compiler performs static type checks during a build.
- What BEAM changes
- Dialyzer uses success typing; its goal is not to prove that every input is safe.
- False friend
- A clean Dialyzer run cannot prevent a bad runtime message.
Python
- Familiar starting point
- Type hints checked by mypy or pyright.
- What BEAM changes
- Specs also serve tools and readers, but the analysis model differs.
- False friend
- A spec is not an input-validation library.
JavaScript
- Familiar starting point
- TypeScript checks types before emitting JavaScript.
- What BEAM changes
- BEAM still runs terms; Dialyzer looks for contradictions among calls that can succeed.
- False friend
- Files and network boundaries still need real validation.
Close the answer and try
Write a spec for a function that accepts a string and returns its length.
@spec length_of(____) :: ____
def length_of(text), do: String.length(text)Target result: Use String.t() and non_neg_integer().
Stuck? Read one hint
A length cannot be negative.
After you run it, see one answer
@spec length_of(String.t()) :: non_neg_integer()
def length_of(text), do: String.length(text)Think about it: Does a typespec validate data at runtime?
No. Specs document and support static analysis; runtime checks still need code.
Remember these three lines
- 1
Specs make API shapes visible.
- 2
Dialyzer finds some impossible or inconsistent paths.
- 3
Specs do not replace tests or validation.