type-aligned list generation

This document provides examples of how you can generate pseudorandom (but deterministic) type-aligned lists using maligned and Scalacheck.

prerequisites

The code examples assume that the following items have been imported:

import com.salesforce.maligned._
import com.salesforce.maligned.gen.TAListGen._
import cats.data.{Chain, Kleisli, Writer}
import cats.implicits._
import org.scalacheck.Arbitrary.arbitrary
import org.scalacheck.Gen
import org.scalacheck.rng.Seed

They also depend on the following helper method to sample a Scalacheck Gen instance with the provided RNG seed:

def sample[A](gen: Gen[A], seed: Seed): A = gen.apply(Gen.Parameters.default, seed).get

generating random type-aligned lists of functions

Importing TAListGen._ provides implicit Scalacheck Arbitrary instances for type-aligned lists (TANonEmptyList and TAList) of function (Function1) elements.

For example, we can create a generator for lists of length 1 to 6 with an input type of Int and an output type of Option[Double]:

val taListGen: Gen[TANonEmptyList[Function1, Int, Option[Double]]] =
  Gen.resize(6, arbitrary[TANonEmptyList[Function1, Int, Option[Double]]])

Let’s go ahead and sample a random list from this generator:

val exampleList: TANonEmptyList[Function1, Int, Option[Double]] = sample(taListGen, Seed(13L))

We can compose this list into a single function:

val f: Int => Option[Double] = exampleList.composeAll

f(3) should ===(Some(-8.952315155229148e-202))

But this doesn’t give us much insight into the internal structure of the list. Is it just a single element that is a function from Int to Option[Double]? We can see that it’s not by checking the length of the list:

exampleList.toList.size should ===(6)

To get a better understanding of the composition of the list, we’ll have to get a little fancier. We can wrap each function element in some logic to log its output. This will allow us to see all of the intermediate values of the composed functions.

val loggingFn1 = new FunctionK2[Function1, Kleisli[Writer[Chain[String], *], *, *]] {
  override def apply[A, B](f: A => B): Kleisli[Writer[Chain[String], *], A, B] =
    Kleisli { a =>
      val output = f(a)
      Writer(Chain(s"output: ${output.toString}"), output)
    }
}

val exampleLoggedList
  : TANonEmptyList[Kleisli[Writer[Chain[String], *], *, *], Int, Option[Double]] =
  exampleList.mapK(loggingFn1)

val fnWithLogging = exampleLoggedList.composeAll

fnWithLogging(3).run should ===(
  (
    Chain(
      "output: -1",
      "output: -1809610947",
      "output: List(-1, 1809634624, 2147483647, -53758633)",
      "output: -4249568764532995907",
      "output: -9223372036854775808",
      "output: Some(-8.952315155229148E-202)"
    ),
    Some(-8.952315155229148e-202)))

As you can see, the list was composed of functions of various types. Because the RNG is deterministic, if we run the function again with the same input value, we’ll get the same output. But if we change the input value, we’ll see completely different intermediate values.

fnWithLogging(3).run should ===(
  (
    Chain(
      "output: -1",
      "output: -1809610947",
      "output: List(-1, 1809634624, 2147483647, -53758633)",
      "output: -4249568764532995907",
      "output: -9223372036854775808",
      "output: Some(-8.952315155229148E-202)"
    ),
    Some(-8.952315155229148e-202)))

fnWithLogging(4).run should ===(
  (
    Chain(
      "output: 1929283752716877934",
      "output: 1",
      "output: List(160958044, 1601040997, -2147483648, -139952998, 1)",
      "output: 7163398877903029397",
      "output: 7118195307056659999",
      "output: Some(1.0051322294659681E109)"
    ),
    Some(1.0051322294659681e109)))

If we generate a new list, it will be comprised of completely different functions (with different inner input/output types):

val exampleList2: TANonEmptyList[Function1, Int, Option[Double]] =
  sample(taListGen, Seed(1000L))

val exampleLoggedList2
  : TANonEmptyList[Kleisli[Writer[Chain[String], *], *, *], Int, Option[Double]] =
  exampleList2.mapK(loggingFn1)

val fn2WithLogging = exampleLoggedList2.composeAll

fn2WithLogging(3).run should ===(
  (
    Chain(
      "output: 8106783551773268132",
      "output: 0",
      "output: 㷱襈鯡䐌",
      "output: Left(false)",
      "output: Some(9.600598437719758E264)"),
    Some(9.600598437719758e264)))
The source code for this page can be found here.