gen

package module
v0.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 22, 2023 License: MIT Imports: 6 Imported by: 0

README

Gen

Gen is a random generator library, which is safe (compile-time type checking), more reliable, and easier to use than testing/quick.Generator.

Let's take a look at Gen's base components, functions, variables, and "how to extend"!

Gen interface

Gen has a generic interface called Gen[T], it has one resposibility, and that is to generate a value of type T:

type Gen[T any] interface {
	Generate() T
}

The logic behind generating depends on the structs/interfaces implementing this interface. You can create various implementations of it as you wish.

Now let's take a look at its most common implementations, which already exist:

Pure

Pure (the word comes from Applicatives in functional programming) is basically the basic lazy constructor for Generators, it takes the Generate function, and returns a generator for that type:

intGen := gen.Pure(func() int { return rand.Intn(100) })
ageGen := gen.Pure(func() int { return 42 })

Only

Only is the most basic generator, as the name declares, it will only generate the value that it's given:

onlyTwo := gen.Only(2)
values := gen.GenerateN(onlyTwo, 10000)
// values will be a slice, containing 10000 elements, 
// which all of them have the value of 2

Although it may seem silly in the first sight, but it can be super-useful specially in property-based-testings, or inferring/creating new generators!

OneOf

OneOf is yet another generator, which as its name declares, can select a value among it's given values:

nameGen := gen.OneOf("Bob", "Alice", "Peter", "John")

It's useful in many cases, especially when you want to avoid full-randomness, and try to rely on meaningful values.

Between (Numeric)

Between is a generator, which only can be used with Numeric data types. Numeric is a simple type constraint:

type Numeric interface {
	uint8 | uint16 | uint32 | uint64 | uint | int8 | int16 | int32 | int64 | int | float32 | float64
}

The order of the given arguments to Between actually does not matter, but it's always a good practice to code what you actually think of:

ageGen := gem.Between(1, 100)
badPracticeAgeGen := gen.Between(100, 1) // still works though!

time.Time is not a Numeric, but there's a function which does the same thing for time!

TimeBetween

The logic is pretty much the same as in Between:

now := time.Now()
tenDaysAgo := now.Add(-10 * 24 * time.Hour)
timeGen := gen.TimeBetween(tenDaysAgo, now)
times := gen.GenerateN(timeGen, 10)
// will generate 10 time.Time instances in the given interval

Sequential

Sequential is yet another generator, which can sequentially generate Numeric values, meaning that it holds an internal state of the current value. It's basically a Range data-type (sort of), and in case it exceeds the maximum amount of the range, it continues from the start:

from := 1
to := 100
step := 2
seq := Sequential(from, to, step)

a := seq.Generate()
b := seq.Generate()
c := gen.GenerateN(seq, 3)

fmt.Printf("a = %d, b = %d, c = %v\n", a, b, c)
// prints a = 1, b = 3, c = [5, 7]

Much like what we had for Between, we also have a TimeSeq which is the exact same thing as Sequential, but for time.Time.

TimeSeq

TimeSeq is basically just the same as Sequential, but it's designed for time.Time. An important difference to note is that since there's no specific unit for time.Duration, the calculations for when the range overflows might differ from Sequential[Numeric]:

now := time.Now()
start := now.Add(-10 * 24 * time.Hour)
end := now.Plus(10 * 24 * time.Hour)
step := 24 * time.Hour

g := TimeSeq(start, end, step)
for _, t := range gen.GenerateN(g, 5) {
    fmt.Println(t.Format("2006-01-02"))
}
// prints 5 consecutive days, starting from today
fmt.Println(g.Generate().Format("2006-01-02"))
// prints 6th day after today

Implementing generators for custom types (the most basic approach)

Well, the most basic approach to create generators for custom types/structs is to create a dedicated struct which does so:

type Dog struct {
    Name, Breed string
}

type DogGen struct {
    // You can use field generators here, like a dedicated field that can generate Dog's name:
    // nameGen gen.Gen[string]
}

func (dg *DogGen) Generate() Dog {
    // implement the logic here
}

But there are more elegant ways to do so!

Composition

Being able to generate simple values is not just enough, imagine given a struct below:

type Person struct {
    Name string
    Age  int
}

It's not much convenient to create a person generator struct, and implement the functions and the logic from scratch. We should be able to compose already-existing generators to get a new one. In gen, there are two ways of doing this:

  1. Putting a small effort and create them using functions.
  2. Rely on reflection, and gen does the trick for you.

Both of them would work, the first approach might take a little bit of coding and functional programming involved, but surely it's worth the safety. Let's take a look at an example of each of them.

