diff --git a/build.sbt b/build.sbt index 7a3f80f3..e508a1f5 100644 --- a/build.sbt +++ b/build.sbt @@ -101,6 +101,7 @@ lazy val pure = .dependsOn(core) .settings(sharedSettings) .settings(name := "minart-pure") + .settings(testSettings) .settings(publishSettings) .jsSettings(jsSettings) .nativeSettings(nativeSettings) diff --git a/pure/shared/src/main/scala/eu/joaocosta/minart/pure/CanvasIO.scala b/pure/shared/src/main/scala/eu/joaocosta/minart/pure/CanvasIO.scala index 2bda8c1e..ff68f164 100644 --- a/pure/shared/src/main/scala/eu/joaocosta/minart/pure/CanvasIO.scala +++ b/pure/shared/src/main/scala/eu/joaocosta/minart/pure/CanvasIO.scala @@ -5,45 +5,19 @@ import eu.joaocosta.minart.core._ /** * Representation of a canvas operation, with the common Monad operations. */ -sealed trait CanvasIO[+A] { - /** Runs this operation on a specified canvas. */ - def run(canvas: Canvas): A - /** Maps the result of this operation. */ - def map[B](f: A => B): CanvasIO[B] - /** Combines two operations by applying a function to the result of the first operation. */ - def flatMap[B](f: A => CanvasIO[B]): CanvasIO[B] = CanvasIO.FlatMap[A, B](this, f) - /** Combines two operations by discarding the result of the first operation. */ - def andThen[B](that: CanvasIO[B]): CanvasIO[B] = CanvasIO.FlatMap[A, B](this, _ => that) - /** Combines two operations by discarding the result of the second operation. */ - def andFinally[B](that: CanvasIO[B]): CanvasIO[A] = CanvasIO.FlatMap[A, A](this, x => that.as(x)) - /** Combines two operations by combining their results with the given function. */ - def zipWith[B, C](that: CanvasIO[B])(f: (A, B) => C): CanvasIO[C] = this.flatMap(x => that.map(y => f(x, y))) - /** Combines two operations by combining their results into a tuple. */ - def zip[B](that: CanvasIO[B]): CanvasIO[(A, B)] = this.zipWith(that)((x, y) => x -> y) - /** Changes the result of this operation to another value */ - def as[B](x: B): CanvasIO[B] = this.map(_ => x) - /** Changes the result of this operation unit */ - lazy val unit: CanvasIO[Unit] = this.as(()) -} - object CanvasIO { - private final case class Suspend[A](thunk: Canvas => A) extends CanvasIO[A] { - def run(canvas: Canvas): A = thunk(canvas) - def map[B](f: A => B): CanvasIO[B] = Suspend(thunk.andThen(f)) - } - private final case class FlatMap[A, B](io: CanvasIO[A], andThen: A => CanvasIO[B]) extends CanvasIO[B] { - def run(canvas: Canvas): B = andThen(io.run(canvas)).run(canvas) - def map[C](f: B => C): CanvasIO[C] = FlatMap[B, C](this, x => Suspend(_ => f(x))) - } /** An operation that does nothing **/ - val noop: CanvasIO[Unit] = Suspend(_ => ()) + val noop: CanvasIO[Unit] = RIO.noop /** Lifts a value into a [[CanvasIO]]. */ - def pure[A](x: A): CanvasIO[A] = Suspend[A](_ => x) + def pure[A](x: A): CanvasIO[A] = RIO.pure(x) + + /** Suspends a computation into a [[CanvasIO]]. */ + def suspend[A](x: => A): CanvasIO[A] = RIO.suspend(x) /** Store an unsafe canvas operation in a [[CanvasIO]]. */ - def accessCanvas[A](f: Canvas => A): CanvasIO[A] = Suspend[A](f) + def accessCanvas[A](f: Canvas => A): CanvasIO[A] = RIO.access[Canvas, A](f) /** Fetches the canvas settings. */ val getSettings: CanvasIO[Canvas.Settings] = accessCanvas(_.settings) diff --git a/pure/shared/src/main/scala/eu/joaocosta/minart/pure/RIO.scala b/pure/shared/src/main/scala/eu/joaocosta/minart/pure/RIO.scala new file mode 100644 index 00000000..8a81f428 --- /dev/null +++ b/pure/shared/src/main/scala/eu/joaocosta/minart/pure/RIO.scala @@ -0,0 +1,70 @@ +package eu.joaocosta.minart.pure + +import eu.joaocosta.minart.core._ + +/** + * Representation of an effectful operation, based on Haskell's RIO Monad. + */ +sealed trait RIO[-R, +A] { + /** Runs this operation */ + def run(resource: R): A + /** Maps the result of this operation. */ + def map[B](f: A => B): RIO[R, B] + /** Combines two operations by applying a function to the result of the first operation. */ + def flatMap[RR <: R, B](f: A => RIO[RR, B]): RIO[RR, B] = RIO.FlatMap[RR, A, B](this, f) + /** Combines two operations by discarding the result of the first operation. */ + def andThen[RR <: R, B](that: RIO[RR, B]): RIO[RR, B] = RIO.FlatMap[RR, A, B](this, _ => that) + /** Combines two operations by discarding the result of the second operation. */ + def andFinally[RR <: R, B](that: RIO[RR, B]): RIO[RR, A] = RIO.FlatMap[RR, A, A](this, x => that.as(x)) + /** Combines two operations by combining their results with the given function. */ + def zipWith[RR <: R, B, C](that: RIO[RR, B])(f: (A, B) => C): RIO[RR, C] = this.flatMap(x => that.map(y => f(x, y))) + /** Combines two operations by combining their results into a tuple. */ + def zip[RR <: R, B](that: RIO[RR, B]): RIO[RR, (A, B)] = this.zipWith(that)((x, y) => x -> y) + /** Changes the result of this operation to another value */ + def as[B](x: B): RIO[R, B] = this.map(_ => x) + /** Transforms the resource required by this operation */ + def contramap[RR](f: RR => R): RIO[RR, A] = RIO.Suspend[RR, A](res => this.run(f(res))) + /** Provides the required resource to this operation */ + def provide(res: R): RIO[Any, A] = this.contramap(_ => res) + /** Changes the result of this operation unit */ + lazy val unit: RIO[R, Unit] = this.as(()) +} + +object RIO { + private final case class Suspend[R, A](thunk: R => A) extends RIO[R, A] { + def run(resource: R): A = thunk(resource) + def map[B](f: A => B): RIO[R, B] = Suspend(thunk.andThen(f)) + } + private final case class FlatMap[R, A, B](io: RIO[R, A], andThen: A => RIO[R, B]) extends RIO[R, B] { + def run(resource: R): B = andThen(io.run(resource)).run(resource) + def map[C](f: B => C): RIO[R, C] = FlatMap[R, B, C](this, x => Suspend(_ => f(x))) + } + + /** An operation that does nothing **/ + val noop: RIO[Any, Unit] = Suspend[Any, Unit](_ => ()) + + /** Lifts a value into a [[RIO]]. */ + def pure[A](x: A): RIO[Any, A] = Suspend[Any, A](_ => x) + + /** Suspends a computation into a [[RIO]]. */ + def suspend[A](x: => A): RIO[Any, A] = Suspend[Any, A](_ => x) + + /** Returns a operation that requires some resource. */ + def access[R, A](f: R => A): RIO[R, A] = Suspend[R, A](f) + + /** Converts an `Iterable[RIO[R, A]]` into a `RIO[R, List[A]]`. */ + def sequence[R, A](it: Iterable[RIO[R, A]]): RIO[R, List[A]] = + access(res => it.map(_.run(res)).toList) + + /** Converts an `Iterable[RIO[R, A]]` into a `RIO[R, Unit]`. */ + def sequence_[R](it: Iterable[RIO[R, Any]]): RIO[R, Unit] = + access(res => it.foreach(_.run(res))) + + /** Converts an `Iterable[A]` into a `RIO[R, List[B]]` by applying an operation to each element. */ + def traverse[R, A, B](it: Iterable[A])(f: A => RIO[R, B]): RIO[R, List[B]] = + sequence(it.map(f)) + + /** Applies an operation to each element of a `Iterable[A]` and discards the result. */ + def foreach[R, A](it: Iterable[A])(f: A => RIO[R, Any]): RIO[R, Unit] = + sequence_(it.map(f)) +} diff --git a/pure/shared/src/main/scala/eu/joaocosta/minart/pure/package.scala b/pure/shared/src/main/scala/eu/joaocosta/minart/pure/package.scala new file mode 100644 index 00000000..a63a95a8 --- /dev/null +++ b/pure/shared/src/main/scala/eu/joaocosta/minart/pure/package.scala @@ -0,0 +1,8 @@ +package eu.joaocosta.minart + +import eu.joaocosta.minart.core.Canvas + +package object pure { + type CanvasIO[+A] = RIO[Canvas, A] +} + diff --git a/pure/shared/src/test/scala/eu/joaocosta/minart/pure/RIOSpec.scala b/pure/shared/src/test/scala/eu/joaocosta/minart/pure/RIOSpec.scala new file mode 100644 index 00000000..b1817796 --- /dev/null +++ b/pure/shared/src/test/scala/eu/joaocosta/minart/pure/RIOSpec.scala @@ -0,0 +1,52 @@ +package eu.joaocosta.minart.pure + +import org.specs2.mutable._ + +import eu.joaocosta.minart.core._ + +class RIOSpec extends Specification { + "A RIO" should { + "store pure results" in { + RIO.pure(1).run(()) === 1 + } + + "suspend computations" in { + var hasRun: Boolean = false + val io = RIO.suspend({ hasRun = true }) + hasRun === false + io.run(()) + hasRun === true + } + + "provide a stack-safe map operation" in { + val io = (1 to 1000).foldLeft[RIO[Any, Int]](RIO.pure(0)) { case (acc, _) => acc.map(_ + 1) } + io.run(()) === 1000 + } + + "provide a stack-safe flatMap operation" in { + val io = (1 to 1000).foldLeft[RIO[Any, Int]](RIO.pure(0)) { case (acc, _) => acc.flatMap(x => RIO.pure(x + 1)) } + io.run(()) === 1000 + } + + "provide zip/zipWith operations" in { + RIO.pure(1).zip(RIO.pure(2)).run(()) === (1, 2) + + RIO.pure(1).zipWith(RIO.pure(2))(_ + _).run(()) === 3 + } + + "provide andThen/andFinally operations" in { + var hasRunAndThen = false + RIO.suspend({ hasRunAndThen = true; 1 }).andThen(RIO.pure(2)).run(()) === 2 + hasRunAndThen === true + + var hasRunAndFinally = false + RIO.pure(1).andFinally(RIO.suspend({ hasRunAndFinally = true; 2 })).run(()) === 1 + hasRunAndFinally === true + } + + "correctly sequence operations" in { + val io = RIO.sequence(List(RIO.pure(1), RIO.pure(2), RIO.pure(3))) + io.run(()) === List(1, 2, 3) + } + } +}