Last active
March 21, 2018 11:47
-
-
Save afsalthaj/4e480a01be3f96abcbf59098a06d09a7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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