Skip to content

Commit

Permalink
Merge pull request #28 from JD557/add-rio
Browse files Browse the repository at this point in the history
Update CanvasIO to be backed by a RIO implementation
  • Loading branch information
JD557 authored Aug 9, 2020
2 parents ea7a345 + e29cceb commit b49f1ca
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 32 deletions.
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ lazy val pure =
.dependsOn(core)
.settings(sharedSettings)
.settings(name := "minart-pure")
.settings(testSettings)
.settings(publishSettings)
.jsSettings(jsSettings)
.nativeSettings(nativeSettings)
Expand Down
38 changes: 6 additions & 32 deletions pure/shared/src/main/scala/eu/joaocosta/minart/pure/CanvasIO.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
70 changes: 70 additions & 0 deletions pure/shared/src/main/scala/eu/joaocosta/minart/pure/RIO.scala
Original file line number Diff line number Diff line change
@@ -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))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package eu.joaocosta.minart

import eu.joaocosta.minart.core.Canvas

package object pure {
type CanvasIO[+A] = RIO[Canvas, A]
}

52 changes: 52 additions & 0 deletions pure/shared/src/test/scala/eu/joaocosta/minart/pure/RIOSpec.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}
}

0 comments on commit b49f1ca

Please sign in to comment.