Skip to content

CONCEPT Cited by 1 source

Type-class-driven random generator

Definition

A type-class-driven random generator is the mechanism underlying ergonomic property-based testing: a uniform type-indexed function (Arbitrary, Random, Gen) that resolves to a per-type generator implementation via type-class / protocol / trait dispatch, so user code reads T.random (or arbitrary @T, gen::<T>()) and gets a valid randomised instance of T without naming the generator explicitly. User-defined types participate by providing a conformance — typically by delegating to the conformances of their fields.

The pattern originated with QuickCheck's Arbitrary type class (Haskell, Claessen & Hughes 2000). Every mainstream PBT library reimplements the same idea in its host language's dispatch mechanism.

The shape across languages

Language Dispatch mechanism Primitive User-type hook
Haskell type class arbitrary :: Gen a instance Arbitrary T
Swift protocol T.random extension T: Random
Rust (proptest) trait any::<T>() impl Arbitrary for T
Python (hypothesis) strategy registry from_type(T) @register_type_strategy(T, ...)
Java (jqwik) provider @ForAll T t @Provide ArbitraryT()
JS (fast-check) value factory fc.anything() (+ custom) fc.record({...})

All of these collapse to the same shape: one name at the call site, per-type resolution at compile / registry time.

Why dispatch matters

Without type-class / protocol dispatch, every test site would have to name its generator explicitly (generate_random_string(), generate_random_int(), generate_random_user_profile()). This is:

  • Verbose. Every call site carries the type name twice — in the generator function name and in the assignment target's type.
  • Non-composable. Nested types require manual threading — a User generator calls a Name generator calls a String generator. Each level is hand-written glue.
  • Refactor-hostile. Renaming UserProfileProfile requires renaming generate_random_user_profile() and every call site.

Type-class dispatch collapses all three: the compiler / runtime resolves .random (or arbitrary) per type, and generic Random instances automatically compose.

Swift worked example

Kandel's canonical worked example from Zalando's 2021 post:

struct LabelProps: Codable, Hashable {
    let text: String
    let backgroundColor: String?
    let font: FontProps
}

extension LabelProps: Random {
    public static var random: LabelProps {
        return LabelProps(
            text: .random,
            backgroundColor: .random,
            font: .random
        )
    }
}

Three things make this ergonomic:

  1. Swift's type inference resolves .random at each use site to the field's type. text: .random picks String.random; font: .random picks FontProps.random.
  2. The conformance delegates. LabelProps.random doesn't reach into String's internals — it calls String.random via protocol dispatch. This composes recursively without hand-threading.
  3. The uniform entry point at the test site is just LabelProps.random. No generateRandomLabelProps(), no arguments, no explicit seed. The generator surface is invisible.

(Source: sources/2021-02-01-zalando-stop-using-constants-feed-randomized-input-to-test-cases)

Build-time codegen as future work

Kandel flags but does not ship build-time synthesis of Random conformances, analogous to how Swift's compiler synthesises Equatable, Hashable, and Codable for structs whose fields are already conformant:

"We could do code generation on build phase to synthesize the Random conformance. Although this is out of scope of this post, its how Equatable conformance works."

The parallel is accurate: for a struct composed of already- conformant fields, the conformance body is purely mechanical (field-by-field delegation). Languages with macro or compiler-plugin support can eliminate the boilerplate.

  • Haskell solves this with deriving Arbitrary + generic-arbitrary.
  • Rust (proptest) offers #[derive(Arbitrary)].
  • Swift had no such mechanism at 2021; Swift Macros (2023+) would enable it.

This is a genuine gap between the Swift ecosystem and more mature PBT ecosystems: users still hand-write the Random conformance body. The library makes it cheap; it doesn't make it zero-cost.

Constrained types need constrained generators

Not every String is a valid Email. If a function takes a String that must be a valid URL or email, String.random will produce garbage 99% of the time. The library pattern answer is a newtype with its own generator:

struct Email { let value: String }  // wrapper
extension Email: Random {
    public static var random: Email {
        // always produce something that satisfies Email's contract
        return Email(value: "\(String.random)@\(String.random).com")
    }
}

The type system then prevents callers from passing an arbitrary String where Email is expected — constraining validity at the type boundary, not inside every test.

Kandel flags this explicitly as the preferred approach:

"Another is to create concrete type which conforms to Random."

This is a generalisation of the parse-don't-validate discipline into the testing layer: validity is a property of the type, not a property the generator hopes to produce.

What this concept is not

  • Not Math.random(). Type-class-driven generators are typed, compositional, and resolve by dispatch; Math.random() returns a primitive float with no type awareness.
  • Not a fuzzer. Fuzzers focus on coverage-guided mutation over byte inputs; type-class-driven generators produce structurally valid typed values and leave coverage to the host framework.
  • Not a shrinker. The generator produces inputs; shrinking is a separate concern (concepts/test-case-minimization) that mature PBT libraries pair with generators but smaller libraries (like Randomizer as described in 2021) may omit.

Seen in

Last updated · 476 distilled / 1,218 read