Schooner.Value (schooner v1.0.0)

Copy Markdown View Source

Term representation of Scheme values.

Where the BEAM already distinguishes types cleanly we use them directly: booleans, the empty list, cons cells, integers, floats, and strings (binaries) all live as their native Elixir form. Where types would otherwise alias — symbols vs strings vs bytevectors (all binaries), characters vs integers, vectors vs records vs closures (all tuples) — the colliding side is tagged.

Representations

  • Booleans — bare Elixir true / false. Only false is Scheme-falsy. Every other value is truthy, including :null and 0.
  • Empty list — bare Elixir []
  • Pair — Elixir cons cell `[carcdr]`. Improper Scheme pairs are
    improper Erlang lists (`[ab]whereb` is not a list).
  • Symbol — {:sym, binary} (binaries, not atoms; user-supplied identifiers must not exhaust the BEAM atom table)
  • String — bare Elixir binary. Disambiguated from symbol/bytevector because those remain tagged.
  • Character — {:char, codepoint}
  • Exact integer — bare Elixir integer
  • Exact rational — {:rational, numerator, denominator}. Always reduced (gcd(num, denom) == 1), denominator strictly greater than 1, sign carried on the numerator. A rational whose denominator would reduce to 1 is collapsed to its bare integer form by rational/2, so the integer/rational split is total — no Schooner value is both.
  • Inexact real — bare Elixir float
  • Non-finite inexact — {:float_special, :pos_inf | :neg_inf | :nan} (BEAM floats cannot represent these; tagged forms carry IEEE-754 semantics through arithmetic and comparison without smuggling forbidden bit patterns into Elixir floats.)

  • Complex — {:complex, real, imag} where each component is any non-complex number (integer, rational, float, or float_special). complex/2 collapses to the bare real component when imag === 0 so the real/complex split is total — any value with a complex tag has a non-zero (or inexact) imaginary part. Polar literals are converted to rectangular form on read.
  • Vector — {:vector, tuple}
  • Bytevector — {:bytevector, binary}
  • Closure — {:closure, params, body, env, name_or_nil}
  • Primitive — {:primitive, name, arity, fun}
  • Record — {:record, type_id, fields_tuple}
  • Record type identity — {:record_type, name, unique_int} — embedded as a literal in the bindings produced by define-record-type. Self-evaluating; compared with === so two definitions with the same record name in different lexical scopes produce distinct identities.
  • Error object — {:error_obj, kind, message, irritants} where kind is :user | :read | :file, message is a Scheme string value, and irritants is an Elixir list of Scheme values (rendered as a Scheme list by the accessor). Constructed by (error msg irritant ...) and reachable via error?, error-object?, read-error?, file-error?.
  • Parameter — {:parameter, id, init, converter}. id is a process-monotonic unique integer used as the lookup key in the per-process dynamic-binding stack. init is the post-converter initial value (returned when no parameterize is currently shadowing the parameter). converter is either nil or a Scheme procedure applied to every value installed for the parameter (initial or via parameterize). Parameters are procedure values: calling one with zero arguments returns the current value.
  • Foreign — {:foreign, term}. Opaque host payload: any Elixir term wrapped so Scheme code can pass it around (bind, return, stash in a pair/vector) but never inspect or forge it. Built by the host via foreign/1; the wrapped term is read back from Elixir with foreign_ref/1. Scheme code can ask (foreign? x) to discriminate, but has no constructor and no accessor, and write redacts the contents to #<foreign> — so the wrapped term remains structurally invisible from Scheme. Identity equality only: eq? / eqv? / equal? compare the wrapped terms with ===, so two foreigns that wrap structurally-equal- but-distinct host values are not equal.
  • EOF — :eof
  • Unspecified — :unspecified

Summary

Functions

Build a rectangular complex value. real and imag must be non-complex numbers (integers, rationals, floats, or :float_special sentinels). When imag is the exact integer 0 the result collapses to real, keeping the real / complex split total: a value tagged :complex always carries a non-zero (or inexact) imaginary part.

Scheme complex?. Schooner ships the full numeric tower from integers up through complex, so complex? and number? agree.

Human-readable rendering. Strings render without quotes and characters as their literal codepoint. Aggregates recurse using display-style rendering for their elements.

Wrap value as an already-forced (eager) promise. Forcing it returns value without invoking any procedure — the same shape as r7rs's (make-promise obj).

Scheme eq?. Identical to eqv? for Schooner — r7rs permits an implementation to collapse the two so long as the stronger discriminations eq? is allowed to draw (distinct heap copies of structurally-equal aggregates) are also drawn by eqv?. They are: see eqv?/2 below.

