Current section
Files
Jump to
Current section
Files
src/kaos.erl
-module(kaos).
-moduledoc """
Kaos is a small, composable generator library for Erlang. It provides building
blocks (generators) to produce random but controlled data and execution
functions to realize those generators deterministically.
## Core Ideas
- Everything you build is a `gen()` value. Use combinators such as
`const/1`, `integer/2`, `float/2`, `boolean/0`, `choose/1`, `weighted/1`,
`list_of/2`, `tuple_of/1`, `map_of/3`, `dict_of/3`, `set_of/2`,
`gb_tree_of/3`, `gb_set_of/2`, `string_of/2`, `binary_of/2`,
`bitstring_of/2`, `shuffle/1`, and composition helpers `map/2`,
`filter/2`, `flatmap/2`, `recurse/1`, `cycle/1`, `iterate/2`.
- Deterministic generation: `generate/3` and `generate/4` (with timeout) use
a seed so the same seed and generator produce the same samples.
- Streaming-friendly: `generate_into/5` and `generate_into/6` fold each
sample into a caller-supplied accumulator, avoiding materializing large
intermediate lists when you do not need them.
## Execution APIs
- `{ok, Samples} = generate(Gen, Seed, Count)` returns a list of values.
- `{ok, Acc} = generate_into(Gen, Seed, Count, MergeFun, InitAcc)` returns a
folded accumulator; the `/6` variant lets you pass a custom timeout. Both
run generation in a linked worker and return `{error, Reason}` tuples on
failure or `{error, timeout}` if the worker exceeds the deadline.
## Typical Use Cases
- Property testing and fuzzing: build diverse, reproducible inputs for unit
or property tests without committing to a particular testing framework.
- Test fixtures and sample content: quickly synthesize realistic maps,
lists, strings, binaries or trees for demos and benchmarks.
- Distribution shaping: pick values with `weighted/1` to approximate
real-world frequency mixes in load tests.
- Large-output workflows: use `generate_into/5` to stream into sets or maps,
running statistics, top-K structures, iolists, files, ETS, or external
sinks without building a huge list in memory.
- Recursive/structured data: `recurse/1` and `iterate/2` help construct nested
terms and controlled growth processes.
## Notes
- Deterministic seeding
- Uses `rand:seed(exsss, Seed)`. Same OTP version + same `Seed` + same `Gen`
yield the same samples. Across OTP versions, streams may differ.
- Worker + timeout semantics
- Generation runs in a linked worker process with a default 30s timeout.
Use `generate/4` or `generate_into/6` to increase the timeout for slow
merges, narrow domains (lots of resampling), or deeper recursion.
- Memory vs streaming
- `generate/3` materializes a list of `Count` elements in memory. Prefer
`generate_into/5` to stream into sets/maps/files when `Count` is large.
- Resampling for uniqueness
- Set and map/tree generators skip duplicates and resample. If the element or
key domain is too narrow for the requested size, generation can take a long
time or time out. Widen the domain or reduce the size.
- Error shapes
- Many constructors validate inputs and will fail with
`{error, {badarg, "..."}, _}` (or a related error), for example:
- Size: `"Size generator must provide an integer >= 0"`
- Byte: `"Byte generator must provide an integer between 0 and 255"`
- Bit: `"Bit generator must produce 0 or 1"`
- String codepoints: `unicode:characters_to_binary/1` may raise `badarg`
- Side effects run in the worker
- `map/2`, `flatmap/2`, `filter/2`, and merge functions execute in the worker
process. Ensure any side effects or external resources are safe to use
from that process.
- Process-local state isolation
- `cycle/1`, `iterate/2`, and `recurse/1` keep state in the process
dictionary. Each `generate*` call uses its own worker, so runs are
isolated and concurrent calls do not interfere.
- Weighted and uniform selection
- `choose/1` selects uniformly among generators. `weighted/1` uses integer
weights normalized by their GCD and selects proportionally; small samples
can deviate from long-run ratios.
## Guides
See more step-by-step examples in the Examples guide: [Examples](examples.html)
## Examples
### Hex Color Codes (#RRGGBB)
```erlang
1> Byte = kaos:byte().
2> Hex2 = kaos:map(fun(B) -> io_lib:format("~2.16.0B", [B]) end, Byte).
3> Color = kaos:map(fun({R,G,B}) -> lists:flatten([$#, R, G, B]) end,
3> kaos:tuple_of([Hex2, Hex2, Hex2]) ).
4> Gen = Color.
5> {ok, Colors} = kaos:generate(Gen, 909, 3).
6> Colors.
["#2AE5E8","#2CC2EA","#F54938"]
```
### Dates (Month Day, Year)
```erlang
1> Year = kaos:integer(2020, 2027).
2> Month = kaos:integer(1, 12).
3> Base = kaos:tuple_of([Year, Month]).
4> Gen = kaos:flatmap(
4> fun({Y,M}) ->
4> {Name,Days} = case M of
4> 1 -> {"January",31}; 2 -> {"February",28}; 3 -> {"March",31};
4> 4 -> {"April",30}; 5 -> {"May",31}; 6 -> {"June",30};
4> 7 -> {"July",31}; 8 -> {"August",31}; 9 -> {"September",30};
4> 10 -> {"October",31}; 11 -> {"November",30}; 12 -> {"December",31}
4> end,
4> Leap = ((Y rem 400) =:= 0) orelse (((Y rem 4) =:= 0) andalso ((Y rem 100) =/= 0)),
4> Max = case {M,Leap} of {2,true} -> 29; _ -> Days end,
4> kaos:map(fun(D) -> lists:flatten(io_lib:format("~s ~B, ~B", [Name, D, Y])) end,
4> kaos:integer(1, Max))
4> end,
4> Base).
5> {ok, Dates} = kaos:generate(Gen, 909, 3).
6> Dates.
["June 15, 2022","November 15, 2024","February 1, 2025"]
```
### URLs
```erlang
1> Label = kaos:string_of(kaos:integer(3, 8), kaos:integer($a, $z)).
2> TLD = kaos:choose([kaos:const("com"), kaos:const("edu"), kaos:const("net"), kaos:const("io")]).
3> Host = kaos:tuple_of([Label, Label, TLD]).
4> Path = kaos:list_of(kaos:integer(1, 4), Label).
5> Scheme = kaos:choose([kaos:const("http"), kaos:const("https")]).
6> Base = kaos:tuple_of([Scheme, Host, Path]).
7> Gen = kaos:map(fun({Sc,{A,B,Tld},Segs}) ->
7> lists:flatten(io_lib:format("~s://~s.~s.~s/~s",
7> [Sc,
7> binary_to_list(A), binary_to_list(B), Tld,
7> string:join([binary_to_list(S) || S <- Segs], "/")]))
7> end,
7> Base).
8> {ok, Urls} = kaos:generate(Gen, 909, 3).
9> Urls.
["http://swqclnwb.eaeaokov.edu/cia/zzwhj/sie/gbx",
"http://cozh.cikojsk.io/pqrbz/piqcu/qjf/uroqkra",
"http://qaf.ngdusuwj.net/teeswqx"]
```
### Email Address
```erlang
1> Label = kaos:string_of(kaos:integer(3, 10), kaos:integer($a, $z)).
2> Local = Label.
3> Dom = kaos:tuple_of([Label, Label]).
4> TLD = kaos:choose([kaos:const("com"), kaos:const("edu"), kaos:const("net"), kaos:const("io")]).
5> Base = kaos:tuple_of([Local, Dom, TLD]).
6> Gen = kaos:map(fun({L,{D1,D2},Tld}) ->
6> lists:flatten(io_lib:format("~s@~s.~s.~s",
6> [binary_to_list(L), binary_to_list(D1), binary_to_list(D2), Tld]))
6> end,
6> Base).
7> {ok, Emails} = kaos:generate(Gen, 909, 3).
8> Emails.
["dswqc@nwbleaea.kovnjqcia.net","zwhjgsie@gbxqb.ozhec.net",
"ojskrrc@qrbzipiq.uaqjf.net"]
```
### JWTs
```erlang
1> Label = kaos:string_of(kaos:integer(3, 10), kaos:integer($a, $z)).
2> Iat = kaos:integer(1700000000, 1800000000).
3> Base = kaos:tuple_of([Label, Label, Iat]). % {sub, aud, iat}
4> B64URL = fun(Bin) ->
4> Str = base64:encode_to_string(Bin),
4> S1 = string:replace(Str, "+", "-", all),
4> S2 = string:replace(S1, "/", "_", all),
4> string:replace(S2, "=", "", all)
4> end.
5> Gen = kaos:map(fun({Sub, Aud, I}) ->
5> HeaderBin = list_to_binary("{\"alg\":\"HS256\",\"typ\":\"JWT\"}"),
5> PayloadBin = iolist_to_binary(
5> io_lib:format("{\"sub\":\"~s\",\"aud\":\"~s\",\"iat\":~b}",
5> [binary_to_list(Sub), binary_to_list(Aud), I])),
5> H64 = B64URL(HeaderBin),
5> P64 = B64URL(PayloadBin),
5> Data = H64 ++ "." ++ P64,
5> Sig = crypto:mac(hmac, sha256, "secret", Data),
5> S64 = B64URL(Sig),
5> lists:flatten(io_lib:format("~s.~s.~s", [H64, P64, S64]))
5> end,
5> Base).
6> {ok, Tokens} = kaos:generate(Gen, 909, 3).
7> Tokens.
["eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkc3dxYyIsImF1ZCI6Im53YmxlYWVhIiwiaWF0IjoxNzk2NDM1NzkzfQ.byAv0IXjYP-DOv4dbxZT6D2CUv2kGoqcu8ezHaQ3Brs",
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJvdm4iLCJhdWQiOiJxY2lhd3p6d2hqIiwiaWF0IjoxNzUwOTIyNTQxfQ.7LSHE_fPVWLCk7d4bCUWCWO4qrXazrmTpIF28PRgYfM",
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJpZWdnYnhxIiwiYXVkIjoiY296aCIsImlhdCI6MTc1NDY0NDg3MX0.vIzHpm3o1NdDmRj8FXKDP8TY2raG2Dcf1vheFmzo9lk"]
```
### Phone Numbers (US Format)
```erlang
1> Dn = kaos:integer(2, 9). % leading digit N=2..9
2> Dx = kaos:integer(0, 9). % other digits 0..9
3> Area = kaos:tuple_of([Dn, Dx, Dx]).
4> Exch = kaos:tuple_of([Dn, Dx, Dx]).
5> Line = kaos:tuple_of([Dx, Dx, Dx, Dx]).
6> Base = kaos:tuple_of([Area, Exch, Line]).
7> Gen = kaos:map(fun({{A1,A2,A3},{E1,E2,E3},{L1,L2,L3,L4}}) ->
7> lists:flatten(io_lib:format("(~B~B~B) ~B~B~B-~B~B~B~B",
7> [A1,A2,A3,E1,E2,E3,L1,L2,L3,L4]))
7> end,
7> Base).
8> {ok, Phones} = kaos:generate(Gen, 909, 3).
9> Phones.
["(414) 664-3345","(748) 468-0251","(980) 608-5343"]
```
""".
-export([
all/1,
array_of/2,
binary_of/2,
bit/0,
bitstring_of/2,
boolean/0,
byte/0,
choose/1,
choose_from_list/1,
const/1,
cycle/1,
dict_of/3,
filter/2,
flatmap/2,
float/2,
gb_set_of/2,
gb_tree_of/3,
integer/2,
iterate/2,
list_of/2,
map/2,
map_of/3,
orddict_of/3,
ordset_of/2,
recurse/1,
set_of/2,
shuffle/1,
string_of/2,
tuple_of/1,
weighted/1,
weighted_from_list/1,
weighted_from_range/2,
generate/3,
generate/4,
generate_into/5,
generate_into/6
]).
-export_type([
depth_function/0,
flatmap_function/0,
gen/0,
generate_error_response/0,
generate_response/0,
generate_into_function/1,
generate_into_response/1,
iterate_function/0,
map_function/0,
predicate_function/0,
weighted_gen/0
]).
-record(map_trait, {
new :: fun(() -> term()),
size :: fun((term()) -> non_neg_integer()),
is_key :: fun((term(), term()) -> boolean()),
put :: fun((term(), term(), term()) -> term())
}).
-record(set_trait, {
new :: fun(() -> term()),
size :: fun((term()) -> non_neg_integer()),
is_member :: fun((term(), term()) -> boolean()),
add :: fun((term(), term()) -> term())
}).
-doc """
List type requiring at least two values of type `T`. Commonly used as
`nonunary_list(gen())` for generator lists in `all/1`, `choose/1`, `cycle/1`,
and `weighted/1`.
""".
-type nonunary_list(T) :: [T | nonempty_list(T)].
-doc """
Function type used by `recurse/1`. Receives a depth counter (starting at 0) and
must return a `gen()` to continue generation at that depth.
""".
-type depth_function() :: fun((non_neg_integer()) -> gen()).
-doc """
Function type used by `flatmap/2`. Transforms a sampled value into another
`gen()` which is then sampled (monadic bind).
""".
-type flatmap_function() :: fun((term()) -> gen()).
-doc """
Error return shapes produced by generation functions on failures (bad arguments,
timeouts, or exceptions in user functions).
""".
-type generate_error_response() :: {error, {badarg, string()}, erlang:stacktrace()}
| {error, timeout}
| {error, term(), erlang:stacktrace()}
| {error, term()}.
-doc """
Success or error shape returned by `generate/3,4`. On success, contains the list
of samples; otherwise one of the error variants.
""".
-type generate_response() :: {ok, list(term())} | generate_error_response().
-doc """
Merge/fold function type used by `generate_into/5,6`. Takes a sampled value and
an accumulator, and returns the updated accumulator.
""".
-type generate_into_function(T) :: fun((term(), T) -> T).
-doc """
Success or error shape returned by `generate_into/5,6`. On success, contains the
final accumulator; otherwise one of the error variants.
""".
-type generate_into_response(T) :: {ok, T} | generate_error_response().
-doc """
Function type used by `iterate/2`. Receives the previous state (any term) and
returns the next state (any term). Seed the initial state with the second
argument to `iterate/2`.
""".
-type iterate_function() :: fun((term()) -> term()).
-doc """
Function type used by `map/2` to transform each sampled value.
""".
-type map_function() :: fun((term()) -> term()).
-doc """
Predicate function type used by `filter/2`. Return `true` to accept, `false` to
resample.
""".
-type predicate_function() :: fun((term()) -> boolean()).
-doc """
Weighted generator pair used by `weighted/1`: a positive integer weight and the
generator to select with that weight.
""".
-type weighted_gen() :: {pos_integer(), gen()}.
-record(gen_all, {gens :: nonempty_list(gen())}).
-record(gen_binary, {gen_size :: gen(), gen_byte :: gen()}).
-record(gen_bitstring, {gen_size :: gen(), gen_bit :: gen()}).
-record(gen_choose, {gens :: nonempty_list(gen())}).
-record(gen_const, {value :: term()}).
-record(gen_cycle, {id :: reference(), gens :: nonempty_list(gen())}).
-record(gen_dict, {gen_size :: gen(), gen_key :: gen(), gen_value :: gen()}).
-record(gen_float, {min_bound :: float(), max_bound :: float()}).
-record(gen_gb_set, {gen_size :: gen(), gen_value:: gen()}).
-record(gen_gb_tree, {gen_size :: gen(), gen_key :: gen(), gen_value :: gen()}).
-record(gen_integer, {min_bound :: integer(), max_bound :: integer()}).
-record(gen_iterate, {id :: reference(), f :: iterate_function(), init_state :: term()}).
-record(gen_list, {gen_size :: gen(), gen_value :: gen()}).
-record(gen_map, {gen_size :: gen(), gen_key :: gen(), gen_value :: gen()}).
-record(gen_orddict, {gen_size :: gen(), gen_key :: gen(), gen_value :: gen()}).
-record(gen_ordset, {gen_size :: gen(), gen_value :: gen()}).
-record(gen_recurse, {f :: depth_function()}).
-record(gen_set, {gen_size :: gen(), gen_value :: gen()}).
-record(gen_shuffle, {list :: list()}).
-record(gen_string, {gen_size :: gen(), gen_char :: gen()}).
-record(gen_tuple, {gens :: list(gen())}).
-record(gen_weighted, {max_bound :: pos_integer(), weighted_gens :: nonempty_list(weighted_gen())}).
-record(mod_filter, {f :: predicate_function(), gen :: gen()}).
-record(mod_flatmap, {f :: flatmap_function(), gen :: gen()}).
-record(mod_map, {f :: map_function(), gen :: gen()}).
-doc """
Opaque generator type used throughout the library. Values of this type are built
via the provided combinators and consumed by `generate*` functions.
""".
-opaque gen() ::
#gen_all{}
| #gen_binary{}
| #gen_bitstring{}
| #gen_choose{}
| #gen_const{}
| #gen_cycle{}
| #gen_dict{}
| #gen_float{}
| #gen_gb_set{}
| #gen_gb_tree{}
| #gen_integer{}
| #gen_iterate{}
| #gen_list{}
| #gen_map{}
| #gen_orddict{}
| #gen_ordset{}
| #gen_recurse{}
| #gen_set{}
| #gen_shuffle{}
| #gen_string{}
| #gen_tuple{}
| #gen_weighted{}
| #mod_filter{}
| #mod_flatmap{}
| #mod_map{}
.
-doc """
Returns a generator that produces a list containing one sample from each generator, in order.
#### Parameters
- `Gens` — list with >= 2 `gen()` values; each is sampled once per output; each produced list has length `length(Gens)`.
#### Example
```erlang
1> Gen = kaos:all([kaos:const(100), kaos:const(200)]).
2> {ok, Samples} = kaos:generate(Gen, 1000, 3).
3> Samples.
[[100,200],[100,200],[100,200]]
```
""".
-spec all(nonunary_list(gen())) -> gen().
all(Gens = [_, _ | _]) -> #gen_all{gens = Gens}.
-doc """
Returns a generator that produces an Erlang `array()` whose size is generated by `GenSize`
and whose elements are generated by `GenValue`.
#### Parameters
- `GenSize` — generator of non-negative integers (>= 0); determines the array
length. If it yields an invalid value, generation fails with {error,
{badarg, "Size generator must provide an integer >= 0"}, _}.
- `GenValue` — generator of element values; sampled once per index from 0..Size-1.
#### Example
```erlang
1> Gen = kaos:array_of(kaos:const(3), kaos:const(42)).
2> {ok, [Arr]} = kaos:generate(Gen, 1001, 1).
3> array:to_list(Arr).
[42,42,42]
```
""".
-spec array_of(gen(), gen()) -> gen().
array_of(GenSize, GenValue) ->
map(fun array:from_list/1, list_of(GenSize, GenValue)).
-doc """
Returns a generator that produces a `binary()` whose size is generated by `GenSize`
and whose bytes are generated by `GenByte`.
#### Parameters
- `GenSize` — generator of non-negative integers (>= 0); determines the number
of bytes. If it yields an invalid value, generation fails with {error,
{badarg, "Size generator must provide an integer >= 0"}, _}.
- `GenByte` — generator of integers in the range 0..255; sampled once per
byte. If it yields an invalid value, generation fails with {error,
{badarg, "Byte generator must provide an integer between 0 and 255"}, _}.
#### Example
```erlang
1> Gen = kaos:binary_of(kaos:const(3), kaos:const(16)).
2> {ok, [Bin]} = kaos:generate(Gen, 909, 1).
3> binary:bin_to_list(Bin).
[16,16,16]
```
""".
-spec binary_of(gen(), gen()) -> gen().
binary_of(GenSize, GenByte) -> #gen_binary{gen_size = GenSize, gen_byte = GenByte}.
-doc """
Returns a generator that produces a single bit, i.e. an integer `0` or `1`.
#### Example
```erlang
1> Gen = kaos:bit().
2> {ok, Bits} = kaos:generate(Gen, 909, 8).
3> Bits.
[0,1,0,0,0,0,1,1]
```
""".
-spec bit() -> gen().
bit() -> choose([const(0), const(1)]).
-doc """
Returns a generator that produces a bitstring whose size is generated by `GenSize`
and whose bits are generated by `GenBit` (each bit must be `0` or `1`).
#### Parameters
- `GenSize` — generator of non-negative integers (>= 0); determines the number
of bits. If it yields an invalid value, generation fails with {error,
{badarg, "Size generator must provide an integer >= 0"}, _}.
- `GenBit` — generator that yields only `0` or `1`; sampled once per bit. If it
yields an invalid value, generation fails with {error, {badarg,
"Bit generator must produce 0 or 1"}, _}.
#### Example
```erlang
1> Gen = kaos:bitstring_of(kaos:const(5), kaos:bit()).
2> {ok, [Bits]} = kaos:generate(Gen, 909, 1).
3> {bit_size(Bits), Bits}.
{5, <<8:5>>}
```
""".
-spec bitstring_of(gen(), gen()) -> gen().
bitstring_of(GenSize, GenBit) -> #gen_bitstring{gen_size = GenSize, gen_bit = GenBit}.
-doc """
Returns a generator that produces a boolean: `true` or `false`.
#### Example
```erlang
1> Gen = kaos:boolean().
2> {ok, Bools} = kaos:generate(Gen, 303, 8).
3> Bools.
[false,true,true,false,false,true,false,true]
```
""".
-spec boolean() -> gen().
boolean() -> choose([const(true), const(false)]).
-doc """
Returns a generator that produces a byte, i.e. an integer in the range `0..255`.
#### Example
```erlang
1> Gen = kaos:byte().
2> {ok, Bytes} = kaos:generate(Gen, 909, 4).
3> Bytes.
[42,229,232,44]
```
""".
-spec byte() -> gen().
byte() -> integer(0, 255).
-doc """
Returns a generator that randomly selects one of the generators in `Gens` and
produces a sample from it each time.
#### Parameters
- `Gens` — list with at least two `gen()` values; one is chosen uniformly per sample.
#### Example
```erlang
1> Gen = kaos:choose([kaos:const(1), kaos:const(ok), kaos:const("abc")]).
2> {ok, Values} = kaos:generate(Gen, 909, 9).
3> Values.
["abc","abc","abc",ok,ok,"abc",1,ok,"abc"]
```
""".
-spec choose(nonunary_list(gen())) -> gen().
choose(Gens = [_, _ | _]) when length(Gens) > 1 -> #gen_choose{gens = Gens}.
-doc """
Returns a generator that uniformly selects one of the terms in `Elements` on
each sample. This is a convenience wrapper around `choose/1` that accepts raw
terms instead of generators.
#### Parameters
- `Elements` — list with at least two terms; one is chosen uniformly per sample.
#### Example
```erlang
1> Gen = kaos:choose_from_list([1, ok, <<"x">>]).
2> {ok, Values} = kaos:generate(Gen, 909, 7).
3> Values.
[<<"x">>,<<"x">>,ok,1,ok,<<"x">>,ok]
```
""".
-spec choose_from_list(nonunary_list(term())) -> gen().
choose_from_list(Elements = [_, _ | _]) ->
kaos:choose(lists:map(fun kaos:const/1, Elements)).
-doc """
Returns a generator that always produces the given value `A`.
#### Parameters
- `A` — any Erlang term to be returned as-is on every sample.
#### Example
```erlang
1> Gen = kaos:const(ready).
2> {ok, Values} = kaos:generate(Gen, 909, 3).
3> Values.
[ready,ready,ready]
```
""".
-spec const(term()) -> gen().
const(A) -> #gen_const{value = A}.
-doc """
Returns a generator that cycles through the generators in `Gens`, taking one
sample from the next generator each time, and wrapping to the start.
#### Parameters
- `Gens` — list with >= 2 `gen()` values; cycled in order for each sample.
#### Example
```erlang
1> Gen = kaos:cycle([kaos:const(1), kaos:const(true), kaos:const("abc")]).
2> {ok, Values} = kaos:generate(Gen, 505, 7).
3> Values.
[1,true,"abc",1,true,"abc",1]
```
""".
-spec cycle(nonunary_list(gen())) -> gen().
cycle(Gens = [_, _ | _]) -> #gen_cycle{id = make_ref(), gens = Gens}.
-doc """
Returns a generator that produces a `dict()` with size generated by `GenSize` and
keys/values generated by `GenKey`/`GenValue`. Duplicate keys are skipped until the
target size is reached.
#### Parameters
- `GenSize` — generator of non-negative integers (>= 0); determines number of
entries. If it yields an invalid value, generation fails with {error,
{badarg, "Size generator must provide an integer >= 0"}, _}.
- `GenKey` — generator for keys; should produce enough unique keys to reach size.
- `GenValue` — generator for values; sampled once per unique key.
#### Example
```erlang
1> Gen = kaos:dict_of(kaos:const(3), kaos:integer(1,100), kaos:const(ready)).
2> {ok, [D]} = kaos:generate(Gen, 909, 1).
3> dict:to_list(D).
[{25,ready},{63,ready},{42,ready}]
```
""".
-spec dict_of(gen(), gen(), gen()) -> gen().
dict_of(GenSize, GenKey, GenValue) -> #gen_dict{gen_size = GenSize, gen_key = GenKey, gen_value = GenValue}.
-doc """
Returns a generator that filters values from `Gen` using predicate `PredFun/1`,
resampling until `PredFun(Value)` returns `true`.
#### Parameters
- `PredFun` — unary predicate; return `true` to accept a value.
- `Gen` — source generator to sample from.
> Note: If the predicate rarely or never accepts values, generation may take a
> long time or time out. Consider widening `Gen` or relaxing the predicate to
> ensure progress.
#### Example
```erlang
1> Even = fun(N) -> (N rem 2) =:= 0 end.
2> Gen = kaos:filter(Even, kaos:integer(1,10)).
3> {ok, Values} = kaos:generate(Gen, 1212, 6).
4> Values.
[4,6,10,10,2,10]
```
""".
-spec filter(predicate_function(), gen()) -> gen().
filter(PredFun, Gen) when is_function(PredFun, 1) -> #mod_filter{f = PredFun, gen = Gen}.
-doc """
Returns a generator that maps each value from `Gen` through `FlatFun/1` to a new
generator, and samples from that generator (monadic bind).
#### Parameters
- `FlatFun` — unary function returning a `gen()` for a given value.
- `Gen` — source generator whose values are transformed.
#### Example
```erlang
1> F = fun(S) -> kaos:list_of(kaos:const(S), kaos:const(ready)) end.
2> Gen = kaos:flatmap(F, kaos:const(3)).
3> {ok, Values} = kaos:generate(Gen, 999, 3).
4> Values.
[[ready,ready,ready],[ready,ready,ready],[ready,ready,ready]]
```
""".
-spec flatmap(flatmap_function(), gen()) -> gen().
flatmap(FlatFun, Gen) when is_function(FlatFun, 1) -> #mod_flatmap{f = FlatFun, gen = Gen}.
-doc """
Returns a generator that produces a floating-point number in the open interval
`(MinBound, MaxBound)`, with `MinBound < MaxBound`.
#### Parameters
- `MinBound` — lower bound (float); must be less than `MaxBound`.
- `MaxBound` — upper bound (float); must be greater than `MinBound`.
#### Example
```erlang
1> Gen = kaos:float(1.5, 2.5).
2> {ok, Values} = kaos:generate(Gen, 101, 3).
3> Values.
[1.9373364319283917,1.9752916097381772,1.7695279988967685]
```
""".
-spec float(float(), float()) -> gen().
float(MinBound, MaxBound)
when is_float(MinBound), is_float(MaxBound), MinBound < MaxBound ->
#gen_float{min_bound = MinBound, max_bound = MaxBound}.
-doc """
Returns a generator that produces a `gb_set()` with size generated by `GenSize`
and elements generated by `GenValue`. Duplicate elements are skipped until the
target size is reached.
#### Parameters
- `GenSize` — generator of non-negative integers (>= 0); determines set
cardinality. If it yields an invalid value, generation fails with {error,
{badarg, "Size generator must provide an integer >= 0"}, _}.
- `GenValue` — generator for set elements; should produce enough unique
elements to reach size. Duplicates are skipped; a narrow domain may increase
generation time or cause timeouts.
#### Example
```erlang
1> Gen = kaos:gb_set_of(kaos:const(3), kaos:integer(1,100)).
2> {ok, [S]} = kaos:generate(Gen, 909, 1).
3> gb_sets:to_list(S).
[25,42,63]
```
""".
-spec gb_set_of(gen(), gen()) -> gen().
gb_set_of(GenSize, GenValue) -> #gen_gb_set{gen_size = GenSize, gen_value = GenValue}.
-doc """
Returns a generator that produces a `gb_tree()` (balanced tree map) with size
generated by `GenSize` and keys/values generated by `GenKey`/`GenValue`.
Duplicate keys are skipped until the target size is reached.
#### Parameters
- `GenSize` — generator of non-negative integers (>= 0); determines number of
entries. If it yields an invalid value, generation fails with {error,
{badarg, "Size generator must provide an integer >= 0"}, _}.
- `GenKey` — generator for keys; should provide enough unique keys to reach
size. Duplicates are skipped; a narrow key domain may increase generation
time or cause timeouts.
- `GenKey` — generator for keys; should provide enough unique keys to reach
size. Duplicates are skipped; a narrow key domain may increase generation
time or cause timeouts.
- `GenValue` — generator for values; sampled once per unique key.
#### Example
```erlang
1> Gen = kaos:gb_tree_of(kaos:const(3), kaos:integer(1,100), kaos:const(ready)).
2> {ok, [T]} = kaos:generate(Gen, 909, 1).
3> gb_trees:to_list(T).
[{25,ready},{42,ready},{63,ready}]
```
""".
-spec gb_tree_of(gen(), gen(), gen()) -> gen().
gb_tree_of(GenSize, GenKey, GenValue) -> #gen_gb_tree{gen_size = GenSize, gen_key = GenKey, gen_value = GenValue}.
-doc """
Returns a generator that produces integers in the inclusive range
`MinBound..MaxBound`, with `MinBound < MaxBound`.
#### Parameters
- `MinBound` — lower bound (integer), strictly less than `MaxBound`.
- `MaxBound` — upper bound (integer), strictly greater than `MinBound`.
#### Example
```erlang
1> Gen = kaos:integer(1, 10).
2> {ok, Values} = kaos:generate(Gen, 707, 5).
3> Values.
[1,6,10,7,6]
```
""".
-spec integer(integer(), integer()) -> gen().
integer(MinBound, MaxBound)
when is_integer(MinBound), is_integer(MaxBound), MinBound < MaxBound ->
#gen_integer{min_bound = MinBound, max_bound = MaxBound}.
-doc """
Returns a generator that iteratively applies `IterFun/1`, threading state from
one step to the next. The first call uses `InitState` as the previous state.
#### Parameters
- `IterFun` — unary step function `(PrevState) -> NextState`.
- `InitState` — initial state used on the first step.
#### Example
```erlang
1> Step = fun({A,B}) -> {B, A+B} end. % Fibonacci state step
2> StateGen = kaos:iterate(Step, {0,1}). % Initial value {0,1}
3> FibGen = kaos:map(fun({_,B}) -> B end, StateGen).
4> {ok, Values} = kaos:generate(FibGen, 909, 10).
5> Values.
[1,1,2,3,5,8,13,21,34,55]
```
""".
-spec iterate(iterate_function(), term()) -> gen().
iterate(IterFun, InitState) when is_function(IterFun, 1) ->
#gen_iterate{id = make_ref(), f = IterFun, init_state = InitState}.
-doc """
Returns a generator that produces a list whose length is generated by `GenSize`
and whose elements are generated by `GenValue`.
#### Parameters
- `GenSize` — generator of non-negative integers (>= 0); determines list
length. If it yields an invalid value, generation fails with {error,
{badarg, "Size generator must provide an integer >= 0"}, _}.
- `GenValue` — generator for each element.
#### Example
```erlang
1> Gen = kaos:list_of(kaos:const(3), kaos:const(ready)).
2> {ok, [L]} = kaos:generate(Gen, 9, 1).
3> L.
[ready,ready,ready]
```
""".
-spec list_of(gen(), gen()) -> gen().
list_of(GenSize, GenValue) -> #gen_list{gen_size = GenSize, gen_value = GenValue}.
-doc """
Returns a generator that produces a `map()` with size generated by `GenSize` and
keys/values generated by `GenKey`/`GenValue`. Duplicate keys are skipped until the
target size is reached.
#### Parameters
- `GenSize` — generator of non-negative integers (>= 0); number of entries. If
it yields an invalid value, generation fails with {error, {badarg, "Size
generator must provide an integer >= 0"}, _}.
- `GenKey` — generator for keys; should provide enough unique keys to reach
size. Duplicates are skipped; a narrow key domain may increase generation
time or cause timeouts.
- `GenValue` — generator for values; sampled once per unique key.
#### Example
```erlang
1> Gen = kaos:map_of(kaos:const(3), kaos:integer(1,100), kaos:const(ready)).
2> {ok, [M]} = kaos:generate(Gen, 909, 1).
3> maps:to_list(M).
[{25,ready},{42,ready},{63,ready}]
```
""".
-spec map_of(gen(), gen(), gen()) -> gen().
map_of(GenSize, GenKey, GenValue) -> #gen_map{gen_size = GenSize, gen_key = GenKey, gen_value = GenValue}.
-doc """
Returns a generator that produces an `orddict()` (sorted list of `{Key,Val}`)
with size generated by `GenSize` and keys/values generated by `GenKey`/`GenValue`.
Duplicate keys are skipped until the target size is reached.
#### Parameters
- `GenSize` — generator of non-negative integers (>= 0); number of entries. If
it yields an invalid value, generation fails with {error, {badarg, "Size
generator must provide an integer >= 0"}, _}.
- `GenKey` — generator for keys; should provide enough unique keys to reach
size. Duplicates are skipped; a narrow key domain may increase generation
time or cause timeouts.
- `GenValue` — generator for values; sampled once per unique key.
#### Example
```erlang
1> Gen = kaos:orddict_of(kaos:const(3), kaos:integer(1,100), kaos:const(ready)).
2> {ok, [OD]} = kaos:generate(Gen, 909, 1).
3> orddict:to_list(OD).
[{25,ready},{42,ready},{63,ready}]
```
""".
-spec orddict_of(gen(), gen(), gen()) -> gen().
orddict_of(GenSize, GenKey, GenValue) -> #gen_orddict{gen_size = GenSize, gen_key = GenKey, gen_value = GenValue}.
-doc """
Returns a generator that produces an `ordset()` (sorted set) with size generated
by `GenSize` and elements generated by `GenValue`. Duplicate elements are
skipped until the target size is reached.
#### Parameters
- `GenSize` — generator of non-negative integers (>= 0); determines set
cardinality. If it yields an invalid value, generation fails with {error,
{badarg, "Size generator must provide an integer >= 0"}, _}.
- `GenValue` — generator for set elements; should produce enough unique
elements to reach size. Duplicates are skipped; a narrow domain may increase
generation time or cause timeouts.
#### Example
```erlang
1> Gen = kaos:ordset_of(kaos:const(3), kaos:integer(1,100)).
2> {ok, [S]} = kaos:generate(Gen, 909, 1).
3> ordsets:to_list(S).
[25,42,63]
```
""".
-spec ordset_of(gen(), gen()) -> gen().
ordset_of(GenSize, GenValue) -> #gen_ordset{gen_size = GenSize, gen_value = GenValue}.
-doc """
Returns a generator that delegates to `DepthFun/1` with a depth counter to help
build recursive structures without unbounded growth. Within `DepthFun`, you may
return `kaos:recurse(DepthFun)` to go one level deeper, or any other generator
to terminate.
#### Parameters
- `DepthFun` — `fun(Depth) -> gen()`; `Depth` starts at 0 and increases when you
return `recurse(DepthFun)`.
> Note: Always include a termination condition (for example, a depth limit);
> without it, recursion may not converge and generation can time out.
#### Example
```erlang
1> F = fun Recur(D) ->
1> case D >= 3 of
1> true -> kaos:const(leaf);
1> false ->
1> kaos:choose([
1> kaos:const(leaf),
1> kaos:map(fun({L,R}) -> {node,L,R} end,
1> kaos:tuple_of([kaos:recurse(Recur), kaos:recurse(Recur)]))
1> ])
1> end
1> end.
2> {ok, [Tree]} = kaos:generate(kaos:recurse(F), 505, 1).
3> Tree.
{node,{node,{node,leaf,leaf},leaf},{node,{node,leaf,leaf},{node,leaf,leaf}}}
```
""".
-spec recurse(depth_function()) -> gen().
recurse(DepthFun) when is_function(DepthFun, 1) -> #gen_recurse{f = DepthFun}.
-doc """
Returns a generator that produces a `sets:set()` with size generated by
`GenSize` and elements generated by `GenValue`. Duplicate elements are skipped
until the target size is reached.
#### Parameters
- `GenSize` — generator of non-negative integers (>= 0); set cardinality. If it
yields an invalid value, generation fails with {error, {badarg, "Size
generator must provide an integer >= 0"}, _}.
- `GenValue` — generator for set elements; should produce enough unique elements to reach size. Duplicates are skipped; a narrow domain may increase generation time or cause timeouts.
#### Example
```erlang
1> Gen = kaos:set_of(kaos:const(3), kaos:integer(1,100)).
2> {ok, [S]} = kaos:generate(Gen, 909, 1).
3> sets:to_list(S).
[25,42,63]
```
""".
-spec set_of(gen(), gen()) -> gen().
set_of(GenSize, GenValue) -> #gen_set{gen_size = GenSize, gen_value = GenValue}.
-doc """
Returns a generator that produces a random permutation of the given list
(`Fisher–Yates` shuffle).
#### Parameters
- `Elements` — list of terms to shuffle.
#### Example
```erlang
1> Gen = kaos:shuffle([1,2,3,4,5]).
2> {ok, [L]} = kaos:generate(Gen, 909, 1).
3> L.
[3,1,5,4,2]
```
""".
-spec shuffle(list()) -> gen().
shuffle(Elements) when is_list(Elements) -> #gen_shuffle{list = Elements}.
-doc """
Returns a generator that produces a UTF-8 binary (string) whose length is
generated by `GenSize` and whose characters are generated by `GenChar`.
#### Parameters
- `GenSize` — generator of non-negative integers (>= 0); number of characters.
If it yields an invalid value, generation fails with {error, {badarg,
"Size generator must provide an integer >= 0"}, _}.
- `GenChar` — generator of Unicode code points (integers). Values must be valid
Unicode scalar values: `0..16#D7FF` or `16#E000..16#10FFFF`. If an invalid code
point is produced or encoding fails, generation returns
`{error, {badarg, "Invalid code point given by generator."}, _}`.
#### Example
```erlang
1> Gen = kaos:string_of(kaos:const(5), kaos:const($A)).
2> {ok, [S]} = kaos:generate(Gen, 111, 1).
3> S.
<<"AAAAA">>
```
""".
-spec string_of(gen(), gen()) -> gen().
string_of(GenSize, GenChar) -> #gen_string{gen_size = GenSize, gen_char = GenChar}.
-doc """
Returns a generator that produces a tuple by sampling each generator in `Gens`
once and assembling the results in order.
#### Parameters
- `Gens` — list of `gen()` values; one element per tuple position.
#### Example
```erlang
1> Gen = kaos:tuple_of([kaos:const(1), kaos:const(true), kaos:const("abc")]).
2> {ok, [T]} = kaos:generate(Gen, 9, 1).
3> T.
{1,true,"abc"}
```
""".
-spec tuple_of(list(gen())) -> gen().
tuple_of(Gens) when is_list(Gens) -> #gen_tuple{gens = Gens}.
-doc """
Returns a generator that selects among `gen()` values with integer weights.
Weights are normalized by their greatest common divisor, and selection is
uniform within the expanded ranges.
#### Parameters
- `WeightedGens` — list of `{Weight, Gen}` pairs where `Weight > 0`.
#### Example
```erlang
1> Gen = kaos:weighted([{20, kaos:const(2)}, {80, kaos:const(8)}]).
2> {ok, Values} = kaos:generate(Gen, 101, 10).
3> Values.
[8,8,8,8,8,2,2,2,8,8]
```
""".
-spec weighted(nonunary_list(weighted_gen())) -> gen().
weighted(WeightedGens = [_, _ | _]) ->
Weights = lists:map(fun({W, _}) when is_integer(W), W > 0 -> W end, WeightedGens),
GCD = reduce(fun gcd/2, Weights),
Increments = lists:map(fun(W) when is_integer(W) -> trunc(W / GCD) end, Weights),
ReverseBounds =
[_ | _] =
lists:foldl(fun (I, []) when is_integer(I) ->
[I];
(I, A = [F | _]) when is_integer(I), is_list(A), is_integer(F) ->
[F + I | A]
end,
[],
Increments),
Bounds = lists:reverse(ReverseBounds),
MaxBound = lists:max(Bounds),
BoundedGens = lists:zipwith(fun(B, {_, G}) -> {B, G} end, Bounds, WeightedGens),
#gen_weighted{max_bound = MaxBound, weighted_gens = BoundedGens}.
-doc """
Returns a `{Weight, Gen}` pair suitable for `weighted/1` where `Weight` is the
number of alternatives in `Elements` and `Gen` selects uniformly from those
elements. Useful when composing several lists with `weighted/1` and you want
each list to contribute proportionally to its size.
#### Parameters
- `Elements` — non-empty list of terms; duplicates are allowed and will be
reflected in the uniform selection within the list. A singleton `[A]`
yields `{1, kaos:const(A)}`.
#### Example
```erlang
1> Fruits = kaos:weighted_from_list([apple, banana, cherry]). % weight 3
2> Digits = kaos:weighted_from_list([0,1,2]). % weight 3
3> Gen = kaos:weighted([Fruits, Digits]).
4> {ok, Values} = kaos:generate(Gen, 707, 6).
5> Values.
[0,apple,1,banana,2,cherry]
```
""".
-spec weighted_from_list(nonempty_list(term())) -> weighted_gen().
weighted_from_list([Element]) ->
{1, kaos:const(Element)};
weighted_from_list(Elements = [_ | _]) ->
{length(Elements), choose_from_list(Elements)}.
-doc """
Returns a `{Weight, Gen}` pair suitable for `weighted/1` for an inclusive
integer range. `Weight` equals the count of integers in the range, and `Gen`
produces integers uniformly from `MinBound..MaxBound`.
#### Parameters
- `MinBound` — lower bound (integer), strictly less than `MaxBound`.
- `MaxBound` — upper bound (integer), strictly greater than `MinBound`.
#### Example
```erlang
1> Small = kaos:weighted_from_range(1, 3). % weight 3 (1,2,3)
2> Big = kaos:weighted_from_range(10, 11). % weight 2 (10,11)
3> Gen = kaos:weighted([Small, Big]).
4> {ok, Values} = kaos:generate(Gen, 505, 8).
5> Values.
[10,1,2,11,3,1,2,3]
```
""".
-spec weighted_from_range(integer(), integer()) -> weighted_gen().
weighted_from_range(MinBound, MaxBound) when is_integer(MinBound), is_integer(MaxBound), MinBound < MaxBound ->
{abs(MaxBound - MinBound) + 1, kaos:integer(MinBound, MaxBound)}.
-doc """
Returns a generator that maps each sampled value from `Gen` through `MapFun/1`.
#### Parameters
- `MapFun` — unary function to transform each sampled value.
- `Gen` — source generator to sample from.
#### Example
```erlang
1> MapFun = fun(S) -> S * 10 end.
2> Gen = kaos:map(MapFun, kaos:const(3)).
3> {ok, Values} = kaos:generate(Gen, 999, 3).
4> Values.
[30,30,30]
```
""".
-spec map(map_function(), gen()) -> gen().
map(MapFun, Gen) when is_function(MapFun, 1) -> #mod_map{f = MapFun, gen = Gen}.
default_timeout() -> 30_000.
-doc """
Generates a list of `Count` samples from `Gen` using `Seed` for deterministic
randomness.
#### Parameters
- `Gen` — generator to sample from.
- `Seed` — term to seed the random number generator.
- `Count` — positive integer; number of samples.
#### Example
```erlang
1> {ok, Values} = kaos:generate(kaos:integer(1,5), 909, 5).
2> Values.
[3,2,5,5,2]
```
""".
-spec generate(gen(), term(), pos_integer()) -> generate_response().
generate(Gen, Seed, Count) when is_integer(Count), Count > 0 ->
generate(Gen, Seed, Count, default_timeout()).
-doc """
Like `generate/3` but with a millisecond timeout. Returns `{error, timeout}` if
the worker exceeds `Timeout`.
#### Parameters
- `Gen` — generator to sample from.
- `Seed` — term to seed the random number generator.
- `Count` — positive integer; number of samples.
- `Timeout` — positive integer; milliseconds before timing out.
#### Example
```erlang
1> {ok, Values} = kaos:generate(kaos:integer(1,5), 909, 3, 5000).
2> Values.
[3,2,5]
```
""".
-spec generate(gen(), term(), pos_integer(), pos_integer()) -> generate_response().
generate(Gen, Seed, Count, Timeout) when is_integer(Count), Count > 0, is_integer(Timeout), Timeout > 0 ->
Result = generate_into(
Gen,
Seed,
Count,
fun (Sample, Acc) -> [Sample | Acc] end,
[],
Timeout
),
case Result of
{ok, SampleList} -> {ok, lists:reverse(SampleList)};
Error -> Error
end.
-doc """
Generates `Count` samples from `Gen`, folding each into the accumulator with
`MergeFun/2`, and returns the final accumulator.
#### Parameters
- `Gen` — generator to sample from.
- `Seed` — term to seed the random number generator.
- `Count` — positive integer; number of samples.
- `MergeFun` — fun `(Sample, Acc) -> Acc1` to merge each sample.
- `InitAcc` — initial accumulator value.
#### Examples
##### Set Accumulation
```erlang
1> MergeFun = fun(V, S) -> sets:add_element(V, S) end.
2> {ok, Set} = kaos:generate_into(kaos:integer(1,3), 505, 20, MergeFun, sets:new()).
3> sets:to_list(Set).
[1,2,3]
```
##### Remote Streaming (Distributed Erlang)
```erlang
1> %% On the sink node (register a simple receiver)
1> register(
1> sink,
1> spawn(fun Loop() ->
1> receive
1> {sample, V} ->
1> io:format("~p~n", [V]),
1> Loop();
1> stop ->
1> ok
1> end
1> end)
1> ).
2> %% On the source node (replace HOST with the sink host)
3> Merge = fun(V, N) -> {sink, 'sink@HOST'} ! {sample, V}, N + 1 end.
4> {ok, Count} = kaos:generate_into(kaos:integer(1,3), 909, 1000, Merge, 0).
```
""".
-spec generate_into(gen(), term(), pos_integer(), generate_into_function(T), T) -> generate_into_response(T).
generate_into(Gen, Seed, Count, MergeFun, InitAcc) when is_integer(Count), Count > 0 ->
generate_into(Gen, Seed, Count, MergeFun, InitAcc, default_timeout()).
-doc """
Like `generate_into/5` but with a millisecond timeout. Returns `{error, timeout}`
if the worker exceeds `Timeout`.
#### Parameters
- `Gen` — generator to sample from.
- `Seed` — term to seed the random number generator.
- `Count` — positive integer; number of samples.
- `MergeFun` — fun `(Sample, Acc) -> Acc1`.
- `InitAcc` — initial accumulator value.
- `Timeout` — positive integer; milliseconds before timing out.
#### Examples
##### Set Accumulation
```erlang
1> MergeFun = fun(V, S) -> sets:add_element(V, S) end.
2> {ok, Set} = kaos:generate_into(kaos:integer(1,3), 505, 20, MergeFun, sets:new(), 5000).
3> sets:to_list(Set).
[1,2,3]
```
##### Remote Streaming (Distributed Erlang)
```erlang
1> %% On the sink node (register a simple receiver)
1> register(
1> sink,
1> spawn(fun Loop() ->
1> receive
1> {sample, V} ->
1> io:format("~p~n", [V]),
1> Loop();
1> stop ->
1> ok
1> end
1> end)
1> ).
2> %% On the source node (replace HOST with the sink host)
3> Merge = fun(V, N) -> {sink, 'sink@HOST'} ! {sample, V}, N + 1 end.
4> {ok, Count} = kaos:generate_into(kaos:integer(1,3), 909, 1000, Merge, 0, 5000).
```
""".
-spec generate_into(gen(), term(), pos_integer(), generate_into_function(T), T, pos_integer()) -> generate_into_response(T).
generate_into(Gen, Seed, Count, MergeFun, InitAcc, Timeout) when is_integer(Count), Count > 0, is_integer(Timeout), Timeout > 0 ->
PrevTrapExit = process_flag(trap_exit, true),
Self = self(),
WorkerPid = spawn_link(
fun () ->
generate_worker(Gen, Seed, Count, Self, MergeFun, InitAcc)
end
),
CleanUpMessages =
fun Loop () ->
receive
_ -> Loop()
after 0 ->
ok
end
end,
CleanUpWorker =
fun () ->
process_flag(trap_exit, PrevTrapExit),
unlink(WorkerPid),
exit(WorkerPid, normal),
CleanUpMessages()
end,
receive
{'EXIT', _, Error} -> CleanUpWorker(), {error, Error};
Error = {error, _, _} -> CleanUpWorker(), Error;
Result = {ok, _} -> CleanUpWorker(), Result
after Timeout ->
CleanUpWorker(),
{error, timeout}
end.
generate_worker(Gen, Seed, Count, To, MergeFun, InitAcc) when is_integer(Count), Count > 0 ->
_ = rand:seed(exsss, Seed),
RunLoop =
fun Loop(RemainingCount, LoopAcc) ->
case RemainingCount of
0 -> LoopAcc;
_ ->
Sample = generate_one(Gen),
Loop(RemainingCount - 1, MergeFun(Sample, LoopAcc))
end
end,
try
To ! {ok, RunLoop(Count, InitAcc)}
catch
_:Reason:Stacktrace ->
To ! {error, Reason, Stacktrace}
end.
gcd(A, B) when is_integer(A), is_integer(B), A < B -> gcd(B, A);
gcd(A, 0) when is_integer(A), A >= 0 -> A;
gcd(A, B) when is_integer(A), A >= 0, is_integer(B), B >= 0 -> gcd(B, A rem B).
reduce(_, [L]) -> L;
reduce(Fun, [L, R | T]) -> reduce(Fun, [Fun(L, R) | T]).
is_valid_code_point(N) when is_integer(N) ->
if
N >= 0 andalso N =< 16#D7FF -> true;
N >= 16#E000 andalso N =< 16#10FFFF -> true;
true -> false
end;
is_valid_code_point(_) -> false.
generate_one(#gen_all{gens = Gens}) ->
lists:map(fun generate_one/1, Gens);
generate_one(#gen_binary{gen_size = GenSize, gen_byte = GenByte}) ->
case Size = generate_one_size(GenSize) of
0 -> <<>>;
_ ->
Bytes = [generate_one_byte(GenByte) || _ <- lists:seq(1, Size)],
<< <<B>> || B <- Bytes >>
end;
generate_one(#gen_bitstring{gen_size = GenSize, gen_bit = GenBit}) ->
GenerateOneBit =
fun () ->
case generate_one(GenBit) of
0 -> 0;
1 -> 1;
_ -> throw({badarg, "Bit generator must produce 0 or 1"})
end
end,
case Size = generate_one_size(GenSize) of
0 -> <<>>;
_ ->
Bits = [GenerateOneBit() || _ <- lists:seq(1, Size)],
<< <<B:1>> || B <- Bits >>
end;
generate_one(#gen_choose{gens = Gens}) ->
Index = generate_one(integer(1, length(Gens))),
generate_one(lists:nth(Index, Gens));
generate_one(#gen_const{value = Value}) ->
Value;
generate_one(#gen_cycle{id = Id, gens = Gens}) ->
StateKey = {kaos, gen_cycle, Id},
InitState = fun () -> put(StateKey, 1) end,
GetNextIndex =
fun NextIndex() ->
case get(StateKey) of
undefined ->
InitState(),
NextIndex();
N when is_integer(N), N > length(Gens) ->
InitState(),
NextIndex();
N when is_integer(N) ->
put(StateKey, N + 1),
N
end
end,
generate_one(lists:nth(GetNextIndex(), Gens));
generate_one(#gen_dict{gen_size = GenSize, gen_key = GenKey, gen_value = GenValue}) ->
Size = generate_one_size(GenSize),
MapTrait = #map_trait{
new = fun dict:new/0,
size = fun dict:size/1,
is_key = fun dict:is_key/2,
put = fun dict:store/3
},
generate_one_map_until_size(MapTrait, GenKey, GenValue, Size);
generate_one(#gen_float{min_bound = MinBound, max_bound = MaxBound}) ->
MaxFromZero = MaxBound - MinBound,
rand:uniform_real() * MaxFromZero + MinBound;
generate_one(#gen_gb_set{gen_size = GenSize, gen_value = GenValue}) ->
Size = generate_one_size(GenSize),
SetTrait =
#set_trait{new = fun gb_sets:new/0,
size = fun gb_sets:size/1,
is_member = fun gb_sets:is_member/2,
add = fun gb_sets:add/2},
generate_one_set_until_size(SetTrait, GenValue, Size);
generate_one(#gen_gb_tree{gen_size = GenSize, gen_key = GenKey, gen_value = GenValue}) ->
Size = generate_one_size(GenSize),
MapTrait =
#map_trait{new = fun gb_trees:empty/0,
size = fun gb_trees:size/1,
is_key = fun gb_trees:is_defined/2,
put = fun gb_trees:enter/3},
generate_one_map_until_size(MapTrait, GenKey, GenValue, Size);
generate_one(#gen_integer{min_bound = MinBound, max_bound = MaxBound}) ->
MaxFromZero = MaxBound - MinBound,
rand:uniform(MaxFromZero + 1) - 1 + MinBound;
generate_one(#gen_iterate{id = Id, f = IterFun, init_state = InitState}) ->
StateKey = {kaos, gen_iterate, Id},
NextState =
case get(StateKey) of
undefined -> InitState;
PrevState -> IterFun(PrevState)
end,
% Next previous state
put(StateKey, NextState),
NextState;
generate_one(#gen_list{gen_size = GenSize, gen_value = GenValue}) ->
Size = generate_one_size(GenSize),
lists:map(fun(_) -> generate_one(GenValue) end, lists:seq(1, Size));
generate_one(#gen_map{gen_size = GenSize, gen_key = GenKey, gen_value = GenValue}) ->
Size = generate_one_size(GenSize),
MapTrait =
#map_trait{new = fun maps:new/0,
size = fun maps:size/1,
is_key = fun maps:is_key/2,
put = fun maps:put/3},
generate_one_map_until_size(MapTrait, GenKey, GenValue, Size);
generate_one(#gen_orddict{gen_size = GenSize, gen_key = GenKey, gen_value = GenValue}) ->
Size = generate_one_size(GenSize),
MapTrait =
#map_trait{new = fun orddict:new/0,
size = fun orddict:size/1,
is_key = fun orddict:is_key/2,
put = fun orddict:store/3},
generate_one_map_until_size(MapTrait, GenKey, GenValue, Size);
generate_one(#gen_ordset{gen_size = GenSize, gen_value = GenValue}) ->
Size = generate_one_size(GenSize),
SetTrait =
#set_trait{new = fun ordsets:new/0,
size = fun ordsets:size/1,
is_member = fun ordsets:is_element/2,
add = fun ordsets:add_element/2},
generate_one_set_until_size(SetTrait, GenValue, Size);
generate_one(#gen_set{gen_size = GenSize, gen_value = GenValue}) ->
Size = generate_one_size(GenSize),
SetTrait =
#set_trait{new = fun sets:new/0,
size = fun sets:size/1,
is_member = fun sets:is_element/2,
add = fun sets:add_element/2},
generate_one_set_until_size(SetTrait, GenValue, Size);
generate_one(#gen_shuffle{list = Elements}) ->
% Implementation of Fisher-Yates shuffle algorithm.
% See: https://en.wikipedia.org/wiki/Fisher–Yates_shuffle
case Elements of
[] -> [];
[E] -> [E];
_ ->
ElemArray = array:from_list(Elements),
N = array:size(ElemArray),
N_2 = N - 2,
N_1 = N - 1,
Shuffle = fun
Recur (Array, I) when I > N_2 ->
array:to_list(Array);
Recur (Array, I) ->
J = generate_one(integer(I, N_1)),
IE = array:get(I, Array),
JE = array:get(J, Array),
ISwapped = array:set(I, JE, Array),
JSwapped = array:set(J, IE, ISwapped),
Recur(JSwapped, I + 1)
end,
Shuffle(ElemArray, 0)
end;
generate_one(#gen_recurse{f = Fun}) ->
StateKey = {kaos, gen_recurse},
Depth = case get(StateKey) of
undefined ->
put(StateKey, 0),
0;
PreviousLevel ->
NewDepth = PreviousLevel + 1,
put(StateKey, NewDepth),
NewDepth
end,
try
generate_one(Fun(Depth))
after
case Depth of
0 ->
erase(StateKey);
_ ->
put(StateKey, Depth - 1)
end
end;
generate_one(#gen_string{gen_size = GenSize, gen_char = GenChar}) ->
BadCodePointError = {badarg, "Invalid code point given by generator."},
CodePoints = generate_one(list_of(GenSize, GenChar)),
AllValid = lists:all(fun is_valid_code_point/1, CodePoints),
case AllValid of
true ->
Result = unicode:characters_to_binary(CodePoints),
case Result of
{_, _, _} -> throw(BadCodePointError);
_ -> Result
end;
false ->
throw(BadCodePointError)
end;
generate_one(#gen_tuple{gens = Gens}) ->
case length(Gens) of
0 ->
{};
_ ->
Values = lists:map(fun(Gen) -> generate_one(Gen) end, Gens),
list_to_tuple(Values)
end;
generate_one(#gen_weighted{max_bound = MaxBound, weighted_gens = WeightedGens}) ->
Selector = generate_one(integer(1, MaxBound)),
{value, {_, FoundGen}} =
lists:search(fun({Bound, _}) -> Selector =< Bound end, WeightedGens),
generate_one(FoundGen);
generate_one(#mod_flatmap{f = Fun, gen = Gen}) ->
GenFn = fun() -> Fun(generate_one(Gen)) end,
generate_one(GenFn());
generate_one(#mod_filter{f = Fun, gen = Gen}) ->
ValFn =
fun Go() ->
Value = generate_one(Gen),
case Fun(Value) of
true ->
Value;
false ->
Go()
end
end,
ValFn();
generate_one(#mod_map{f = Fun, gen = Gen}) ->
Fun(generate_one(Gen));
generate_one(Unknown) ->
Message = lists:flatten(io_lib:format("Argument provided does not appear to be a generator: ~p", [Unknown])),
throw({badarg, Message}).
generate_one_byte(GenByte) ->
case Byte = generate_one(GenByte) of
_ when not is_integer(Byte) ->
throw({badarg, "Byte generator must provide an integer"});
_ when Byte < 0 orelse Byte > 255 ->
throw({badarg, "Byte generator must provide an integer between 0 and 255"});
_ ->
Byte
end.
generate_one_size(GenSize) ->
case Size = generate_one(GenSize) of
_ when not is_integer(Size) ->
throw({badarg, "Size generator must provide an integer"});
_ when Size < 0 ->
throw({badarg, "Size generator must provide an integer >= 0"});
_ ->
Size
end.
generate_one_map_until_size(MapTrait, GenKey, GenValue, Size) when is_record(MapTrait, map_trait) ->
generate_one_map_until_size(MapTrait, (MapTrait#map_trait.new)(), GenKey, GenValue, Size).
generate_one_map_until_size(MapTrait, Map, GenKey, GenValue, Size) when is_record(MapTrait, map_trait) ->
case (MapTrait#map_trait.size)(Map) >= Size of
true ->
Map;
false ->
Key = generate_one(GenKey),
case (MapTrait#map_trait.is_key)(Key, Map) of
true ->
generate_one_map_until_size(MapTrait, Map, GenKey, GenValue, Size);
false ->
Value = generate_one(GenValue),
UpdatedMap = (MapTrait#map_trait.put)(Key, Value, Map),
generate_one_map_until_size(MapTrait, UpdatedMap, GenKey, GenValue, Size)
end
end.
generate_one_set_until_size(SetTrait, GenValue, Size) when is_record(SetTrait, set_trait) ->
generate_one_set_until_size(SetTrait, (SetTrait#set_trait.new)(), GenValue, Size).
generate_one_set_until_size(SetTrait, Set, GenValue, Size) when is_record(SetTrait, set_trait) ->
case (SetTrait#set_trait.size)(Set) >= Size of
true ->
Set;
false ->
Value = generate_one(GenValue),
case (SetTrait#set_trait.is_member)(Value, Set) of
true ->
generate_one_set_until_size(SetTrait, Set, GenValue, Size);
false ->
UpdatedSet = (SetTrait#set_trait.add)(Value, Set),
generate_one_set_until_size(SetTrait, UpdatedSet, GenValue, Size)
end
end.