Skip to content

Instantly share code, notes, and snippets.

@afsalthaj
Last active March 21, 2018 11:47
Show Gist options
  • Save afsalthaj/4e480a01be3f96abcbf59098a06d09a7 to your computer and use it in GitHub Desktop.
Save afsalthaj/4e480a01be3f96abcbf59098a06d09a7 to your computer and use it in GitHub Desktop.
package com.telstra.dx.iot.bin.generator
import org.scalacheck.Gen
import scala.annotation.tailrec
import scalaz.{-\/, \/, \/-}
import scalaz.syntax.either._
import scalaz.syntax.std.boolean._
// An algebra for a stateful data generation. More or less scalaz.State but not exactly, but focussed on
// just generating data + stack safe!
// Example: GeneratorService[CustomerData, CustomerData].run{ identity } { _.salary > 20 } { println(_).right[Throwable] }
// More at the end of this gist!
trait GeneratorService[A, B]{ self =>
def zero: B
def nextValue: A => B
// In this way you can start with a generator service for a single component and chain across
def map[C](f: B => C): GeneratorService[A, C] =
new GeneratorService[A, C] {
def zero: C = f(self.zero)
def nextValue: (A) => C = a => f(self.nextValue(a))
}
def flatMap[C](f: B => GeneratorService[A, C]): GeneratorService[A, C] =
new GeneratorService[A, C] {
def zero: C = f(self.zero).zero
def nextValue: (A) => C = a => f(self.nextValue(a)).nextValue(a)
}
def map2[C, D](b: GeneratorService[A, C])(f: (B, C) => D): GeneratorService[A, D] =
self.flatMap(bb => b.map(cc => f(bb, cc)))
def run[E](f: B => A)(stopCondition: B => Boolean)(callBack: B => E \/ Unit) =
GeneratorService.run(this)(f)(stopCondition)(callBack)
}
object GeneratorService {
def apply[A, B](implicit instance: GeneratorService[A, B]): GeneratorService[A, B] =
instance
def unit[A, B](b: => B): GeneratorService[A, B] =
new GeneratorService[A, B] {
def zero: B = b
def nextValue: (A) => B = _ => b
}
def sequence[A, B](x: List[GeneratorService[A, B]]): GeneratorService[A, List[B]] =
x.foldLeft(GeneratorService.unit[A, List[B]](List[B]()))((acc, a) => a.map2(acc)(_ :: _))
@tailrec
private def unfold[S, E](z: S)(f: S => Option[S])(sideEffect: S => E \/ Unit): E \/ Unit = {
sideEffect(z) match {
case \/-(_) => f(z) match {
case Some(n) => unfold[S, E](n)(f)(sideEffect)
case None => ().right[E]
}
case -\/(r) => r.left[Unit]
}
}
def run[A, B, E](gen: GeneratorService[A, B])(f: B => A)(stopCondition: B => Boolean)(callBack: B => E \/ Unit): E \/ Unit =
unfold(gen.zero)(b => {
val nn = gen.nextValue(f(b))
(! stopCondition(nn)).option(nn)
})(callBack)
object Laws {
// A basic check the above monad makes sense.. Well not convinced though!
// Implicits defined based on what monad.laws required..(Ex: Int, and Int => Int)
import scalaz._, Scalaz._, scalacheck.ScalazProperties._
implicit val monadGenerator: Monad[GeneratorService[Int, ?]] = new scalaz.Monad[GeneratorService[Int, ?]] {
override def bind[A, B](fa: GeneratorService[Int, A])(f: (A) => GeneratorService[Int, B]): GeneratorService[Int, B] = {
fa.flatMap(f)
}
override def point[A](a: => A): GeneratorService[Int, A] = GeneratorService.unit[Int, A](a)
}
implicit def arbitraryGeneratorService: org.scalacheck.Arbitrary[GeneratorService[Int, Int => Int]] =
org.scalacheck.Arbitrary {
for {
t <- Gen.posNum[Int]
} yield new GeneratorService[Int, Int => Int] {
override def zero: Int => Int = _ => t
override def nextValue: (Int) => Int => Int = _ => _ => t
}
}
implicit def arbitraryGeneratorService1: org.scalacheck.Arbitrary[GeneratorService[Int, Int]] =
org.scalacheck.Arbitrary {
for {
t <- Gen.posNum[Int]
} yield new GeneratorService[Int, Int] {
override def zero: Int = t
override def nextValue: (Int) => Int = _ => t
}
}
implicit def arbitraryGeneratorService2 = new scalaz.Equal[GeneratorService[Int, Int]]{
override def equal(a1: GeneratorService[Int, Int], a2: GeneratorService[Int, Int]): Boolean = {
a1.zero == a2.zero && a1.nextValue(a1.zero) == a2.nextValue(a1.zero)
}
}
// Run in console to check this
def isMonad = monad.laws[GeneratorService[Int, ?]].check
}
}
// HOW TO USE THIS LIBRARY
trait GeneratorServiceInstances0 {
implicit object `Instance For CostSavingData` extends GeneratorService[Int, Int] {
override def zero: Int = 0
override def nextValue: Int => Int = c => {
if (c == 25)
c + 2
else
c + 1
}
}
}
object GeneratorServiceInstances extends GeneratorServiceInstances0
import GeneratorServiceInstances._
// Generates integers based on the above rule (that involves checking every previous integer values)
// and stops the generation if it is greater than 100. During every generation, it prints to the console/ or it could be sending
// to a database. This is almost similar to scalaz state monad, except that it is stack safe and more intuitive to compose.
GeneratorService[Int, Int].map(_ + 20).run { identity } { _ > 100 } { println(_).right[Throwable] }
// the above one is also equivalent to the below code base. Likewise you can do any sort of combinations. You get the point
GeneratorService[Int, Int].map(_ + 20).flatMap (t => GeneratorService.unit[Int, (Int, Unit)]((t, println(t)))).run{t => t._1}{_._1 > 100 }{_ => ().right[Throwable]}
// Sometimes you need to generate data based on some config but the config will be passed only at the very edge of the program. In that case,
// Functional Programmers tries to push concrete things, and application of client variables at the very end.
// For demo purpose, let the config be an optional startTime. (In real life the config could be event `Database Client`)
// Please note the type variables in GeneratorService trait.
// A represents the previous variable. B represents the nextVariable that depends on A.
// In this particular example, B is `Config => DateTime` coz the final value of DateTimes depends on the Config, and the previous Variable A is just `DateTime`.
// The previous variable needn't be `Config => DateTime` coz Config is a constant it never changes.
import org.joda.time.DateTime
final case class Config(startTime: Option[DateTime])
implicit object `Instance For CostSavingData` extends GeneratorService[DateTime, Config => DateTime] {
override def zero: Config => DateTime =
_.getOrElse(DateTime.now)
override def nextValue: DateTime => (Config => DateTime)
dateTime => (_ => {
dateTime.plusDays(1)
})
}
// At the edge
val config: Config = ???
GeneratorService[DateTime, Config => DateTime].map(configToDateTime => configToDateTime(config))
.run { identity } { _.isAfterNow } { t => println(t.toString).right[Throwable] }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment