Last active
August 24, 2017 14:25
-
-
Save loicknuchel/8a3ee51f09e9e8ad1cc40043eaaa5af9 to your computer and use it in GitHub Desktop.
Lazy monad
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
| import org.scalatest.concurrent.ScalaFutures | |
| import org.scalatest.{FunSpec, Matchers} | |
| import scala.concurrent.{ExecutionContext, Future} | |
| import scala.util.{Success, Try} | |
| class Lazy[A](block: => A) { | |
| private var evaluated = false | |
| private lazy val underlying: A = { | |
| evaluated = true | |
| block | |
| } | |
| def get: A = underlying | |
| def isEvaluated: Boolean = evaluated | |
| def map[B](f: A => B): Lazy[B] = Lazy(f(underlying)) | |
| def flatMap[B](f: A => Lazy[B]): Lazy[B] = Lazy(f(underlying).get) | |
| def flatten[B](implicit ev: A <:< Lazy[B]): Lazy[B] = Lazy(ev(underlying).get) | |
| def filter(p: A => Boolean): Lazy[A] = Lazy(if(p(underlying)) underlying else throw new NoSuchElementException("Lazy.filter: predicate does not hold for " + underlying)) | |
| def withFilter(p: A => Boolean): Lazy[A] = filter(p) | |
| def collect[B](pf: PartialFunction[A, B]): Lazy[B] = Lazy(pf.applyOrElse(underlying, (v: A) => throw new NoSuchElementException("Lazy.collect: predicate does not hold for " + v))) | |
| def compose[B](o: Lazy[B]): Lazy[A] = Lazy({o.get; underlying}) | |
| def sequence[B](o: Lazy[B]): Lazy[B] = Lazy({underlying; o.get}) | |
| def asTry: Try[A] = Try(underlying) | |
| def asFuture(implicit ec: ExecutionContext): Future[A] = Future(underlying) | |
| override def toString: String = | |
| if(isEvaluated) s"Lazy($underlying)" | |
| else "Lazy(not evaluated)" | |
| } | |
| object Lazy { | |
| def apply[A](block: => A): Lazy[A] = new Lazy(block) | |
| def sequence[A](in: Seq[Lazy[A]]): Lazy[Seq[A]] = Lazy(in.map(_.get)) | |
| def sequence[A](in: List[Lazy[A]]): Lazy[List[A]] = Lazy(in.map(_.get)) | |
| def lazily[A](f: => A): Lazy[A] = new Lazy(f) | |
| //implicit def eval[A](lazy: Lazy[A]): A = lazy.get | |
| } | |
| class LazySpec extends FunSpec with Matchers with ScalaFutures { | |
| describe("Lazy") { | |
| it("should not evaluate code on initialization") { | |
| var x = 0 | |
| val l = Lazy({ x += 1 }) | |
| x shouldBe 0 | |
| l.get | |
| x shouldBe 1 | |
| } | |
| it("should tell if it was evaluated") { | |
| val l = Lazy(1) | |
| l.isEvaluated shouldBe false | |
| l.get shouldBe 1 | |
| l.isEvaluated shouldBe true | |
| } | |
| it("should execute code only once") { | |
| var x = 0 | |
| val l = Lazy({ x += 1 }) | |
| x shouldBe 0 | |
| l.get | |
| x shouldBe 1 | |
| l.get | |
| x shouldBe 1 | |
| } | |
| it("should be able to chain lazy executions") { | |
| var x = 0 | |
| val l: Lazy[String] = Lazy({ x += 1; "a" }) | |
| val m: Lazy[String] = l.map(v => { x += 1; v + "b" }) | |
| x shouldBe 0 | |
| m.get shouldBe "ab" | |
| x shouldBe 2 | |
| } | |
| it("should unstack only one context at a time") { | |
| var x = 0 | |
| val l: Lazy[String] = Lazy({ x += 1; "a" }) | |
| val m: Lazy[Lazy[String]] = l.map(v => Lazy({ x += 1; v + "b" })) | |
| x shouldBe 0 | |
| val n: Lazy[String] = m.get | |
| x shouldBe 1 | |
| n.get shouldBe "ab" | |
| x shouldBe 2 | |
| } | |
| it("should be able to flatMap") { | |
| var x = 0 | |
| val l: Lazy[String] = Lazy({ x += 1; "a" }) | |
| x shouldBe 0 | |
| val m: Lazy[String] = l.flatMap(v => Lazy({ x += 1; v + "b" })) | |
| x shouldBe 0 | |
| m.get shouldBe "ab" | |
| x shouldBe 2 | |
| } | |
| it("should be able to flatten") { | |
| var x = 0 | |
| val l: Lazy[String] = Lazy({ x += 1; "a" }) | |
| x shouldBe 0 | |
| val m: Lazy[Lazy[String]] = l.map(v => Lazy({ x += 1; v + "b" })) | |
| x shouldBe 0 | |
| val n: Lazy[String] = m.flatten | |
| x shouldBe 0 | |
| n.get shouldBe "ab" | |
| x shouldBe 2 | |
| } | |
| it("should filter") { | |
| var x = 0 | |
| val l = Lazy({ x += 1; 1 }) | |
| val f1 = l.filter(_ > 0) | |
| val f2 = l.filter(_ < 0) | |
| x shouldBe 0 | |
| f1.asTry shouldBe Success(1) | |
| f2.asTry.isFailure shouldBe true | |
| x shouldBe 1 | |
| } | |
| it("should collect") { | |
| var x = 0 | |
| val l: Lazy[Int] = Lazy({ x += 1; 1 }) | |
| val c1 = l.collect { case v if v > 0 => v.toString } | |
| val c2 = l.collect { case v if v < 0 => v.toString } | |
| x shouldBe 0 | |
| c1.asTry shouldBe Success("1") | |
| c2.asTry.isFailure shouldBe true | |
| x shouldBe 1 | |
| x = 0 | |
| val m: Lazy[Option[Int]] = Lazy({ x += 1; Some(1) }) | |
| val c3 = m.collect { case Some(v) => v } | |
| val c4 = m.collect { case None => 2 } | |
| x shouldBe 0 | |
| c3.asTry shouldBe Success(1) | |
| c4.asTry.isFailure shouldBe true | |
| x shouldBe 1 | |
| } | |
| it("should compose Lazy") { | |
| var x = 0 | |
| val l: Lazy[String] = Lazy({ x += 1; "a" }) | |
| val m: Lazy[String] = l.compose(Lazy({ x += 1; 2 })) | |
| x shouldBe 0 | |
| m.get shouldBe "a" | |
| x shouldBe 2 | |
| } | |
| it("should sequence Lazy") { | |
| var x = 0 | |
| val l: Lazy[String] = Lazy({ x += 1; "a" }) | |
| val m: Lazy[Int] = l.sequence(Lazy({ x += 1; 2 })) | |
| x shouldBe 0 | |
| m.get shouldBe 2 | |
| x shouldBe 2 | |
| } | |
| it("should be converted into a Try") { | |
| val l1 = Lazy(1) | |
| l1.asTry shouldBe Success(1) | |
| val l2 = Lazy(throw new Exception("fail")) | |
| l2.asTry.isFailure shouldBe true | |
| } | |
| it("should be converted into a Future") { | |
| import scala.concurrent.ExecutionContext.Implicits.global | |
| val l1 = Lazy(1) | |
| whenReady(l1.asFuture) { result => | |
| result shouldBe 1 | |
| } | |
| val l2 = Lazy(throw new Exception("fail")) | |
| whenReady(l2.asFuture.failed) { err => | |
| err.getMessage shouldBe "fail" | |
| } | |
| } | |
| it("should toString") { | |
| val l = Lazy(1) | |
| l.toString shouldBe "Lazy(not evaluated)" | |
| l.get | |
| l.toString shouldBe "Lazy(1)" | |
| } | |
| it("should sequence a Lazy Seq") { | |
| var x = 0 | |
| val list = List(Lazy({ x += 1; 1 }), Lazy({ x += 1; 2 }), Lazy({ x += 1; 3 })) | |
| x shouldBe 0 | |
| val seq = Lazy.sequence(list) | |
| x shouldBe 0 | |
| seq.get shouldBe List(1, 2, 3) | |
| x shouldBe 3 | |
| } | |
| it("should have syntaxic sugar") { | |
| import Lazy._ | |
| val l1 = lazily { 1 } | |
| l1.get shouldBe 1 | |
| } | |
| it("should comply to for-comprehension") { | |
| var x = 0 | |
| val res = for { | |
| r1 <- Lazy({x += 1; "a"}) | |
| r2 <- Lazy({x += 1; "b"}) | |
| r3 <- Lazy({x += 1; "c"}) if r2 == "b" | |
| } yield r1 + r2 + r3 | |
| x shouldBe 0 | |
| res.get shouldBe "abc" | |
| x shouldBe 3 | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment