Elixir basics · Lesson 23 (open contents)
01 · Let the code speak02 · Meet six kinds of values03 · Put values together04 · Build a new list05 · Make the shapes match06 · Choose with case07 · Parse a number safely08 · Unpack &1 and the pipe09 · Put code in a project10 · Ask precise true-or-false questions11 · Read and update nested data12 · Count visible text and bytes13 · Let function clauses choose14 · Solve a list twice15 · Choose the smallest clear control form16 · Give domain data a shape17 · Put one tested module in Mix18 · Make success and failure predictable19 · Read and write one real file20 · Take only the data you need21 · Give different data one shared action22 · Write a callback contract23 · Write down the data contract24 · Test behavior and boundaries25 · Turn a project into a command26 · Write the project promise27 · Parse one real line28 · Keep the public API small29 · Make the promise executable30 · Package the edge and leave clues31 · Prove the project is done
ELIXIR · INTERMEDIATE · LESSON 2325 minutes

Write down the data contract

Describe public input and output shapes with `@type` and `@spec`.

01 · Start with the whole example

Run this first

Run it once, change one input, and compare the new result.

Elixir
# 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)}
Check the result first
  • The result is {{:ok, 3}, {:error, :not_positive}}.
02 · Take the code apart

Read from the first line down

  1. Read @type result(a): Name a reusable tagged-result shape.
  2. Read @spec: Declare a function's input and output contract.
  3. Read ::: Separate a name or expression from its type.
03 · Meet the new symbols

Symbols are not secret signs

@type result(a)

Name a reusable tagged-result shape.

@spec

Declare a function's input and output contract.

::

Separate a name or expression from its type.

04 · Ideas inside the code

Match each name to its meaning

01

type alias

@type gives a readable name to a term shape.

02

function spec

@spec documents accepted arguments and possible return values.

03

Dialyzer

Dialyzer compares inferred success types with specs to find suspicious paths.

05 · Make the idea clear

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.

DESIGN DECISION

Why this shape?

Why write specs if the code can run without them?

Choice

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.

Cost and boundary

A very broad spec says little, and an incorrect spec does not become runtime validation. Parse and validate data at system boundaries.

A FAMILIAR POINT OF VIEW

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.
06 · Change it yourself

Close the answer and try

Write a spec for a function that accepts a string and returns its length.

Practice starting point
@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
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.

Take with you

Remember these three lines

  1. 1

    Specs make API shapes visible.

  2. 2

    Dialyzer finds some impossible or inconsistent paths.

  3. 3

    Specs do not replace tests or validation.

Lesson completeRun the code once, then mark this lesson as done.