All Downloads are FREE. Search and download functionalities are using the official Maven repository.

hedgehog.Gen.scala Maven / Gradle / Ivy

The newest version!
package hedgehog

import hedgehog.core._
import hedgehog.predef._

trait GenTOps extends MonadGenOps[Gen] {

  /**********************************************************************/
  // Combinators

  /**********************************************************************/
  // Combinators - Integral

  def int(range: Range[Int]): GenT[Int] =
    integral(range, _.toInt)

  def short(range: Range[Short]): GenT[Short] =
    integral(range, _.toShort)

  def long(range: Range[Long]): GenT[Long] =
    integral(range, identity)

  def byte(range: Range[Byte]): GenT[Byte] =
    integral(range, _.toByte)

  def char(lo: Char, hi: Char): GenT[Char] =
    long(Range.constant(lo.toLong, hi.toLong)).map(_.toChar)

  /**********************************************************************/
  // Combinators - Enumeration

  /**
   * Generates a random boolean.
   *
   * _This generator shrinks to 'False'._
   */
  def boolean: GenT[Boolean] =
    element1(false, true)

  /**********************************************************************/
  // Combinators - Fractional

  def double(range: Range[Double]): GenT[Double] =
    double_(range).shrink(Shrink.towardsFloat(range.origin, _))

  def double_(range: Range[Double]): GenT[Double] =
    Gen.generate((size, seed) => {
      val (x, y) = range.bounds(size)
      seed.chooseDouble(x, y)
    })

  /**********************************************************************/
  // Combinators - Choice

  /**
   * Trivial generator that always produces the same element.
   */
  def constant[A](x: => A): GenT[A] =
    GenT.GenApplicative.point(x)

  /**
   * Randomly selects one of the elements in the list.
   *
   * This generator shrinks towards the first element in the list.
   */
  def element1[A](x: A, xs: A*): GenT[A] =
    element(x, xs.toList)

  /**
   * Randomly selects one of the elements in the list.
   *
   * This generator shrinks towards the first element in the list.
   */
  def element[A](x: A, xs: List[A]): GenT[A] =
    int(Range.constant(0, xs.length)).map(i => (x :: xs)(i))

  /**
   * Randomly selects one of the elements in the list.
   *
   * This generator shrinks towards the first element in the list.
   *
   * WARNING: This may throw an exception if the list is empty,
   * please use one of the other `element` variants if possible
   */
  def elementUnsafe[A](xs: List[A]): GenT[A] =
    xs match {
      case Nil =>
        sys.error("element: used with empty list")
      case h :: t =>
        element(h, t)
    }

  /**
   * Randomly selects one of the generators in the list.
   *
   * This generator shrinks towards the first generator in the list.
   */
  def choice1[A](x: GenT[A], xs: GenT[A]*): GenT[A] =
    choice(x, xs.toList)

  /**
   * Randomly selects one of the generators in the list.
   *
   * This generator shrinks towards the first generator in the list.
   */
  def choice[A](x: GenT[A], xs: List[GenT[A]]): GenT[A] =
    int(Range.constant(0, xs.length)).flatMap(i => (x :: xs)(i))

  /**
   * Uses a weighted distribution to randomly select one of the generators in the list.
   *
   * This generator shrinks towards the first generator in the list.
   */
   def frequency1[A](a: (Int, GenT[A]), l: (Int, GenT[A])*): GenT[A] =
     frequency(a, l.toList)

   /**
    * Uses a weighted distribution to randomly select one of the generators in the list.
    *
    * This generator shrinks towards the first generator in the list.
    */
   def frequency[A](a: (Int, GenT[A]), l: List[(Int, GenT[A])]): GenT[A] = {
     @annotation.tailrec
     def pick(n: Long, x: (Int, GenT[A]), xs: List[(Int, GenT[A])]): GenT[A] =
       if (n <= x._1)
         x._2
       else
         xs match {
           case Nil =>
             sys.error("Invariant: frequency hits an impossible code path")
           case h :: t =>
             pick(n - x._1, h, t)
         }
     val hasNonPositive = (a :: l).exists {
       case (weight, _) => weight <= 0
     }
     if (hasNonPositive) sys.error("Invariant: a non-positive weight was given")
     val total = (a :: l).map(_._1.toLong).sum
     for {
       n <- long(Range.constant(1, total))
       x <- pick(n, a, l)
     } yield x
   }

