Skip to content

Instantly share code, notes, and snippets.

@loicknuchel
Last active August 24, 2017 14:25
Show Gist options
  • Select an option

  • Save loicknuchel/8a3ee51f09e9e8ad1cc40043eaaa5af9 to your computer and use it in GitHub Desktop.

Select an option

Save loicknuchel/8a3ee51f09e9e8ad1cc40043eaaa5af9 to your computer and use it in GitHub Desktop.
Lazy monad
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