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[]and0. - 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 and compared with===; the unique integer keeps two definitions with the same record name in different lexical scopes distinct. - 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 unique integer (:erlang.unique_integer([:monotonic])) 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.eq?/eqv?/equal?compare the wrapped terms with===, so foreigns wrapping the same pid or reference are equal, as are foreigns wrapping structurally equal terms. - 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?. Implemented as eqv?/2, which r7rs permits; eqv?
already distinguishes separately built aggregates by identity.
Scheme equal?. Recurses structurally into pairs, vectors, and
records, and compares bytevectors and strings by content. Any other
two values are equal only if they are the same term (===); NaN is
never equal to anything.
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 and non-list values.
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; the inverse of list/1. Raises ArgumentError on
an improper list.
Scheme truthiness: only false is false; every other value is
truthy, including [], 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?. Implemented as eqv?/2, which r7rs permits; eqv?
already distinguishes separately built aggregates by identity.
Scheme equal?. Recurses structurally into pairs, vectors, and
records, and compares bytevectors and strings by content. Any other
two values are equal only if they are the same term (===); NaN is
never equal to anything.
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.
This is the identity on Elixir lists. It is kept as a named constructor so call sites state their intent and a future representation change has one place to land.
Scheme list? — true for [] and proper (null-terminated) cons
chains; false for improper lists and non-list values.
@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; the inverse of list/1. Raises ArgumentError on
an improper list.
Scheme truthiness: only false is false; every other value is
truthy, including [], 0, and the empty string.
Machine-readable rendering. Strings are wrapped in quotes, characters as
#\name or #\x.., and unprintable characters are escaped.