  /**
    * Uses a weighted distribution to randomly select one of the generators in the list.
    *
    * This generator shrinks towards the first generator in the list.
    *
    * WARNING: This may throw an exception if the list is empty,
    * please use one of the other `frequency` variants if possible.
    */
  def frequencyUnsafe[A](xs: List[(Int, GenT[A])]): GenT[A] =
    xs match {
      case Nil =>
        sys.error("frequency: used with empty list")
      case h :: t =>
        frequency(h, t)
    }
}

trait MonadGenOps[M[_]] {

  /**********************************************************************/
  // Combinators

  /**
   * Runs a `Option` generator until it produces a `Some`.
   *
   * This is implemented using `filter` and has the same caveats.
   */
  def fromSome[A](gen: M[Option[A]])(implicit F: Monad[M], G: MonadGen[M]): M[A] =
    F.map(filter(gen)(_.isDefined))(
      _.getOrElse(sys.error("fromSome: internal error, unexpected None"))
    )

  /**
   * Construct a generator that depends on the size parameter.
   */
  def generate[A](f: (Size, Seed) => (Seed, A))(implicit G: MonadGen[M]): M[A] =
    G.lift(GenT((size, seed) => {
      val (s2, a) = f(size, seed)
      Tree.TreeApplicative.point((s2, some(a)))
    }))

  /** Generates a list using a 'Range' to determine the length. */
  def list[A](gen: M[A], range: Range[Int])(implicit F: Monad[M], G: MonadGen[M]): M[List[A]] =
    sized(size =>
      ensure(
        G.shrink(
          F.bind(integral_(range, _.toInt))(k => replicateM[M, A](k, gen))
        , Shrink.list
        )
      , Range.atLeast(range.lowerBound(size), _)
      )
    )

  /**********************************************************************/
  // Combinators - Size

  /**
   * Construct a generator that depends on the size parameter.
   */
  def sized[A](f: Size => M[A])(implicit F: Monad[M], G: MonadGen[M]): M[A] =
    F.bind(generate((size, seed) => (seed, size)))(f)

  /**********************************************************************/
  // Combinators - Integral

  /**
   * Generates a random integral number in the given `[inclusive,inclusive]` range.
   *
   * When the generator tries to shrink, it will shrink towards the
   * [[Range.origin]] of the specified [[Range]].
   *
   * For example, the following generator will produce a number between `1970`
   * and `2100`, but will shrink towards `2000`:
   *
   * {{{
   * Gen.integral(Range.constantFrom(2000, 1970, 2100))
   * }}}
   *
   * Some sample outputs from this generator might look like:
   *
   * {{{
   * === Outcome ===
   * 1973
   * === Shrinks ===
   * 2000
   * 1987
   * 1980
   * 1976
   * 1974
   *
   * === Outcome ===
   * 2061
   * === Shrinks ===
   * 2000
   * 2031
   * 2046
   * 2054
   * 2058
   * 2060
   * }}}
   */
  def integral[A : Integral](range: Range[A], fromLong : Long => A)(implicit F: MonadGen[M]): M[A] =
    F.shrink(integral_[A](range, fromLong), Shrink.towards(range.origin, _))

  /**
   * Generates a random integral number in the `[inclusive,inclusive]` range.
   *
   * ''This generator does not shrink.''
   */
  def integral_[A](range: Range[A], fromLong : Long => A)(implicit G: MonadGen[M], I: Integral[A]): M[A] =
    generate((size, seed) => {
      val (x, y) = range.bounds(size)
      val (s2, a) = seed.chooseLong(I.toLong(x), I.toLong(y))
      (s2, fromLong(a))
    })

  /**********************************************************************/
  // Combinators - Conditional

  /**
   * Discards the whole generator.
   */
  def discard[A](implicit G: MonadGen[M]): M[A] =
    G.lift(
      GenT((_, seed) => Tree.TreeApplicative.point((seed, None)))
    )

  /**
   * Generates a value that satisfies a predicate.
   *
   * We keep some state to avoid looping forever.
   * If we trigger these limits then the whole generator is discarded.
   */
  def filter[A](gen: M[A])(p: A => Boolean)(implicit F: Monad[M], G: MonadGen[M]): M[A] = {
    def try_(k: Int): M[A] =
      if (k > 100)
        discard
      else
        F.bind(G.scale(gen, s => Size(2 * k + s.value)))(x =>
          if (p(x))
            F.point(x)
          else
            try_(k + 1)
        )
    try_(0)
  }

  /**
   * Discards the generator if the generated value does not satisfy the predicate.
   */
  def ensure[A](gen: M[A], p: A => Boolean)(implicit F: Monad[M], G: MonadGen[M]): M[A] =
    F.bind(gen)(x => if (p(x)) F.point(x) else discard)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy