ExSystolic.PE behaviour (ex_systolic v0.2.0)

Copy Markdown View Source

Behaviour that every processing element must implement.

A PE is a pure state machine: given its current state and a map of named inputs, it produces a new state and a map of named outputs. No side effects, no mutation, no global state.

Design rationale

Using a behaviour (rather than a callback-on-module-attribute scheme) gives the compiler a chance to warn about missing callbacks and makes the contract explicit in the type system. Each PE module is a self-contained unit that can be tested in complete isolation.

The two callbacks

  • init/1 constructs the initial PE state from keyword options.
  • step/4 advances the PE by one tick, returning updated state and outputs.

Determinism contract (HARD)

PE callbacks must be pure. Determinism is the foundation of the whole library. In particular, the following are forbidden inside init/1 and step/4:

Violating these rules will silently break determinism and trace reproducibility. If a PE needs randomness, take a seed via init/1 options and use :rand.uniform_s/1 against an explicit state passed through PE state.

Reserved input value

The atom :empty is reserved to denote "no value present" on an input port (typically because the corresponding link buffer was empty this tick). PEs must treat :empty as "absent input"; do not return :empty as a meaningful payload. See ExSystolic.PE.value/2 for a helper that coerces :empty/nil to a default value.

Summary

Callbacks

Initializes PE state from the given options.

Executes one tick of the PE.

Functions

Returns true when an input value is "present" (neither nil nor :empty).

Coerces an absent or empty input to a default value.

Types

context()

@type context() :: %{required(atom()) => term()}

inputs()

@type inputs() :: %{required(atom()) => term() | :empty}

outputs()

@type outputs() :: %{required(atom()) => term()}

state()

@type state() :: term()

Callbacks

init(opts)

@callback init(opts :: keyword()) :: state()

Initializes PE state from the given options.

step(state, inputs, tick, context)

@callback step(state(), inputs(), tick :: non_neg_integer(), context()) ::
  {state(), outputs()}

Executes one tick of the PE.

  • state -- current PE state
  • inputs -- map of port_name => value received this tick
  • tick -- current tick number (0-based)
  • context -- additional context (e.g. coordinate)

Returns {new_state, outputs} where outputs is a map of port_name => value to send this tick.

Functions

present?(arg1)

@spec present?(term() | :empty | nil) :: boolean()

Returns true when an input value is "present" (neither nil nor :empty).

Examples

iex> ExSystolic.PE.present?(:empty)
false

iex> ExSystolic.PE.present?(nil)
false

iex> ExSystolic.PE.present?(0)
true

iex> ExSystolic.PE.present?(false)
true

value(value, default)

@spec value(term() | :empty | nil, term()) :: term()

Coerces an absent or empty input to a default value.

Returns default when value is either nil (port missing from the inputs map) or :empty (the reserved empty-buffer marker). Otherwise returns value unchanged.

Examples

iex> ExSystolic.PE.value(:empty, 0)
0

iex> ExSystolic.PE.value(nil, 0)
0

iex> ExSystolic.PE.value(7, 0)
7

iex> ExSystolic.PE.value(0, 99)
0