alux-shape 0.1.0

Data shapes as a first-order program, independent of any encoder
Documentation

alux-shape

alux-shape describes the shape of data independently of any encoder.

The crate is a specification. It carries no interpreter and depends only on alux-ext: a shape is described here as first-order syntax over two sorts, and an interpretation gives that description reality — a declaration in another language, a judgement about a value, a writer of bytes, a memory layout. None of those is the type of the thing described; each is what the term means to that interpreter.

use alux_shape::{FieldAlg, ShapeAlg, ShapeExt, Words};

const USER: Words<'static> = &["user"];

/// A shape is a term, polymorphic in the algebra that interprets it.
fn user<A>(alg: &A) -> A::Ty
where
    A: ShapeAlg + FieldAlg,
{
    let id = alg.field(&["id"], alg.int(false, 64));
    let display_name = alg.field(&["display", "name"], alg.text());
    let checksum = alg.field(&["checksum"], alg.bytes_hex(Some(32)));

    alg.named_product(USER, vec![id, display_name, checksum])
}

A name is a sequence of words

[display, name] is a name. displayName, display_name, and DisplayName are three spellings of it, and which one appears is settled where the term is interpreted. Holding words rather than a cased string is what makes that exact: converting between cases is heuristic wherever acronyms or digits meet a boundary, while joining words is not, so the same term spells identically in every host because none of them re-segments anything.

ShapeAlg::named states an identity, not an instruction. An interpretation that declares types emits one and refers to it; one that types structurally may spell the shape at each use.

Two ways in

A type that already exists states its term through #[derive(Shape)], re-exported here and implemented by alux-shape-macros: it reads the serde attributes already present, so the shape and the serialization are two readings of one annotation. A shape with no layout behind it is declared over the algebra instead.

Either way the term is the same, which is what lets a type move between them: fold both readings through an interpretation and compare.

What it owns, and what it does not

It owns eighteen primitives and the operations they derive. Every tagged encoding of a choice is a choice of products over a name-valued discriminant, so sum_external, sum_internal, sum_adjacent, sum_untagged, and sum_of_names are stated once in an extension rather than being primitives an interpretation could disagree about.

It owns no spelling, no encoder, and no layout. Whether a member is read by its name or by its position is not stated in a term either, and cannot be: a member is a lens, and which coordinate addresses it is settled when a shape is composed with a transport.

Seeing one in a language

A term says nothing about how a host writes it, so what a shape looks like in a language is an interpretation of the term rather than a property of it. Two exist for languages people read:

  • alux-shape-rust reads a term as a Rust layout, struct declarations with the serde attributes that make the encoding agree with the shape, and a written form for each leaf so Hex<[u8; 32]> stays distinct from the bytes it writes.
  • alux-shape-typescript reads the same term as TypeScript declarations, where a product is an interface, a merge is an intersection, and an optional member is a union with null.

The same value folds through both, so a type stated once is available in either language without being written in either. Four more interpretations exist for other purposes: alux-shape-text for a readable description, alux-shape-json for a decision about a JSON value, alux-shape-term for the term itself, and alux-shape-morph for a shape stated in terms of another.

And you can write your own. An interpretation is two carrier types and the primitives over them, Sorts, ShapeAlg, and FieldAlg. Everything derived comes with them: the five encodings of a choice are stated once in an extension, so an interpretation that never mentions them still has them. Nothing in this crate learns about a new language, which is the point of the term being first-order.