ExTLA.Function (ExTLA v0.6.0)

Copy Markdown View Source

TLA+ function operations. In TLA+, a function maps every element of its domain to a value. Elixir maps serve as the implementation.

"Total over its domain" is the property that matters, and it is why except/2 and except_with/3 raise on a point outside the domain rather than adding it: [f EXCEPT ![a] = v] where a ∉ DOMAIN f is undefined in TLA+, and quietly extending the domain would produce a state that no reachable spec behavior can produce — while still being fingerprinted as if it were one.

This module holds the only implementation of each of these operators. ExTLA.Spec.Helpers, which a spec body imports, delegates to it and defines nothing of its own, so a spec body and direct callers always get the same answer.

Summary

Functions

Construct a function: [x ∈ S ↦ expr(x)].

DOMAIN f — the domain of a function, as a set.

EXCEPT — update a function at specific points: [f EXCEPT ![a] = v, ![b] = w].

EXCEPT with self-reference — update using the old value: [f EXCEPT ![a] = @ + 1].

Function set: [S → T] — all total functions from S to T.

Record set: [field1: S1, field2: S2] — all records with fields drawn from the respective sets.

Functions

construct(domain, mapping)

Construct a function: [x ∈ S ↦ expr(x)].

iex> ExTLA.Function.construct(MapSet.new([1, 2]), &(&1 * 10))
%{1 => 10, 2 => 20}

S is a set — a MapSet or a range. A list is a sequence, which is a function on 1..Len(s) already, and building a function over one would silently deduplicate it; a map is a function already. Both are refused by name, the way ExTLA.Spec.Helpers.at/2 refuses a term that is neither. A spec body spells this operator for_each/2, which is the name the message uses.

domain(f)

DOMAIN f — the domain of a function, as a set.

iex> ExTLA.Function.domain(%{a: 1, b: 2})
MapSet.new([:a, :b])

A sequence is a function on 1..Len(s) — that is what ExTLA.Spec.Helpers.at/2 applies it as — so its domain is that range:

iex> ExTLA.Function.domain([:x, :y, :z])
MapSet.new([1, 2, 3])

Anything else is refused by name rather than answered. A MapSet is a struct and therefore a map, so taking its keys would answer a set with the struct's own field names — a wrong answer that looks like an answer.

except(f, updates)

EXCEPT — update a function at specific points: [f EXCEPT ![a] = v, ![b] = w].

Raises KeyError on a point outside the domain — see the module documentation.

iex> ExTLA.Function.except(%{a: 1, b: 2}, a: 9)
%{a: 9, b: 2}

except_with(f, key, update_fn)

EXCEPT with self-reference — update using the old value: [f EXCEPT ![a] = @ + 1].

iex> ExTLA.Function.except_with(%{a: 1}, :a, &(&1 + 1))
%{a: 2}

function_set(domain, range)

Function set: [S → T] — all total functions from S to T.

Warning: |T|^|S| elements. Use with small sets: three processes over four states is 64 functions, but five over four is already 1024.

iex> ExTLA.Function.function_set(MapSet.new([:a]), MapSet.new([0, 1]))
MapSet.new([%{a: 0}, %{a: 1}])

iex> ExTLA.Function.function_set(MapSet.new([]), MapSet.new([0, 1]))
MapSet.new([%{}])

record_set(field_specs)

Record set: [field1: S1, field2: S2] — all records with fields drawn from the respective sets.

iex> ExTLA.Function.record_set(pc: MapSet.new([:idle, :busy]), n: MapSet.new([0]))
MapSet.new([%{pc: :idle, n: 0}, %{pc: :busy, n: 0}])