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
Usergenerator calls aNamegenerator calls aStringgenerator. Each level is hand-written glue. - Refactor-hostile. Renaming
UserProfile→Profilerequires renaminggenerate_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:
- Swift's type inference resolves
.randomat each use site to the field's type.text: .randompicksString.random;font: .randompicksFontProps.random. - The conformance delegates.
LabelProps.randomdoesn't reach intoString's internals — it callsString.randomvia protocol dispatch. This composes recursively without hand-threading. - The uniform entry point at the test site is just
LabelProps.random. NogenerateRandomLabelProps(), 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
Equatableconformance 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¶
- sources/2021-02-01-zalando-stop-using-constants-feed-randomized-input-to-test-cases
— canonical Swift implementer disclosure. Kandel's
Randomizer library provides
Randomprotocol + Standard Library conformances + an extension point for user-defined types; no build-time codegen.
Related¶
- patterns/property-based-testing — the pattern type-class-driven generators enable.
- concepts/example-based-test-constant-input-antipattern — what the generator dispatch fixes.
- systems/randomizer-swift — Zalando's Swift implementation.
- concepts/test-case-minimization — the shrinker pairs with the generator.
- patterns/seed-recorded-failure-reproducibility — seed replay pairs with the generator for deterministic failure reproduction.