Scheme equal?. Recurses structurally into pairs, vectors, bytevectors, strings, and records. For everything else it falls through to eqv?.

Scheme eqv?. Atomic values (numbers, characters, symbols, booleans, (), :eof, :unspecified) compare by content, with exactness preserved for numbers — (eqv? 1 1.0) is #f. Aggregates (pairs, vectors, strings, bytevectors, records, procedures, promises, parameters, error objects) compare by physical identity via erts_debug.same/2 — two structurally-equal aggregates built independently are not eqv?. This is r7rs's "denote different locations in the store" semantics, recovered for an immutable value model by leaning on the BEAM's heap layout instead of explicit identity tags.

Wrap an arbitrary Elixir term as an opaque foreign Scheme value. Scheme code can bind, store, and pass the result through any value-shaped position (variables, pairs, vectors, closures) but cannot inspect, deconstruct, or forge it: there is no surface syntax that constructs a foreign and write redacts the wrapped term to #<foreign>. The host retrieves the wrapped term with foreign_ref/1.

Unwrap a foreign value, returning the host term it wraps. Raises ArgumentError for any non-foreign — this accessor is host-only and assumes the caller has already established the value is one.

Build an improper Scheme list. The last argument becomes the cdr of the innermost pair.

Wrap thunk (a zero-arg Scheme procedure) as a lazy promise. force applies the thunk and, if the result is itself a promise, keeps unwrapping iteratively. Schooner promises do not memoise — no mutation is available, so repeated forcing re-evaluates.

Build a Scheme list (proper, null-terminated) from an Elixir list.

Scheme list? — true for [] and proper (null-terminated) cons chains; false for improper lists, atoms, and anything that ends in a non-pair non-null.

Build a parameter value with a fresh id. init is the already-converted initial value. converter is either nil (when make-parameter was called without one) or a Scheme procedure applied to every value installed for the parameter via parameterize.

Build a normalised exact rational. Result is always reduced (numerator and denominator share no common factor), the sign is carried on the numerator, and a denominator of 1 collapses to a bare integer so integer-valued rationals are never tagged.

Scheme rational?. True for exact integers, exact rationals, and any finite inexact real (since every finite IEEE-754 double has an exact rational representation). False for the non-finite specials (+inf.0, -inf.0, +nan.0) and non-numeric values.

Scheme real?. Real numbers are anything outside the :complex tag — integers, rationals, finite floats, and the non-finite specials. A complex value whose imaginary part is exact zero collapses to its real part on construction, so a tagged complex always has a non-zero (or inexact) imaginary part and is therefore not real.

Convert a proper Scheme list (cons cells terminated by []) into an Elixir list. Raises ArgumentError on an improper list — the inverse of list/1.

Scheme truthiness: only false is false; every other value is truthy, including :null, 0, and the empty string.

Machine-readable rendering. Strings are wrapped in quotes, characters as #\name or #\x.., and unprintable characters are escaped.

Types

arity_spec()

@type arity_spec() ::
  non_neg_integer()
  | {:at_least, non_neg_integer()}
  | {:between, non_neg_integer(), non_neg_integer()}

bool_v()

@type bool_v() :: boolean()

bytevector_v()

@type bytevector_v() :: {:bytevector, binary()}

char_v()

@type char_v() :: {:char, non_neg_integer()}

closure_v()

@type closure_v() :: {:closure, term(), term(), term(), binary() | nil}

complex_v()

@type complex_v() :: {:complex, real_v(), real_v()}

error_kind()

@type error_kind() :: :user | :read | :file

error_obj_v()

@type error_obj_v() :: {:error_obj, error_kind(), t(), [t()]}

float_special_v()

@type float_special_v() :: {:float_special, :pos_inf | :neg_inf | :nan}

foreign_v()

@type foreign_v() :: {:foreign, term()}

pair_v()

@type pair_v() :: nonempty_maybe_improper_list(t(), t())

parameter_v()

@type parameter_v() :: {:parameter, integer(), t(), t() | nil}

primitive_v()

@type primitive_v() :: {:primitive, binary(), arity_spec(), (list() -> t())}

promise_v()

@type promise_v() :: {:promise, :forced, t()} | {:promise, :lazy, term()}

rational_v()

@type rational_v() :: {:rational, integer(), pos_integer()}

real_v()

@type real_v() :: integer() | rational_v() | float() | float_special_v()

record_type_id_v()

@type record_type_id_v() :: {:record_type, binary(), pos_integer()}

record_v()

@type record_v() :: {:record, term(), tuple()}

string_v()

@type string_v() :: binary()