Note that the Person struct here is a simple struct, the case might be different in your codebase!

Safe way to compose generators

So given the Person struct as above, we can create the person generator as follows, with FlatMap and Map functions:

nameGen := gen.OneOf("Bob", "April")
ageGen := gen.Between(10, 90)
personGen := gen.FlatMap(nameGen, func (name string) gen.Gen[Person] {
    return gen.Map(ageGen, func (age int) Person {
        return Person{name, age}
    })
})

That's all! Using this approach, you're manually designing the behavior of the generator, without creating a dedicated struct for it. FlatMap is basically the bind or flatMap (Monad) function in FP languages (if you're familiar with FP), while Using is basically the Map function (Functor). As the number of fields in the struct grow, it becomes harder to use FlatMap and Map, and it becomes slower. Luckily, there's another functional and safe alternative, which is incredibly faster, and easier to use.

MapN

MapN functions basically abstract away the usage of FlatMap, and since there are less function calls, it's also faster. Given the Programmer struct below, you can compare both approaches in terms of readability. There are also benchmarks which demonstrate how faster this approach is rather than both FlatMap/Map, and using generator structs.

type Programmer struct {
    Name       string
    Surname    string
    GithubUrl  string
    FavLang    string
    Origin     string
    Age        int
    Experiance int
}

// More readable and faster functional approach using MapN
var functional2 Gen[Programmer] = Map7(
    nameGen, surnameGen, gitGen, langGen, originGen, ageGen, experienceGen,
    func(name, surname, git, lang, origin string, age, experience int) Programmer {
        return Programmer{name, surname, git, lang, origin, age, experience}
    },
)

// Basic functional approach using FlatMap and Map
var functional1 Gen[Programmer] = FlatMap(nameGen, func(name string) Gen[Programmer] {
    return FlatMap(surnameGen, func(surname string) Gen[Programmer] {
        return FlatMap(gitGen, func(git string) Gen[Programmer] {
            return FlatMap(langGen, func(lang string) Gen[Programmer] {
                return FlatMap(originGen, func(origin string) Gen[Programmer] {
                    return FlatMap(ageGen, func(age int) Gen[Programmer] {
                        return Map(experienceGen, func(experience int) Programmer {
                            return Programmer{
                                name, surname, git, lang, origin, age, experience,
                            }
                        })
                    })
                })
            })
        })
    })
})

As you can guess, the N in MapN denotes the number of generators you want to use. The types of variables used in the compose function must be respectively the same types as the given generators in order.

Unsafe yet easy way to compose generators

Given the same scenario above, you can provide the base generators, and use the Infer function:

nameGen := gen.OneOf("Bob", "April")
ageGen := gen.Between(10, 90)
personGen := gen.Infer[Person](
    gen.Wrap(nameGen), gen.Wrap(ageGen),
)

Notice that you have to use Wrap, because unfortunately, go does not yet support wildcards for generic types. It may seem more convenient than the first approach, so let's compare the two of them.

Safe approach vs Unsafe approach

1- The first downside to the unsafe approach is that you cannot take the full control of the generation logic, first, because it depends on reflection, and also, it depends on the types of generators. Say our Person struct looked a bit different:

type Perosn struct {
    Name    string
    Age     int
    Surname string
}

nameGen := gen.OneOf("Bob", "April")
ageGen := gen.Between(10, 90)
surnameGen := gen.Only("Potter")

personGen := gen.Infer[Person](
    gen.Wrap(nameGen), gen.Wrap(ageGen), gen.Wrap(surnameGen),
)

In this case, because the Infer function relies on the types of the generators, it uses the last generator of string, to generate both name ans the surname! while in the first approach, you're the one who rules!

nameGen := gen.OneOf("Bob", "April")
ageGen := gen.Between(10, 90)

personGen := gen.FlatMap(nameGen, func (name string) gen.Gen[Person] {
    return gen.Map(ageGen, func (age int) Person {
        return Person{name, age, "Potter"}
    })
})

2- In the current version of the library, some types are not yet supported, like functions!

Here's also a benchmark of these 2, using the same Person struct:

goos: darwin
goarch: arm64
pkg: gen
BenchmarkComposition/gen-composition-8         	 6094166	       164.6 ns/op	     192 B/op	       6 allocs/op
BenchmarkComposition/gen-infered-composition-8 	  582614	        1979 ns/op	    1699 B/op	     107 allocs/op

Arbitrary Values

Generating arbitrary values is so common, that gen already has some arbitrary generators for most-common language types. There are arbitrary generators for these types:

int types, uint types, float types, rune and strings

They're caleld Arbitrary followed by their type name (e.g., ArbitraryUint32).

Randomness

Gen uses math/rand to arbitrarily create random values under the hood, so it also makes sense if you could take control of that random value. You can use the Seed function to seed the random generator:

gen.Seed(int64(6897235))

Gen uses current unix millis by default.

Generating multiple values

There's a function in the gen package called GenerateN, which given a generator, and an unsigned integer, it would generate a slice of values which the generator can generate, with the length of the given integer:

var personGen gen.Gen[Person]
var persons []Person = gen.GenerateN(personGen, 100) // a slice of 100 persons

Benchmarks

There are several benchmarks, some of them compare gen.Gen with quick.Generator, some of them compare different approaches to the same goal in gen, and there's also a pretty good coverage of default generators. You can take a look at gen_test.go for the implementations:

goos: darwin
goarch: arm64
pkg: github.com/AminMal/gen
gen-only-8                                         1000000000               0.9521 ns/op
quick-only-8                                        301776836                3.968 ns/op
gen-between-8                                        79958463                14.37 ns/op
quick-between-8                                      81586863                14.35 ns/op
gen-one-of-8                                       1000000000               1.005 ns/op
quick-one-of-8                                       73754053                15.98 ns/op
gen-composition-functional-8                          7461547               162.5 ns/op
gen-infered-composition-8                              588592              2006 ns/op
gen-composition-8                                    95243446                12.80 ns/op
quick-composition-8                                  12520965                91.13 ns/op
gen-composition-7-fields-8                           29447280                43.28 ns/op
gen-composition-map/flatmap-7-fields-8                2195730               543.0 ns/op
gen-composition-mapN-7-fields-8                      20983605                56.79 ns/op
quick-composition-7-fields-8                          7518525               159.6 ns/op

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func GenerateN added in v0.2.0

func GenerateN[T any](g Gen[T], n uint) []T

func Seed

func Seed(seed int64)

Seed the random generator of the package

Types

type Gen

type Gen[T any] interface {
	// Generate generates a single value of type `T`
	Generate() T
}

Gen describes how to generate a value of a specific type `T`. The behavior of the Gen only depends on the structs implementing it.

var ArbitraryFloat32 Gen[float32] = Between(float32(math.MinInt32)/2+1, float32(math.MaxInt32)/2-1)

------ float types ------ ArbitraryFloat32 is an arbitrary float32 generator within float32 min value / 2 and float32 max value / 2

var ArbitraryFloat64 Gen[float64] = Between(float64(math.MinInt64)/2+1, float64(math.MaxInt64)/2-1)

ArbitraryFloat64 is an arbitrary float64 generator within float64 min value / 2 and float32 max value / 2

var ArbitraryInt Gen[int] = Between((math.MinInt/2 + 1), (math.MaxInt/2 - 1))

------ int types ------ ArbitraryInt is an arbitrary int generator within int min value / 2 and int max value / 2

var ArbitraryInt32 Gen[int32] = Between(int32(math.MinInt32)/2+1, int32(math.MaxInt32)/2-1)

ArbitraryInt32 is an arbitrary int32 generator within int32 min value / 2 and int32 max value / 2

var ArbitraryInt64 Gen[int64] = Between(int64(math.MinInt64)/2+1, int64(math.MaxInt64)/2-1)

ArbitraryInt64 is an arbitrary int64 generator within int64 min value / 2 and int64 max value / 2

var ArbitraryRune Gen[rune] = ArbitraryInt32

------ rune ------ ArbitraryRune is an arbitrary rune generator.

var ArbitraryUint Gen[uint] = Between(uint(0), uint(math.MaxUint))

------ uint types ------ ArbitraryUint is an arbitrary uint generator within 0 and uint max value

var ArbitraryUint16 Gen[uint16] = Between(uint16(0), uint16(math.MaxUint16))

ArbitraryUint16 is an arbitrary uint16 generator within 0 and uint16 max value

var ArbitraryUint32 Gen[uint32] = Between(uint32(0), uint32(math.MaxUint32))

ArbitraryUint32 is an arbitrary uint32 generator within 0 and uint32 max value

var ArbitraryUint64 Gen[uint64] = Between(uint64(0), uint64(math.MaxUint64))

ArbitraryUint64 is an arbitrary uint64 generator within 0 and uint64 max value

var ArbitraryUint8 Gen[uint8] = Between(uint8(0), uint8(math.MaxUint8))

ArbitraryUint8 is an arbitrary uint8 generator within 0 and uint8 max value

func Between

func Between[T Numeric](min, max T) Gen[T]

Between generates values within the given range. The order of the parameters doesn't actually matter, but it's more convenient to pass them properly. If max equals min, it returns an Only generator

func FlatMap

func FlatMap[T any, K any](gen Gen[T], flatMapFunc func(T) Gen[K]) Gen[K]

FlatMap creates a flattened lazy generator given the base generator as `gen`, and a bind function.

func Infer

func Infer[T any](valueGenerators ...*WrappedGen) (Gen[T], error)

Infer can infer generators for the given type parameter `T`, using the given wrapped generators. In case if the type T contains functions or types that gen's adhoc does not currently support, it returns an error, and if not, it returns the generator.

func Map

func Map[T any, K any](gen Gen[T], compositionAction func(T) K) Gen[K]

Map creates a lazy generator, which when it's Generate method is invoked, it does the composition action on generated value by gen.

func Map10 added in v0.3.0

func Map10[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, K any](
	g1 Gen[T1], g2 Gen[T2], g3 Gen[T3], g4 Gen[T4], g5 Gen[T5], g6 Gen[T6], g7 Gen[T7],
	g8 Gen[T8], g9 Gen[T9], g10 Gen[T10], compose func(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10) K,
) Gen[K]

Map10 takes 10 generators, and a composition action, and returns a generator which when invoked, will use the composition action and the given generators to generate new values

func Map11 added in v0.3.1

func Map11[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, K any](
	g1 Gen[T1], g2 Gen[T2], g3 Gen[T3], g4 Gen[T4], g5 Gen[T5], g6 Gen[T6], g7 Gen[T7],
	g8 Gen[T8], g9 Gen[T9], g10 Gen[T10], g11 Gen[T11], compose func(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11) K,
) Gen[K]

Map11 takes 11 generators, and a composition action, and returns a generator which when invoked, will use the composition action and the given generators to generate new values

func Map12 added in v0.3.1

func Map12[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, K any](
	g1 Gen[T1], g2 Gen[T2], g3 Gen[T3], g4 Gen[T4], g5 Gen[T5], g6 Gen[T6], g7 Gen[T7],
	g8 Gen[T8], g9 Gen[T9], g10 Gen[T10], g11 Gen[T11], g12 Gen[T12],
	compose func(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12) K,
) Gen[K]

Map12 takes 12 generators, and a composition action, and returns a generator which when invoked, will use the composition action and the given generators to generate new values

func Map13 added in v0.3.1

func Map13[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, K any](
	g1 Gen[T1], g2 Gen[T2], g3 Gen[T3], g4 Gen[T4], g5 Gen[T5], g6 Gen[T6], g7 Gen[T7],
	g8 Gen[T8], g9 Gen[T9], g10 Gen[T10], g11 Gen[T11], g12 Gen[T12], g13 Gen[T13],
	compose func(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13) K,
) Gen[K]

Map13 takes 13 generators, and a composition action, and returns a generator which when invoked, will use the composition action and the given generators to generate new values

func Map14 added in v0.3.1

func Map14[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, K any](
	g1 Gen[T1], g2 Gen[T2], g3 Gen[T3], g4 Gen[T4], g5 Gen[T5], g6 Gen[T6], g7 Gen[T7],
	g8 Gen[T8], g9 Gen[T9], g10 Gen[T10], g11 Gen[T11], g12 Gen[T12], g13 Gen[T13], g14 Gen[T14],
	compose func(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14) K,
) Gen[K]

Map14 takes 14 generators, and a composition action, and returns a generator which when invoked, will use the composition action and the given generators to generate new values

func Map15 added in v0.3.1

func Map15[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, K any](
	g1 Gen[T1], g2 Gen[T2], g3 Gen[T3], g4 Gen[T4], g5 Gen[T5], g6 Gen[T6], g7 Gen[T7],
	g8 Gen[T8], g9 Gen[T9], g10 Gen[T10], g11 Gen[T11], g12 Gen[T12], g13 Gen[T13], g14 Gen[T14], g15 Gen[T15],
	compose func(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15) K,
) Gen[K]

Map15 takes 15 generators, and a composition action, and returns a generator which when invoked, will use the composition action and the given generators to generate new values

func Map2 added in v0.3.0

func Map2[T1 any, T2 any, K any](g1 Gen[T1], g2 Gen[T2], compose func(T1, T2) K) Gen[K]

Map2 takes 2 generators, and a composition action, and returns a generator which when invoked, will use the composition action and the given generators to generate new values

func Map3 added in v0.3.0

func Map3[T1 any, T2 any, T3 any, K any](g1 Gen[T1], g2 Gen[T2], g3 Gen[T3], compose func(T1, T2, T3) K) Gen[K]

Map3 takes 3 generators, and a composition action, and returns a generator which when invoked, will use the composition action and the given generators to generate new values

func Map4 added in v0.3.0

func Map4[T1, T2, T3, T4, K any](
	g1 Gen[T1], g2 Gen[T2], g3 Gen[T3], g4 Gen[T4], compose func(T1, T2, T3, T4) K,
) Gen[K]

Map4 takes 4 generators, and a composition action, and returns a generator which when invoked, will use the composition action and the given generators to generate new values

func Map5 added in v0.3.0

func Map5[T1, T2, T3, T4, T5, K any](
	g1 Gen[T1], g2 Gen[T2], g3 Gen[T3], g4 Gen[T4], g5 Gen[T5], compose func(T1, T2, T3, T4, T5) K,
) Gen[K]

Map5 takes 5 generators, and a composition action, and returns a generator which when invoked, will use the composition action and the given generators to generate new values

func Map6 added in v0.3.0

func Map6[T1, T2, T3, T4, T5, T6, K any](
	g1 Gen[T1], g2 Gen[T2], g3 Gen[T3], g4 Gen[T4], g5 Gen[T5], g6 Gen[T6], compose func(T1, T2, T3, T4, T5, T6) K,
) Gen[K]

Map6 takes 6 generators, and a composition action, and returns a generator which when invoked, will use the composition action and the given generators to generate new values

func Map7 added in v0.3.0

func Map7[T1, T2, T3, T4, T5, T6, T7, K any](
	g1 Gen[T1], g2 Gen[T2], g3 Gen[T3], g4 Gen[T4], g5 Gen[T5], g6 Gen[T6], g7 Gen[T7], compose func(T1, T2, T3, T4, T5, T6, T7) K,
) Gen[K]

Map7 takes 7 generators, and a composition action, and returns a generator which when invoked, will use the composition action and the given generators to generate new values

func Map8 added in v0.3.0

func Map8[T1, T2, T3, T4, T5, T6, T7, T8, K any](
	g1 Gen[T1], g2 Gen[T2], g3 Gen[T3], g4 Gen[T4], g5 Gen[T5], g6 Gen[T6],
	g7 Gen[T7], g8 Gen[T8], compose func(T1, T2, T3, T4, T5, T6, T7, T8) K,
) Gen[K]

Map8 takes 8 generators, and a composition action, and returns a generator which when invoked, will use the composition action and the given generators to generate new values

func Map9 added in v0.3.0

func Map9[T1, T2, T3, T4, T5, T6, T7, T8, T9, K any](
	g1 Gen[T1], g2 Gen[T2], g3 Gen[T3], g4 Gen[T4], g5 Gen[T5], g6 Gen[T6], g7 Gen[T7],
	g8 Gen[T8], g9 Gen[T9], compose func(T1, T2, T3, T4, T5, T6, T7, T8, T9) K,
) Gen[K]

Map9 takes 9 generators, and a composition action, and returns a generator which when invoked, will use the composition action and the given generators to generate new values

func OneOf

func OneOf[T any](values ...T) Gen[T]

OneOf picks out a value among those values that it's given. If the values contain only one element, it returns an Only generator.

func Only

func Only[T any](value T) Gen[T]

Only can generate only the value it's given.

func Pure added in v0.2.2

func Pure[T any](generator func() T) Gen[T]

Pure is the most basic Gen type-class constructor, which returns a T generator given the generate function

func Sequential

func Sequential[T Numeric](from, to, step T) Gen[T]

Sequential is a sequential generator that holds the current state of the generator. It will generate numerics, between `from` and `to` (inclusive), with the given `step` size.

func StringGen

func StringGen(alphabet string, minLength uint, maxLength uint) Gen[string]

StringGen is a string generator that generates random strings using the given alphabet and minLength and maxLength.

func TimeBetween

func TimeBetween(start time.Time, end time.Time) Gen[time.Time]

TimeBetween is a generator for `time.Time` that will generate random `time.Time`s between the given start and end.

func TimeSeq

func TimeSeq(from, to time.Time, step time.Duration) Gen[time.Time]

TimeSeq is a sequential time generator, it generates `time.Time`s within the given range and step.

type Numeric

type Numeric interface {
	uint8 | uint16 | uint32 | uint64 | uint | int8 | int16 | int32 | int64 | int | float32 | float64
}

Numeric represents numeric types constraint.

type WrappedGen

type WrappedGen struct {
	// contains filtered or unexported fields
}

WrappedGen basically wraps `Gen`s to provide a generator that works with `reflect.Value`

func Wrap

func Wrap[T any](g Gen[T]) *WrappedGen

Wrap wraps around a `Gen` and returns a *WrappedGen.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL