Sign in

    #Feat — Functional Enumeration of Algebraic Types

    A MoonBit port of the ideas from Feat: functional enumeration of algebraic types (Duregård, Jansson, Wang, 2012). Feat turns an algebraic type into a bijection between the non-negative integers and its values, so you can enumerate, index, and sample finite parts of arbitrarily large types without ever materializing the whole set.

    Size notion. In this README "size" means whatever the Enumerable instance chooses — usually one pay per constructor, but a user-defined instance can charge differently. The driver only relies on: each part is finite, parts are ordered by increasing size, and recursion is productive (guaranteed by pay).

    This package is a building block for MoonBit QuickCheck. It backs the "small check" mode (exhaustive testing for small sizes) and is independently useful whenever you want a deterministic, size-indexed view of a type.

    #Why enumeration?

    Random testing (QuickCheck) and exhaustive testing (SmallCheck) are two sides of the same coin:

    • Random is cheap, finds bugs lurking behind large inputs, but misses corner cases clustered near the "small" end of the space.
    • Exhaustive catches every small-input bug but blows up combinatorially.

    Feat's Enumerate[T] interleaves both: values are partitioned by a user-chosen notion of size, so you can pick out the i-th value overall or the j-th value at size k — deterministically, without running the generator from the start.

    flowchart LR T["Type T"] --> E["Enumerate[T]<br/>LazyList of Finite[T]"] E --> P0["part₀ : Finite[T]<br/>(size-0 values)"] E --> P1["part₁ : Finite[T]<br/>(size-1 values)"] E --> P2["part₂ : Finite[T]<br/>(size-2 values)"] E --> Pdots["…"] P2 --> Card["fCard : BigInt<br/>(count)"] P2 --> Idx["fIndex : BigInt → T<br/>(selector)"]

    #Install & Import

    feat lives inside moonbitlang/quickcheck. Add the main package and then import the sub-package in your moon.pkg.json:

    moon add moonbitlang/quickcheck

    { "import": [ { "path": "moonbitlang/quickcheck/feat", "alias": "feat" } ] }


    #The two core types

    #Finite[T] — a random-access indexed chunk

    A Finite[T] is a pair of a cardinality and an indexer. No values are stored; fIndex(i) computes the i-th element on demand.

    ///|
    pub(all) struct Finite[T] {
    fCard : BigInt
    fIndex : (BigInt) -> T
    }

    Most users do not construct Finites via helper functions. Instead, they usually get them out of Enumerate::eval() and inspect them with Finite::iter / Finite::to_array. If you do need a custom chunk, you can write one directly:

    ///|
    test "hand-rolled finite chunk" {
    let f : @feat.Finite[String] = {
    fCard: 2,
    fIndex: i => {
    if i == 0 {
    "left"
    } else {
    guard i == 1 else { abort("index out of bounds") }
    "right"
    }
    },
    }
    inspect(
    f.to_array(),
    content=(
    #|(2, @list.from_array(["left", "right"]))
    ),
    )
    }

    Finites are still composable once you have them. The most common case is to combine parts produced by enumerations:

    ///|
    test "disjoint union of two singleton parts" {
    let left = @feat.singleton("left").eval().head()
    let right = @feat.singleton("right").eval().head()
    let joined = left + right
    inspect(
    joined.to_array(),
    content=(
    #|(2, @list.from_array(["left", "right"]))
    ),
    )
    }

    Every Finite[T] is also iterable — for x in finite { ... } desugars to finite.iter(), which walks fIndex(0)..fIndex(fCard - 1) lazily. Use it whenever you want to stream a chunk's contents without materialising the full list via to_array:

    ///|
    test "for x in finite" {
    let acc : Array[BigInt] = []
    let finite : @feat.Finite[BigInt] = { fCard: 4, fIndex: i => i }
    for x in finite {
    acc.push(x)
    }
    assert_eq(acc, [0, 1, 2, 3])
    }

    #Enumerate[T] — a lazy list of Finite[T]

    An Enumerate[T] is a lazy stream of parts, where the k-th part contains all values of size k. Because the tail is lazy, infinite types (List, Tree, recursive enums…) are perfectly legal.

    ///|
    pub(all) struct Enumerate[T] {
    parts : LazyList[Finite[T]]
    }

    The pay combinator advances the size counter by one — it is the only way to consume "fuel" and the reason a recursive enumeration doesn't diverge:

    ///|
    test "singleton has size 0" {
    let e = @feat.singleton(42)
    let parts = e.eval()
    // The first (and only) part holds the single value.
    inspect(parts.head().to_array(), content="(1, @list.from_array([42]))")
    }

    ///|
    test "pay shifts everything one size up" {
    // Before pay: part₀ = {42}
    // After pay: part₀ = {}, part₁ = {42}
    let shifted = @feat.pay(() => @feat.singleton(42))
    let parts = shifted.eval()
    inspect(parts.head().to_array(), content="(0, @list.from_array([]))")
    inspect(parts.tail().head().to_array(), content="(1, @list.from_array([42]))")
    }


    #The Enumerable trait — deriving enumerations

    This is the main user-facing trait of the package. Implement Enumerable for a type T and you get: indexed access (enumerate()[i]), size-bounded sampling via feat_random, and a ready-to-use Gen[T] for the top-level QuickCheck driver. Everything else in this package either consumes or produces an Enumerable.

    #Signature

    ///|
    pub(open) trait Enumerable {
    enumerate() -> Enumerate[Self]
    }

    • pub(open) — anyone can add an instance for their own type.
    • The method is nullary: the enumeration of T depends only on T, not on any runtime state.
    • Return type Enumerate[Self] is the lazy list-of-parts value documented above — see Enumerate[T].

    #The contract

    An implementation must guarantee three properties so the driver can use it safely:

    #PropertyWhat breaks if you violate it
    1Cardinalities are non-negative. Every part has fCard : BigInt and must satisfy fCard >= 0. Parts with fCard == 0 are empty and collapse away.The package assumes every part behaves like a finite set; a negative card produced by hand breaks that model and will lead to wrong indexing behaviour.
    2Productivity under recursion. Every recursive self-reference inside an Enumerable instance must be guarded by a pay(...). An Enumerate[T] is a LazyList of parts, so it may be infinite — but each part must be reachable in finite time.Evaluating enumerate() blows the stack or loops forever.
    3Total indexing per part. For every i with 0 <= i < part.fCard, part.fIndex(i) must return a valid T.Indexing aborts with "index out of bounds".

    These are the same invariants that the provided combinators already preserve — so if you stick to singleton, union / +, product, unary, consts, and pay, you get them for free.

    #Built-in instances

    TypeShapeNotes
    Unitsingleton(())Size 0, card 1.
    Boolpay(true + false)Size 1, card 2 (inside pay).
    Byteflat Finite of card 256Size 0; indexes 0..255 directly.
    Charflat Finite of card 1,112,064Size 0; skips the UTF-16 surrogate range.
    Int / Int64interleaved 0, 1, -1, 2, -2, ...Size 0 through infinity; infinite parts each of card 1.
    UInt / UInt640, 1, 2, ... with pay per stepEach successor costs one unit of size.
    Option[E] where E : Enumerablepay(None + E::enumerate().fmap(Some))None at size 1, Some(x) at 1 + size(x).
    Result[T, E] where T, E : Enumerablepay(Err + Ok)Both arms cost one pay.
    List[E] where E : Enumerablepay(empty + Cons(e, lst))Each cons cell costs one pay.
    (A, B) where A, B : Enumerablepay(product(A::enumerate(), B::enumerate()))Size = 1 + sum of component sizes.

    Key observation: all non-primitive instances insert exactly one pay per constructor boundary. That's the rule you follow when writing your own impl.

    #Implementing Enumerable for your own type

    flowchart TD UserType["your recursive type T"] --> Constructors["one branch per constructor"] Constructors --> Leaf["Leaf constructor:<br/>singleton(value)"] Constructors --> Recursive["Recursive constructor:<br/>unary or product"] Leaf --> Union["+ (union)"] Recursive --> Union Union --> Pay["pay(() => …)"] Pay --> Result["Enumerate[T]"]

    Three rules, and that's it:

    1. One + summand per constructor. Leaf constructors become singleton(...); constructors that carry children use unary(pair => Cons(pair.0, pair.1)) or product(...).
    2. Wrap the whole thing in a pay(...). The pay is what gives the fixpoint a chance to suspend before recursing into T::enumerate() again — this is the productivity guarantee (contract item 2).
    3. Reach child enumerations through Enumerable::enumerate(), not by re-constructing them. That way the compiler's inference picks up user-defined instances and built-ins uniformly.

    A minimal recursive example (binary tree of Leaf | Node):

    ///|
    enum Tree {
    Leaf
    Node(Tree, Tree)
    }

    ///|
    impl @feat.Enumerable for Tree with enumerate() {
    // One size unit per constructor; the recursive children are reached via
    // `unary`, which goes through the built-in `Enumerable` instance for
    // `(Tree, Tree)`. That instance itself inserts a `pay`, which is what keeps
    // the fixpoint productive.
    @feat.pay(() => {
    @feat.singleton(Leaf) +
    @feat.unary((pair : (Tree, Tree)) => Node(pair.0, pair.1))
    })
    }

    ///|
    impl Show for Tree with output(self, logger) {
    match self {
    Leaf => logger.write_string("Leaf")
    Node(l, r) => {
    logger.write_string("Node(")
    l.output(logger)
    logger.write_string(", ")
    r.output(logger)
    logger.write_string(")")
    }
    }
    }

    ///|
    test "enumerate the first few binary trees" {
    let trees : @feat.Enumerate[Tree] = Enumerable::enumerate()
    inspect(trees[0], content="Leaf")
    inspect(trees[1], content="Node(Leaf, Leaf)")
    }

    #Common pitfalls

    PitfallWhy it hurtsFix
    Unguarded self-reference: T::enumerate() called without a surrounding pay.Recursion diverges (contract #2).Wrap the body in @feat.pay(() => ...). Going through unary + the pair instance achieves the same because the built-in (A, B) instance inserts its own pay.
    Computing a large Cartesian product with product before unioning.Not wrong, just slow — the resulting parts get large and indexing locality suffers.Prefer unary for a single-pair constructor, or consts([...]) for a disjunction — these keep the structure flat.
    Mixing eager List of Enumerate with consts at the top of a recursive definition.The List itself is eager: its elements are forced when the consts is reached, which can run into the recursion before pay kicks in.Ensure the consts(...) is inside pay, or use + between lazy Enumerates.
    Forgetting that zero-cardinality parts short-circuit.Empty parts are skipped cheaply, but a hand-rolled Finite with a bogus non-zero fCard will still be indexed.If you need an empty part, set fCard: 0 and use an aborting fIndex.

    #Where the trait is consumed

    • unary(f) requires the input type of f to be Enumerable.
    • Gen::feat_random(size) takes T : Enumerable and turns it into a Gen[T] by drawing uniformly from parts 0..=size.
    • The main moonbitlang/quickcheck driver bridges Enumerable into small-check-style exhaustive testing via the same feat_random pipeline.


    #Using an enumeration

    #Indexing

    Enumerate::at(i) (also written e[i]) is the "i-th value, overall" view. Sizes are walked in order: all size-0 values, then all size-1 values, etc.

    ///|
    test "index into Bool's enumeration" {
    // Enumerable::enumerate() for Bool yields [true, false] (inside pay).
    let e : @feat.Enumerate[Bool] = Enumerable::enumerate()
    inspect(e[0], content="true")
    inspect(e[1], content="false")
    }

    ///|
    test "index into a list enumeration" {
    let e : @feat.Enumerate[@list.List[Bool]] = Enumerable::enumerate()
    // Sizes grow as more cons cells are added.
    inspect(e[0], content="@list.from_array([])")
    inspect(e[1], content="@list.from_array([true])")
    inspect(e[2], content="@list.from_array([false])")
    }

    #Sampling the whole of size k

    eval() exposes the underlying LazyList[Finite[T]]. Combined with Finite::to_array, that lets you pull out every value at a specific size — the SmallCheck-style "show me everything of size ≤ k" pattern.

    ///|
    test "materialize every Bool at size 1" {
    let parts = (Enumerable::enumerate() : @feat.Enumerate[Bool]).eval()
    // part 0 is empty (Bool is defined with a pay).
    inspect(parts.head().to_array(), content="(0, @list.from_array([]))")
    // part 1 holds both booleans.
    inspect(
    parts.tail().head().to_array(),
    content="(2, @list.from_array([true, false]))",
    )
    }

    #Mapping and combining

    Enumerate[T] is a functor together with a disjoint-union + and a size-aware Cartesian product:

    OperationSignatureMeaning
    Enumerate::fmap(e, f)Enumerate[T] -> (T -> U) -> Enumerate[U]Re-label every element
    e1 + e2Enumerate[T] -> Enumerate[T] -> Enumerate[T]Interleave by size
    product(e1, e2)Enumerate[A] -> Enumerate[B] -> Enumerate[(A, B)]Pair every A with every B, still size-indexed
    pay(() => …)(() -> Enumerate[T]) -> Enumerate[T]Charge 1 unit of size
    unary(f)(T -> U) -> Enumerate[U] where T : EnumerableShortcut for T::enumerate().fmap(f)

    ///|
    test "fmap rewrites every element in place" {
    let bools : @feat.Enumerate[Bool] = Enumerable::enumerate()
    let labels = bools.fmap(b => if b { "yes" } else { "no" })
    assert_eq(labels[0], "yes")
    assert_eq(labels[1], "no")
    }

    ///|
    test "product generates every pair in part order" {
    let pairs = @feat.product(zero_or_one_part(), zero_or_one_part())
    inspect(pairs[0], content="(0, 0)")
    inspect(pairs[1], content="(0, 1)")
    inspect(pairs[2], content="(1, 0)")
    inspect(pairs[3], content="(1, 1)")
    }

    ///|
    fn zero_or_one_part() -> @feat.Enumerate[BigInt] {
    { parts: Cons({ fCard: 2, fIndex: i => i }, @lazy.LazyRef::from_value(Nil)) }
    }


    #When to reach for Feat vs. random QuickCheck

    SituationPrefer
    "Try every value up to size 10."Feat (indexing in a loop)
    "Find a counterexample in a space I can't enumerate in reasonable time."QuickCheck (random Arbitrary)
    "Deterministic, reproducible fuzz corpus across runs."Feat (size-indexed, no RNG)
    "I need shrinking to a small counterexample."QuickCheck + Shrink or falsify

    For large-scale property tests, Feat is also used to seed an initial corpus, which is then handed to the random driver.

    #API Reference (quick scan)

    #Values

    NameWhat it does
    empty()Enumerate[T] with no elements
    singleton(x)One-element enumeration at size 0
    pay(thunk)Shift every part one size up
    a + bInterleaved disjoint union of two enumerations
    product(a, b)Pair-up enumeration; size is the sum of component sizes
    consts(list)pay-wrapped union of a List of enumerations
    unary(f)T::enumerate().fmap(f) for T : Enumerable
    Finite::iter, Finite::to_arrayInspect a Finite[T] part once you have one

    #Types

    TypePurpose
    Finite[T]Cardinality + indexer (BigInt -> T); usually obtained from eval()
    Enumerate[T]LazyList[Finite[T]]; one element per size
    Enumerable traitenumerate() -> Enumerate[Self]

    #Further reading

    #License

    Apache-2.0.

    LazyList

    A call-by-need list: either empty (Nil) or a head value paired with a thunk producing the rest (Cons(head, LazyRef<tail>)). Because the tail is a LazyRef, infinite lists (repeat, infinite_stream) are representable and cheap — only the prefix you iterate is forced.

    Enumerable

    pub(open) trait Enumerable {
    enumerate() -> Enumerate[Self]
    }

    Types that can enumerate all of their values in size-indexed order.

    enumerate() returns an Enumerate[Self]: a lazy stream of parts where the k-th part contains every value of size k. Primitive types already implement this trait; algebraic composites (sums, products, recursive types) compose via singleton / + / product / unary wrapped in a single pay.

    See the package README for the full contract, the built-in instance table, and the recipe for implementing Enumerable on a user-defined recursive type.
    impl Enumerable for Unit
    impl Enumerable for Bool
    impl Enumerable for Byte
    impl Enumerable for Char
    impl Enumerable for Int
    impl Enumerable for Int64
    impl Enumerable for UInt
    impl Enumerable for Option[E]
    impl Enumerable for Result[T, E]
    impl Enumerable for Tuple2[A, B]

    Enumerate

    pub(all) struct Enumerate[T] {
    parts :
    LazyList
    [Finite[T]]
    }

    A lazy list of Finite[T] parts, one part per size class.

    Conceptually: Enumerate[T] partitions the type T by a user- chosen notion of "size" (usually one unit per constructor, charged via pay). Part k is a Finite[T] — a cardinality plus an indexer — covering every value of size k. The outer LazyList can be infinite: T = List[E] has parts at every size, no bound.

    Invariants (preserved by the provided combinators):
    • every part has fCard >= 0;
    • recursive references are guarded by pay so forcing the outer list is productive;
    • each part's fIndex is total on [0, fCard).
    impl Add for Enumerate[T]

    Enumerate::at

    #alias(en_index, deprecated="Use `_[_]` instead")
    #alias("_[_]")
    fn[T] Enumerate::at(self : Enumerate[T], idx :
    BigInt
    ) -> T

    Return the i-th value overall: walk parts from size 0 upward, subtracting each part's cardinality from idx until idx falls inside the current part, then read the value via fIndex.

    Aborts on out-of-range indices (reaches Nil).

    test {
    let bools : Enumerate[Bool] = Enumerable::enumerate()
    assert_eq(bools[0], true)
    assert_eq(bools[1], false)
    }

    Enumerate::eval

    Expose the underlying size-indexed parts. Callers typically use this together with Finite::to_array to materialise every value of a specific size.

    Enumerate::fmap

    fn[T, U] Enumerate::fmap(self : Enumerate[T], f : (T) -> U) -> Enumerate[U]

    Rewrite every element without changing the structure: part shapes stay the same, only the inhabitants are relabelled via f.

    test {
    let bools : Enumerate[Bool] = Enumerable::enumerate()
    let labels = bools.fmap(b => if b { "yes" } else { "no" })
    assert_eq(labels[0], "yes")
    assert_eq(labels[1], "no")
    }

    Enumerate::sample_finite

    fn[T] Enumerate::sample_finite(self : Enumerate[T], size : Int) -> Finite[T]

    Finite sampling domain used by Gen::feat_random. It aggregates the first size parts; if they are all empty, it continues to the next non-empty part so the result is still finite.

    Finite

    pub(all) struct Finite[T] {
    fCard :
    BigInt

    fIndex : (
    BigInt
    ) -> T
    }

    A finite chunk of T values, represented compactly as a cardinality plus an indexer rather than as a stored collection. No values are materialised until fIndex is called.

    Fields:
    • fCard: number of elements (≥ 0). Zero denotes the empty chunk.
    • fIndex: (i : BigInt) -> T, total on [0, fCard).

    Finite[T] is the basic building block used by Enumerate[T]: one part per size class. The combinators below give it a ring-like algebra (disjoint union, Cartesian product, maps, fmap).
    impl Add for Finite[T]
    impl Show for Finite[T]

    Finite::iter

    fn[T] Finite::iter(self : Finite[T]) -> Iter[T]

    Lazy traversal over the chunk: walks indices 0, 1, 2, …, fCard-1 and yields fIndex(i) at each step. Because MoonBit's for x in <expr> desugars to <expr>.iter(), this is what lets you use a Finite[T] directly with the loop sugar.

    The traversal is lazy: early break stops calling fIndex. Like every Iter in MoonBit it is single-shot.

    test {
    let squares : Finite[BigInt] = { fCard: 4, fIndex: j => j * j }
    assert_eq(squares.iter().collect(), [0, 1, 4, 9])
    }

    Finite::to_array

    Materialise a Finite to (cardinality, list of all elements). Eagerly evaluates every element — only suitable for small chunks.

    test {
    let f : Finite[BigInt] = { fCard: 3, fIndex: j => j }
    inspect(f.to_array(), content="(3, @list.from_array([0, 1, 2]))")
    }

    consts

    pay-wrapped union of a list of sub-enumerations. Typical use: write one Enumerate per constructor of a sum type, then combine with consts. The outer pay charges one size unit for the constructor tag.

    empty

    fn[T] empty() -> Enumerate[T]

    The empty enumeration: no values at any size. Identity for union.

    test {
    let e : Enumerate[Int] = empty()
    inspect(e.eval(), content="[]")
    }

    pay

    fn[T] pay(f : () -> Enumerate[T]) -> Enumerate[T]

    Shift every size class up by one: part k of pay(f) equals part k - 1 of f(). The outer part 0 becomes empty. This is the productivity knob for recursive Enumerable instances — every recursive self-reference must be wrapped in pay or the fixpoint diverges.

    product

    fn[T, U] product(e1 : Enumerate[T], e2 : Enumerate[U]) -> Enumerate[(T, U)]

    Cartesian product, keyed by sum of sizes: part k of product(a, b) contains every pair whose component sizes add to k. Order inside a part follows the diagonalisation convention (first by decreasing a-size, then by increasing b-size).

    singleton

    fn[T] singleton(val : T) -> Enumerate[T]

    One-value enumeration at size 0. Building block for Enumerable instances of leaf constructors.

    test {
    let e : Enumerate[String] = singleton("leaf")
    assert_eq(e.at(0), "leaf")
    }

    unary

    fn[T : Enumerable, U] unary(f : (T) -> U) -> Enumerate[U]

    Apply a unary constructor: unary(f) == T::enumerate().fmap(f). For multi-argument constructors, write a tuple lambda so f takes a single tuple argument — e.g. unary(p => Node(p.0, p.1)) — then unary(f) goes through the built-in pair Enumerable instance which inserts the required pay.