sym_v()

@type sym_v() :: {:sym, binary()}

t()

@type t() ::
  bool_v()
  | []
  | pair_v()
  | sym_v()
  | string_v()
  | char_v()
  | integer()
  | rational_v()
  | float()
  | float_special_v()
  | complex_v()
  | vector_v()
  | bytevector_v()
  | closure_v()
  | primitive_v()
  | record_v()
  | record_type_id_v()
  | error_obj_v()
  | promise_v()
  | parameter_v()
  | foreign_v()
  | :eof
  | :unspecified

vector_v()

@type vector_v() :: {:vector, tuple()}

Functions

bool(bool)

@spec bool(boolean()) :: bool_v()

boolean?(arg1)

@spec boolean?(term()) :: boolean()

bytevector(bytes)

@spec bytevector([byte()] | binary()) :: bytevector_v()

bytevector?(arg1)

@spec bytevector?(term()) :: boolean()

char(cp)

@spec char(non_neg_integer()) :: char_v()

char?(arg1)

@spec char?(term()) :: boolean()

closure(params, body, env, name \\ nil)

@spec closure(term(), term(), term(), binary() | nil) :: closure_v()

complex(real, imag)

@spec complex(real_v(), real_v()) :: real_v() | complex_v()

Build a rectangular complex value. real and imag must be non-complex numbers (integers, rationals, floats, or :float_special sentinels). When imag is the exact integer 0 the result collapses to real, keeping the real / complex split total: a value tagged :complex always carries a non-zero (or inexact) imaginary part.

Inexact zeros (+0.0, -0.0) do not collapse — (make-rectangular 1.0 0.0) is 1.0+0.0i, distinct from 1.0. Per r7rs §6.2.6 only an exactly zero imaginary part makes the whole number real?.

complex?(v)

@spec complex?(term()) :: boolean()

Scheme complex?. Schooner ships the full numeric tower from integers up through complex, so complex? and number? agree.

display(value)

@spec display(t()) :: binary()

Human-readable rendering. Strings render without quotes and characters as their literal codepoint. Aggregates recurse using display-style rendering for their elements.

display_iodata(value)

@spec display_iodata(t()) :: iodata()

eager_promise(value)

@spec eager_promise(t()) :: promise_v()

Wrap value as an already-forced (eager) promise. Forcing it returns value without invoking any procedure — the same shape as r7rs's (make-promise obj).

eof?(arg1)

@spec eof?(term()) :: boolean()

eq?(a, b)

@spec eq?(t(), t()) :: boolean()

Scheme eq?. Identical to eqv? for Schooner — r7rs permits an implementation to collapse the two so long as the stronger discriminations eq? is allowed to draw (distinct heap copies of structurally-equal aggregates) are also drawn by eqv?. They are: see eqv?/2 below.

equal?(a, a)

@spec equal?(t(), t()) :: boolean()

Scheme equal?. Recurses structurally into pairs, vectors, bytevectors, strings, and records. For everything else it falls through to eqv?.

eqv?(a, b)

@spec eqv?(t(), t()) :: boolean()

Scheme eqv?. Atomic values (numbers, characters, symbols, booleans, (), :eof, :unspecified) compare by content, with exactness preserved for numbers — (eqv? 1 1.0) is #f. Aggregates (pairs, vectors, strings, bytevectors, records, procedures, promises, parameters, error objects) compare by physical identity via erts_debug.same/2 — two structurally-equal aggregates built independently are not eqv?. This is r7rs's "denote different locations in the store" semantics, recovered for an immutable value model by leaning on the BEAM's heap layout instead of explicit identity tags.

erts_debug.same/2 is an unofficial OTP BIF; it has been stable across many releases and is used by the Erlang/OTP test suite. Compiler / loader literal sharing means two source-level literals like '(1 2) written in the same module may be deduplicated and report same — r7rs explicitly permits constants to be shared, so this is allowed but worth pinning with a test if relevant to caller behaviour.

error_kind?(arg1, kind)

@spec error_kind?(term(), error_kind()) :: boolean()

error_object(kind, message, irritants)

@spec error_object(error_kind(), t(), [t()]) :: error_obj_v()

error_object?(arg1)

@spec error_object?(term()) :: boolean()

exact?(n)

@spec exact?(term()) :: boolean()

float_special?(arg1)

@spec float_special?(term()) :: boolean()

foreign(term)

@spec foreign(term()) :: foreign_v()

