liveBook · Manning

Function generation

As explained in Functional Programming in Scala, Second Edition

A function generator produces functions whose outputs are determined by generated values and information derived from their inputs.

Function generation creates test inputs that are themselves functions. It matters when a property quantifies over a higher-order argument: for any list and predicate, every element returned by takeWhile should satisfy that predicate. Testing only hand-picked predicates leaves the testing framework unable to explore that argument space. section 8.3 section 8.6

A generator for output values can trivially produce a function that ignores its input: generate an Int, then return a String => Int that always returns it. Such constant functions are valid but weak test cases. For predicates, they amount to functions that always return true or always return false, which cannot probe where takeWhile stops. section 8.6

The essential move is to make each generated function deterministic while letting its input influence the generated result. First obtain private random information when constructing the function. Later, when the function receives an argument, derive an RNG from that information and the argument, then run the output generator with the derived RNG. The same argument therefore gives the same result, while different arguments can select different generated values. section 8.6

From String to generated output

Given Gen[A], genStringFn produces Gen[String => A]. It draws random state once, returns a function, and has that function combine its String input with the captured random state before running Gen[A]. The output type remains fully generic, so the technique does not rely on manufacturing an A directly from a String. section 8.6

General function generation separates the two responsibilities. A Cogen[A] perturbs an RNG using an input A; a Gen[B] produces an output B from the perturbed RNG. Combining them yields Gen[A => B]. This avoids requiring a hash-like operation to return the output type and makes the input-dependence strategy explicit. section 8.6

def fn[A, B](in: Cogen[A], out: Gen[B]): Gen[A => B] =
  State[RNG, A => B]: rng =>
    val (seed, rng2) = rng.nextInt
    val f = (a: A) => out.run(in(a, rng2))(0)
    (f, rng2)

The generalized constructor shows how an input perturbation and an output generator combine into a generator of functions. section 8.6

Use function generation to strengthen higher-order properties:

  • Generate predicates for takeWhile rather than restricting tests to a single predicate such as isEven. section 8.3 section 8.6
  • Check both that the returned prefix satisfies the predicate and that the next remaining element, when present, fails it. section 8.6
  • Relate takeWhile and dropWhile by checking that their results concatenate to the original list. section 8.6

How Functional Programming in Scala, Second Edition covers it

How deep it goes
Develops a concrete RNG-based String-function construction, then generalizes it with Cogen and Gen.
Examples
Uses List.takeWhile predicates, constant String-to-Int functions, and a generic String-to-A generator.
What it assumes
Assumes familiarity with generators, RNG state, and function types while motivating function generation from a testing gap.
What you can do after
Build it from scratch by combining an input RNG perturbation with an output generator to produce deterministic functions.
How it connects
Connects property-based testing with higher-order functions, takeWhile, dropWhile, Gen, RNG, and Cogen.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Functional Programming in Scala, Second Edition. The section links point to the parts of the book it draws on.