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

com.stripe.brushfire.Reorder.scala Maven / Gradle / Ivy

package com.stripe.brushfire

import scala.math.Ordering
import scala.util.Random
import scala.util.hashing.MurmurHash3

/**
 * Simple data type that provides rules to order nodes during
 * traversal.
 *
 * In some cases subtypes of Reorder will also wraps RNG state, for
 * instances that need to randomly select instances. Thus, Reorder is
 * not guaranteed to be referentially-transparent. Fresh instances
 * should be used with each traversal.
 *
 * Reorder will also continue to recurse into a given structure using
 * a provided callback method.
 *
 * The reason that the node type is provided to `apply` (instead of
 * `Reorder`) has to do with how generic trees are specified. Since
 * TreeOps uses path-dependent types to specify node types, the
 * current design is a bit of a kludge that makes it easy to get the
 * types right when handling a Reorder instance to a TreeTraversal
 * instance (which is parameterized on a generic tree type).
 */
trait Reorder[A] {

  /**
   * Seed a reorder instance with a stable identifier.
   *
   * This ensures that reorders which are non-deterministic in general
   * (e.g. shuffled) will produce the same reordering for the same
   * seed across many traversals.
   *
   * On deterministic reorders this method is a noop.
   */
  def setSeed(seed: Option[String]): Reorder[A]

  /**
   * Perform a reordering.
   *
   * This method takes two nodes (`n1` and `n2`), as well as two
   * functions:
   *
   *  - `f`: function from node to identifying annotation
   *  - `g`: function from two nodes to a combined result
   *
   * The `f` function is used in cases where sorting or weighting is
   * necessary (in those cases A will be a weight or similar). The `g`
   * function is used to recurse on the result -- i.e. the possibly
   * reordered nodes `n1` and `n2` will be passed to `g` after the
   * reordering occurs.
   */
  def apply[N, S](n1: N, n2: N, f: N => A, g: (N, N) => S): S
}

object Reorder {

  /**
   * Reorder instance that traverses into the left node first.
   */
  def unchanged[A]: Reorder[A] =
    new UnchangedReorder()

  /**
   * Reorder instance that traverses into the node with the higher
   * weight first.
   */
  def weightedDepthFirst[A](implicit ev: Ordering[A]): Reorder[A] =
    new WeightedReorder()

  /**
   * Reorder instance that traverses into a random node first. Each
   * node has equal probability of being selected.
   */
  def shuffled[A](seed: Int): Reorder[A] =
    new ShuffledReorder(new Random(seed))

  /**
   * Reorder instance that traverses into a random node, but choose
   * the random node based on the ratio between its weight and the
   * total weight of both nodes.
   *
   * If the left node's weight was 10, and the right node's weight was
   * 5, the left node would be picked 2/3 of the time.
   */
  def probabilisticWeightedDepthFirst[A](seed: Int, conversion: A => Double): Reorder[A] =
    new ProbabilisticWeighted(new Random(seed), conversion)

  class UnchangedReorder[A] extends Reorder[A] {
    def setSeed(seed: Option[String]): Reorder[A] =
      this
    def apply[N, S](n1: N, n2: N, f: N => A, g: (N, N) => S): S =
      g(n1, n2)
  }

  class WeightedReorder[A](implicit ev: Ordering[A]) extends Reorder[A] {
    def setSeed(seed: Option[String]): Reorder[A] =
      this
    def apply[N, S](n1: N, n2: N, f: N => A, g: (N, N) => S): S =
      if (ev.compare(f(n1), f(n2)) >= 0) g(n1, n2) else g(n2, n1)
  }

  class ShuffledReorder[A](r: Random) extends Reorder[A] {
    def setSeed(seed: Option[String]): Reorder[A] =
      seed match {
        case Some(s) =>
          new ShuffledReorder(new Random(MurmurHash3.stringHash(s)))
        case None =>
          this
      }
    def apply[N, S](n1: N, n2: N, f: N => A, g: (N, N) => S): S =
      if (r.nextBoolean) g(n1, n2) else g(n2, n1)
  }

  class ProbabilisticWeighted[A](r: Random, conversion: A => Double) extends Reorder[A] {
    def setSeed(seed: Option[String]): Reorder[A] =
      seed match {
        case Some(s) =>
          new ProbabilisticWeighted(new Random(MurmurHash3.stringHash(s)), conversion)
        case None =>
          this
      }
    def apply[N, S](n1: N, n2: N, f: N => A, g: (N, N) => S): S = {
      val w1 = conversion(f(n1))
      val w2 = conversion(f(n2))
      val sum = w1 + w2
      if (r.nextDouble * sum < w1) g(n1, n2) else g(n2, n1)
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy