Tempo.IntervalSet.Backend behaviour (Tempo v1.0.0)

Copy Markdown View Source

The behaviour a Tempo.IntervalSet storage backend implements.

An interval set's representation is pluggable: the set struct carries a :backend module and an opaque backend state (in its :intervals field), and every Tempo.IntervalSet function reaches the members through this contract rather than assuming a concrete data structure. Tempo.IntervalSet.Backend.List — a sorted plain list — is the default and the reference implementation.

The contract

The universal primitive is the ordered walk: walk/1 returns an enumerable that yields the member intervals in time order, sorted by from endpoint, disjoint, each half-open [from, to). A backend is correct as soon as it can walk; the remaining callbacks exist so a backend can answer common questions without a full walk, and default to walk-derived implementations via use Tempo.IntervalSet.Backend.

bounded?/1 is the honesty bit: a backend over a finite member list returns true; a lazy backend over a potentially unbounded generator returns false, and aggregate operations (to_list/1, count/1, set-wide duration) refuse rather than walk forever. Callers must consult bounded?/1 before calling to_list/1 or count/1.

Implementing a backend

defmodule MyBackend do
  use Tempo.IntervalSet.Backend

  @impl true
  def from_list(intervals, _options), do: build_my_state(intervals)

  @impl true
  def to_list(state), do: my_state_to_list(state)

  @impl true
  def walk(state), do: my_lazy_stream(state)

  @impl true
  def bounded?(_state), do: true
end

use provides overridable count/1, empty?/1, and first/1 derived from to_list/1 and walk/1; override them when the representation can answer faster (a tree knows its size; any backend can peek its first member without materialising).

Construct a set on a specific backend with Tempo.IntervalSet.new(intervals, backend: MyBackend). Set operations preserve the backend of member-preserving results; newly constructed extents default to the list backend.

Summary

Types

The backend's opaque representation of the member intervals. For the list backend this is the member list itself; other backends choose their own shape.

Callbacks

Whether the member list is finite. Aggregate operations consult this before materialising; false makes them refuse rather than hang.

The number of members. Only valid when bounded?/1 is true.

Whether the set has no members. Safe on every backend — peeking one element never requires a full walk.

The earliest member, or nil when empty. Safe on every backend.

Build backend state from a sorted, disjoint member list.

Candidate members whose extent may intersect the half-open UTC-second range [lo, hi).

The members as a plain list in time order. Only valid when bounded?/1 is true; an unbounded backend raises.

An enumerable yielding the members in time order. The universal primitive: safe on every backend, lazy where the backend is lazy.

Types

state()

@type state() :: term()

The backend's opaque representation of the member intervals. For the list backend this is the member list itself; other backends choose their own shape.

Callbacks

bounded?(state)

@callback bounded?(state()) :: boolean()

Whether the member list is finite. Aggregate operations consult this before materialising; false makes them refuse rather than hang.

count(state)

@callback count(state()) :: non_neg_integer()

The number of members. Only valid when bounded?/1 is true.

empty?(state)

@callback empty?(state()) :: boolean()

Whether the set has no members. Safe on every backend — peeking one element never requires a full walk.

first(state)

@callback first(state()) :: Tempo.Interval.t() | nil

The earliest member, or nil when empty. Safe on every backend.

from_list(list, keyword)

@callback from_list(
  [Tempo.Interval.t()],
  keyword()
) :: state()

Build backend state from a sorted, disjoint member list.

Tempo.IntervalSet.new/2 validates, sorts, and (by default) coalesces the members before calling this — the backend may assume time order and disjointness.

overlapping(state, tuple)

@callback overlapping(
  state(),
  {number(), number()}
) :: [Tempo.Interval.t()]

Candidate members whose extent may intersect the half-open UTC-second range [lo, hi).

A pruning callback, not a semantic one: the contract is to return at least every member whose [from, to) intersects the range — a backend may return extra members, or all of them (the default). The caller applies the exact resolution-aware check to the candidates, so a backend that prunes well (an interval tree) accelerates stabbing and overlap queries without owning their semantics.

to_list(state)

@callback to_list(state()) :: [Tempo.Interval.t()]

The members as a plain list in time order. Only valid when bounded?/1 is true; an unbounded backend raises.

walk(state)

@callback walk(state()) :: Enumerable.t()

An enumerable yielding the members in time order. The universal primitive: safe on every backend, lazy where the backend is lazy.