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. Onlyfalseis Scheme-falsy. Every other value is truthy, including:nulland0. - Empty list — bare Elixir
[] Pair — Elixir cons cell `[car cdr]`. Improper Scheme pairs are improper Erlang lists (`[a b] 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 byrational/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/2collapses to the bare real component whenimag === 0so 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 bydefine-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}wherekindis:user | :read | :file,messageis a Scheme string value, andirritantsis an Elixir list of Scheme values (rendered as a Scheme list by the accessor). Constructed by(error msg irritant ...)and reachable viaerror?,error-object?,read-error?,file-error?. - Parameter —
{:parameter, id, init, converter}.idis a process-monotonic unique integer used as the lookup key in the per-process dynamic-binding stack.initis the post-converter initial value (returned when noparameterizeis currently shadowing the parameter).converteris eithernilor a Scheme procedure applied to every value installed for the parameter (initial or viaparameterize). 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 viaforeign/1; the wrapped term is read back from Elixir withforeign_ref/1. Scheme code can ask(foreign? x)to discriminate, but has no constructor and no accessor, andwriteredacts 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
@type arity_spec() :: non_neg_integer() | {:at_least, non_neg_integer()} | {:between, non_neg_integer(), non_neg_integer()}
@type bool_v() :: boolean()
@type bytevector_v() :: {:bytevector, binary()}
@type char_v() :: {:char, non_neg_integer()}
@type error_kind() :: :user | :read | :file
@type error_obj_v() :: {:error_obj, error_kind(), t(), [t()]}
@type float_special_v() :: {:float_special, :pos_inf | :neg_inf | :nan}
@type foreign_v() :: {:foreign, term()}
@type pair_v() :: nonempty_maybe_improper_list(t(), t())
@type primitive_v() :: {:primitive, binary(), arity_spec(), (list() -> t())}
@type rational_v() :: {:rational, integer(), pos_integer()}
@type real_v() :: integer() | rational_v() | float() | float_special_v()
@type record_type_id_v() :: {:record_type, binary(), pos_integer()}
@type string_v() :: binary()
@type sym_v() :: {:sym, binary()}
@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
@type vector_v() :: {:vector, tuple()}
Functions
@spec bytevector([byte()] | binary()) :: bytevector_v()
@spec char(non_neg_integer()) :: char_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?.
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.
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.
@spec error_kind?(term(), error_kind()) :: boolean()
@spec error_object(error_kind(), t(), [t()]) :: error_obj_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.
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.
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.
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.
@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.
@spec primitive(binary(), arity_spec(), (list() -> t())) :: primitive_v()
@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.
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.