Structural capabilities
Structural capabilities are the standard library’s reusable operations for values whose shape can be inspected. They power equality assertions, ordered collections, hash-based algorithms, and diagnostic output without making every datatype implement the same code by hand.
The four public interfaces are:
IEqual, whose method isequal(left, right) -> Bool;ICompare, whose method iscompare(left, right) -> Int;IHash, whose method ishash(value) -> Int;IDebug, whose method isto_repr(value) -> Debug.Repr.
Prelude re-exports these interfaces and the four named structural helpers:
| Public Prelude helper | Result |
|---|---|
Prelude.structural_equal(left, right) |
Bool |
Prelude.structural_compare(left, right) |
-1, 0, or 1 |
Prelude.structural_hash(value) |
signed Int hash |
Prelude.structural_debug_repr(value) |
lazy Debug.Repr |
These are public functions in the Prelude module. A qualified call makes the
API boundary explicit; a direct call is useful when the concrete type is known.
Generic code normally asks for an interface provider instead; that keeps the
relationship between the value and its capability explicit. The underlying
Equal, Compare, and Hash modules are standard-library implementation
modules; application code should use the Prelude helpers.
Use the operations directly
Section titled “Use the operations directly”The generated operations work recursively for ordinary structural values. This small example compares the fields of two records, orders them, hashes one, and renders it for a diagnostic:
func main() -> Unit do first = {x: 2, y: 3}; second = {x: 2, y: 4}; equal = Prelude.structural_equal(first, second); order = Prelude.structural_compare(first, second); digest = Prelude.structural_hash(first); rendered = Debug.to_debug_string(first); Console.println(if equal then "equal" else "different" end); Debug.trace(order); Debug.trace(digest); Console.println(rendered);endstructural_compare returns only the sign of the ordering. Records and tuples
are compared lexicographically in canonical field order, so the first field
that differs determines the result. The same field order is used for hashing.
Ask for a capability in generic code
Section titled “Ask for a capability in generic code”An unconstrained type parameter does not automatically have equality, ordering, hashing, or debugging operations. Add a contextual extension question when a generic function needs one:
func same[T]( left : T, right : T, ?{extension EqualForT: IEqual for T},) -> Bool do EqualForT.equal(left, right)end
func main() -> Unit do point = {x: 4, y: 9}; Debug.trace(same(point, {x: 4, y: 9}));endThe EqualForT binding is an interface dictionary selected at the call site.
The standard library’s StructuralEqual[T] provider can satisfy this question
when the selected type is structurally supported. The same pattern works with
ICompare, IHash, and IDebug; see Question parameters
for the syntax and Interfaces and extensions
for provider resolution.
What is structural?
Section titled “What is structural?”The generated operations recurse through these representations:
Unit,Bool,Int,String,Char, andDoubleuse their primitive semantics. Hashing follows the standard library’s deterministic rules, including UTF-16 string contents and the binary representation ofDouble.- Records and tuples visit fields in canonical order.
- Immutable
Array[T]values compare length and then elements in index order. Their standard-library provider asks for the corresponding capability forT. - Ordinary datatypes compare constructor identity first and then payloads in declaration order. Named payloads retain their field labels in debug output.
- Error-type constructors use their stable constructor tags and payloads, including constructors inherited into a derived error set.
- The standard
Option[T]andResult[T, E]views delegate to these same structural operations, recursively requiring capabilities for their payload types.
For equality, equal values always have equal structural hashes. Structural
hashes are deterministic for structural values. Values that use runtime
identity, such as functions and @identity datatypes, have identity-based
hashes that are not stable across process executions.
Here is a datatype whose constructor payloads are compared recursively:
datatype Tree Leaf(Int) Branch(Tree, Tree)end
func main() -> Unit do left = Tree::Branch(Tree::Leaf(1), Tree::Leaf(2)); right = Tree::Branch(Tree::Leaf(1), Tree::Leaf(3)); Debug.trace(Prelude.structural_equal(left, right)); Debug.trace(Prelude.structural_compare(left, right)); Debug.trace(Debug.to_debug_string(left));endConstructor order is declaration order. In this example Leaf precedes
Branch, and two Branch values are compared by their first payload before
their second payload.
Debug.Repr is a diagnostic view
Section titled “Debug.Repr is a diagnostic view”Prelude.structural_debug_repr returns a lazy Debug.Repr, not a String.
Debug.to_debug_string renders it, and Debug.trace renders and prints it.
The representation keeps container bodies as indexed callbacks so a renderer
does not have to materialize every element before it knows how much output is
needed.
The standard renderer is deliberately bounded: it expands at most 64 elements
from one container and stops descending after depth 8. Omitted elements and
deep recursive values are shown as .... Strings and characters are quoted.
This makes debug output useful for failures and logs, but it is not a stable
serialization format and should not replace a domain-specific to_string or
JSON encoder.
datatype Event Event{name: String, code: Int}end
func main() -> Unit do event = Event::Event{name: "ready", code: 200}; Debug.trace(event); Console.println(Debug.to_debug_string([event, event]));endProviders and generated implementations
Section titled “Providers and generated implementations”StructuralEqual, StructuralCompare, StructuralHash, and
StructuralDebug are ordinary associated extensions. The interfaces associate
these providers with their own interface identities, which lets a suitable
provider be considered for a contextual question or method call. They are not
a magical universal fallback: an unrelated interface or an unassociated
extension does not become available merely because its module is visible.
When a concrete type reaches a structural entry point, the compiler generates
and caches a specialized implementation for that type. A generated
implementation can contain calls to the same operation for nested fields, so a
record containing an Option[Array[Int]] produces a recursive capability
requirement for each layer. The generated function is an implementation detail;
it is not a first-class function value and can only be called directly.
The standard library also provides explicit primitive and collection
extensions. For example, Array[T] uses an IEqual for T, ICompare for T,
IHash for T, and IDebug for T question to implement its own provider. This
is why a generic array algorithm can remain type-safe without assuming that
every possible T supports every operation.
Identity, opaque, and unsupported values
Section titled “Identity, opaque, and unsupported values”Some values intentionally do not expose their representation:
- A function is compared, ordered, and hashed by runtime identity. Its debug
representation is opaque (
<function>). - A datatype marked
@identityis an indivisible reference identity: equality, comparison, and hashing compare the value itself rather than its payload. Debug output is a separate capability, so its representation still renders the datatype’s owner-visible payload fields structurally, like any other visible datatype. Only an opaque@identitydatatype falls back to an explicitIDebugprovider, following the ordinary opaque-datatype visibility rules. MutArray[T]is identity-based and is shown as an opaqueMutArrayvalue; use an immutable snapshot when a value should participate in structural collection operations.- An opaque datatype imported from another module cannot be structurally traversed by consumers. Its defining module must provide the public capability extensions that callers are meant to use.
- Interface dictionaries and other unsupported native values have no structural equality, comparison, hashing, or debug fallback in this language version. A concrete specialization that needs one is diagnosed rather than silently converted.
Use an explicit domain operation when identity is the intended meaning. Use a public extension when an opaque type should expose a stable comparison, serialization, or diagnostic policy without exposing its internal fields.
The Prelude and modules page lists the capability aliases,
and Output, debugging, and tests shows
how Debug and Testing.assert_equal consume IEqual and IDebug.
Next: Advanced pattern matching.