Wrap an arbitrary Elixir term as an opaque foreign Scheme value. Scheme code can bind, store, and pass the result through any value-shaped position (variables, pairs, vectors, closures) but cannot inspect, deconstruct, or forge it: there is no surface syntax that constructs a foreign and write redacts the wrapped term to #<foreign>. The host retrieves the wrapped term with foreign_ref/1.

foreign?(arg1)

@spec foreign?(term()) :: boolean()

foreign_ref(other)

@spec foreign_ref(foreign_v()) :: term()

Unwrap a foreign value, returning the host term it wraps. Raises ArgumentError for any non-foreign — this accessor is host-only and assumes the caller has already established the value is one.

improper_list(list, tail)

@spec improper_list([t(), ...], t()) :: t()

Build an improper Scheme list. The last argument becomes the cdr of the innermost pair.

inexact?(n)

@spec inexact?(term()) :: boolean()

integer?(n)

@spec integer?(term()) :: boolean()

lazy_promise(thunk)

@spec lazy_promise(term()) :: promise_v()

Wrap thunk (a zero-arg Scheme procedure) as a lazy promise. force applies the thunk and, if the result is itself a promise, keeps unwrapping iteratively. Schooner promises do not memoise — no mutation is available, so repeated forcing re-evaluates.

list(items)

@spec list([t()]) :: t()

Build a Scheme list (proper, null-terminated) from an Elixir list.

After the pair/null untagging this is the identity on Elixir lists. Kept as a named constructor so call sites stay self-documenting and the seam survives any future representation change.

list?(arg1)

@spec list?(term()) :: boolean()

Scheme list? — true for [] and proper (null-terminated) cons chains; false for improper lists, atoms, and anything that ends in a non-pair non-null.

null?(arg1)

@spec null?(term()) :: boolean()

number?(n)

@spec number?(term()) :: boolean()

pair(car, cdr)

@spec pair(t(), t()) :: pair_v()

pair?(arg1)

@spec pair?(term()) :: boolean()

parameter(init, converter)

@spec parameter(t(), t() | nil) :: parameter_v()

Build a parameter value with a fresh id. init is the already-converted initial value. converter is either nil (when make-parameter was called without one) or a Scheme procedure applied to every value installed for the parameter via parameterize.

parameter?(arg1)

@spec parameter?(term()) :: boolean()

primitive(name, arity, fun)

@spec primitive(binary(), arity_spec(), (list() -> t())) :: primitive_v()

procedure?(arg1)

@spec procedure?(term()) :: boolean()

promise?(arg1)

@spec promise?(term()) :: boolean()

rational(num, den)

@spec rational(integer(), integer()) :: integer() | rational_v()

Build a normalised exact rational. Result is always reduced (numerator and denominator share no common factor), the sign is carried on the numerator, and a denominator of 1 collapses to a bare integer so integer-valued rationals are never tagged.

Raises ArithmeticError on a zero denominator — callers in primitive paths should detect division-by-zero earlier and surface a structured Scheme-level error instead.

rational?(n)

@spec rational?(term()) :: boolean()

Scheme rational?. True for exact integers, exact rationals, and any finite inexact real (since every finite IEEE-754 double has an exact rational representation). False for the non-finite specials (+inf.0, -inf.0, +nan.0) and non-numeric values.

real?(v)

@spec real?(term()) :: boolean()

Scheme real?. Real numbers are anything outside the :complex tag — integers, rationals, finite floats, and the non-finite specials. A complex value whose imaginary part is exact zero collapses to its real part on construction, so a tagged complex always has a non-zero (or inexact) imaginary part and is therefore not real.

record(type_id, fields)

@spec record(term(), tuple()) :: record_v()

record?(arg1)

@spec record?(term()) :: boolean()

string(s)

@spec string(binary()) :: string_v()

string?(s)

@spec string?(term()) :: boolean()

symbol(name)

@spec symbol(binary()) :: sym_v()

symbol?(arg1)

@spec symbol?(term()) :: boolean()

to_list(other)

@spec to_list(t()) :: [t()]

Convert a proper Scheme list (cons cells terminated by []) into an Elixir list. Raises ArgumentError on an improper list — the inverse of list/1.

truthy?(arg1)

@spec truthy?(t()) :: boolean()

Scheme truthiness: only false is false; every other value is truthy, including :null, 0, and the empty string.

unspecified?(arg1)

@spec unspecified?(term()) :: boolean()

vector(items)

@spec vector([t()]) :: vector_v()

vector?(arg1)

@spec vector?(term()) :: boolean()

write(value)

@spec write(t()) :: binary()

Machine-readable rendering. Strings are wrapped in quotes, characters as #\name or #\x.., and unprintable characters are escaped.

write_iodata(value)

@spec write_iodata(t()) :: iodata()