diff --git a/CHANGELOG.md b/CHANGELOG.md
index e00d558..e3d0a21 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ Changes:
- `lite-diff`: Initial release ([#16](https://github.com/MakeNowJust-Labo/lite/pull/16))
- `lite-pfix`: Initial release ([#27](https://github.com/MakeNowJust-Labo/lite/pull/27))
- `lite-shohw`: Use `lite-pfix` ([#27](https://github.com/MakeNowJust-Labo/lite/pull/27))
+- `lite-crazy`: Initial release ([#28](https://github.com/MakeNowJust-Labo/lite/pull/28))
- Support Scala 2.13.6
- Support Scala 3.0.1 ([#23](https://github.com/MakeNowJust-Labo/lite/pull/23))
- Update sbt to 1.5.5 ([#24](https://github.com/MakeNowJust-Labo/lite/pull/24))
diff --git a/README.md b/README.md
index cd3368b..1d171ad 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,7 @@
## Libraries
+- [**lite-crazy**](modules/lite-crazy): A lazy-evaluated value cell and time travelling state monads implementations.
- [**lite-diff**](modules/lite-diff): Computes a diff between two sequences.
- [**lite-gimei**](modules/lite-gimei): A generator of Japanese dummy names and addresses with furigana.
- [**lite-grapheme**](modules/lite-grapheme): Iterates the given string on each grapheme cluster.
diff --git a/build.sbt b/build.sbt
index d3fd5bd..3242e32 100644
--- a/build.sbt
+++ b/build.sbt
@@ -29,7 +29,7 @@ ThisBuild / semanticdbVersion := scalafixSemanticdb.revision
ThisBuild / scalafixDependencies += "com.github.liancheng" %% "organize-imports" % "0.5.0"
ThisBuild / scalafixDependencies += "com.github.vovapolu" %% "scaluzzi" % "0.1.18"
-val crossProjectNames = Seq("diff", "gimei", "grapheme", "romaji", "show")
+val crossProjectNames = Seq("crazy", "diff", "gimei", "grapheme", "pfix", "romaji", "show")
val platformSuffices = Seq("JVM", "JS", "Native")
platformSuffices.flatMap { platform =>
addCommandAlias(s"test$platform", crossProjectNames.map(name => s"$name$platform/test").mkString("; "))
@@ -42,12 +42,42 @@ lazy val root = project
publish / skip := true,
coverageEnabled := false
)
+ .aggregate(crazyJVM, crazyJS, crazyNative)
.aggregate(diffJVM, diffJS, diffNative)
.aggregate(gimeiJVM, gimeiJS, gimeiNative)
.aggregate(graphemeJVM, graphemeJS, graphemeNative)
.aggregate(romajiJVM, romajiJS, romajiNative)
+ .aggregate(pfixJVM, pfixJS, pfixNative)
.aggregate(showJVM, showJS, showNative)
+lazy val crazy = crossProject(JVMPlatform, JSPlatform, NativePlatform)
+ .in(file("modules/lite-crazy"))
+ .settings(
+ name := "lite-crazy",
+ console / initialCommands :=
+ """|import codes.quine.labo.lite.crazy._
+ |""".stripMargin,
+ Compile / console / scalacOptions -= "-Wunused",
+ Test / console / scalacOptions -= "-Wunused",
+ // Set URL mapping of scala standard API for Scaladoc.
+ apiMappings ++= scalaInstance.value.libraryJars
+ .filter(file => file.getName.startsWith("scala-library") && file.getName.endsWith(".jar"))
+ .map(_ -> url(s"http://www.scala-lang.org/api/${scalaVersion.value}/"))
+ .toMap,
+ // Settings for test:
+ libraryDependencies += "org.scalameta" %%% "munit" % "0.7.27" % Test,
+ testFrameworks += new TestFramework("munit.Framework")
+ )
+ .jsSettings(Test / scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule) })
+ .nativeSettings(
+ crossScalaVersions := Seq("2.13.6"),
+ coverageEnabled := false
+ )
+
+lazy val crazyJVM = crazy.jvm
+lazy val crazyJS = crazy.js
+lazy val crazyNative = crazy.native
+
lazy val diff = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.in(file("modules/lite-diff"))
.settings(
diff --git a/modules/lite-crazy/README.md b/modules/lite-crazy/README.md
new file mode 100644
index 0000000..65f660d
--- /dev/null
+++ b/modules/lite-crazy/README.md
@@ -0,0 +1,18 @@
+# lite-crazy
+
+> A lazy-evaluated value cell and time travelling state monads implementations.
+
+[![Maven Central](https://img.shields.io/maven-central/v/codes.quine.labo/lite-crazy_2.13?logo=scala&style=for-the-badge)](https://search.maven.org/artifact/codes.quine.labo/lite-crazy_2.13)
+
+## Install
+
+Insert the following to your `build.sbt`.
+
+```sbt
+libraryDependencies += "codes.quine.labo" %% "lite-crazy" % "0.3.0"
+```
+
+## Reference
+
+-
+-
diff --git a/modules/lite-crazy/shared/src/main/scala/codes/quine/labo/lite/crazy/Lazy.scala b/modules/lite-crazy/shared/src/main/scala/codes/quine/labo/lite/crazy/Lazy.scala
new file mode 100644
index 0000000..deac088
--- /dev/null
+++ b/modules/lite-crazy/shared/src/main/scala/codes/quine/labo/lite/crazy/Lazy.scala
@@ -0,0 +1,38 @@
+package codes.quine.labo.lite.crazy
+
+/** Lazy is a lazy-evaluated value cell. */
+final class Lazy[A] private (private[this] var thunk: () => A) {
+
+ private[this] var cache: Option[A] = None
+
+ /** Returns an underlying value. */
+ def value: A = cache match {
+ case Some(x) => x
+ case None =>
+ synchronized {
+ val x = thunk()
+ thunk = null
+ cache = Some(x)
+ x
+ }
+ }
+
+ /** Applies an underlying value to the given mapping, and returns a new lazy cell holds this result. */
+ def map[B](f: A => B): Lazy[B] = Lazy(f(value))
+
+ /** Applies an underlying value to the given mapping, and returns a new lazy cell holds flattened result. */
+ def flatMap[B](f: A => Lazy[B]): Lazy[B] = Lazy(f(value).value)
+}
+
+object Lazy {
+
+ /** Returns a lazy-evaluated value cell with the given value. */
+ def apply[A](x: => A): Lazy[A] = new Lazy(() => x)
+
+ /** Returns an lazy-evaluated value cell getting from fixpoint function. */
+ def fix[A](f: Lazy[A] => A): Lazy[A] = {
+ var lx: Lazy[A] = null
+ lx = Lazy(f(lx))
+ lx
+ }
+}
diff --git a/modules/lite-crazy/shared/src/main/scala/codes/quine/labo/lite/crazy/RState.scala b/modules/lite-crazy/shared/src/main/scala/codes/quine/labo/lite/crazy/RState.scala
new file mode 100644
index 0000000..213f461
--- /dev/null
+++ b/modules/lite-crazy/shared/src/main/scala/codes/quine/labo/lite/crazy/RState.scala
@@ -0,0 +1,42 @@
+package codes.quine.labo.lite.crazy
+
+/** RState is reversed state monad implementation. */
+final case class RState[S, A](run: Lazy[S] => (Lazy[S], Lazy[A])) {
+
+ /** Applies a result value to the given mapping, and returns a new reversed state monad holds this result. */
+ def map[B](f: Lazy[A] => Lazy[B]): RState[S, B] = flatMap(lx => RState.pure(f(lx)))
+
+ /** Applies a result value to the given mapping, and returns a new reversed state monad holds flattened result.
+ * Hence the mapping requires a lazy cell as its argument, we can write an action depends on future state.
+ */
+ def flatMap[B](f: Lazy[A] => RState[S, B]): RState[S, B] =
+ RState[S, B] { ls0 =>
+ type Fix = (Lazy[(Lazy[S], Lazy[A])], Lazy[(Lazy[S], Lazy[B])], Lazy[S], Lazy[S], Lazy[A], Lazy[B])
+ val lfix = Lazy.fix[Fix] { lfix =>
+ val ls2x = Lazy(run(lfix.value._3))
+ val ls1y = Lazy(f(lfix.value._5).run(ls0))
+ val ls1 = Lazy(ls1y.value._1.value)
+ val ls2 = Lazy(ls2x.value._1.value)
+ val lx = Lazy(ls2x.value._2.value)
+ val ly = Lazy(ls1y.value._2.value)
+ (ls2x, ls1y, ls1, ls2, lx, ly)
+ }
+ val (_, _, _, ls2, _, ly) = lfix.value
+ (ls2, ly)
+ }
+}
+
+object RState {
+
+ /** Returns a reversed state monad holds the specified value as its result value. */
+ def pure[S, A](lx: Lazy[A]): RState[S, A] = RState(ls => (ls, lx))
+
+ /** Returns a reversed state monad holds a future state as its result value. */
+ def get[S]: RState[S, S] = RState(ls => (ls, ls))
+
+ /** Returns a reversed state monad for putting the specified value to its state. */
+ def put[S](ls: Lazy[S]): RState[S, Unit] = RState(_ => (ls, Lazy(())))
+
+ /** Returns a reversed state monad for modifying its state by the given mapping. */
+ def modify[S](f: Lazy[S] => Lazy[S]): RState[S, Unit] = RState(ls => (f(ls), Lazy(())))
+}
diff --git a/modules/lite-crazy/shared/src/main/scala/codes/quine/labo/lite/crazy/Tardis.scala b/modules/lite-crazy/shared/src/main/scala/codes/quine/labo/lite/crazy/Tardis.scala
new file mode 100644
index 0000000..141943d
--- /dev/null
+++ b/modules/lite-crazy/shared/src/main/scala/codes/quine/labo/lite/crazy/Tardis.scala
@@ -0,0 +1,64 @@
+package codes.quine.labo.lite.crazy
+
+/** Tardis a state monad implementation holds both of backward and forward states. */
+final case class Tardis[BW, FW, A](run: (Lazy[BW], Lazy[FW]) => (Lazy[BW], Lazy[FW], Lazy[A])) {
+
+ /** Applies a result value to the given mapping, and returns a new tardis monad holds this result. */
+ def map[B](f: Lazy[A] => Lazy[B]): Tardis[BW, FW, B] = flatMap(lx => Tardis.pure(f(lx)))
+
+ /** Applies a result value to the given mapping, and returns a new tardis monad holds flattened result.
+ * Hence the mapping requires lazy cells as its arguments, we can write an action depends on both of future and past state.
+ */
+ def flatMap[B](f: Lazy[A] => Tardis[BW, FW, B]): Tardis[BW, FW, B] =
+ Tardis { case (lb0, lf0) =>
+ type Fix = (
+ Lazy[(Lazy[BW], Lazy[FW], Lazy[A])],
+ Lazy[(Lazy[BW], Lazy[FW], Lazy[B])],
+ Lazy[BW],
+ Lazy[BW],
+ Lazy[FW],
+ Lazy[FW],
+ Lazy[A],
+ Lazy[B]
+ )
+ val lfix = Lazy.fix[Fix] { lfix =>
+ val lb2f1x = Lazy(run(lfix.value._3, lf0))
+ val lb1f2y = Lazy(f(lfix.value._7).run(lb0, lfix.value._5))
+ val lb1 = Lazy(lb1f2y.value._1.value)
+ val lb2 = Lazy(lb2f1x.value._1.value)
+ val lf1 = Lazy(lb2f1x.value._2.value)
+ val lf2 = Lazy(lb1f2y.value._2.value)
+ val lx = Lazy(lb2f1x.value._3.value)
+ val ly = Lazy(lb1f2y.value._3.value)
+ (lb2f1x, lb1f2y, lb1, lb2, lf1, lf2, lx, ly)
+ }
+ val (_, _, _, lb2, _, lf2, _, ly) = lfix.value
+ (lb2, lf2, ly)
+ }
+}
+
+object Tardis {
+
+ /** Returns a new tardis monad holds the specified value as its result. */
+ def pure[BW, FW, A](lx: Lazy[A]): Tardis[BW, FW, A] = Tardis((lb, lf) => (lb, lf, lx))
+
+ /** Returns a new tardis monad holds a backward state as its result. */
+ def getBackward[BW, FW]: Tardis[BW, FW, BW] = Tardis((lb, lf) => (lb, lf, lb))
+
+ /** Returns a new tardis monad for putting the specified value to a backward state. */
+ def putBackward[BW, FW](lb: Lazy[BW]): Tardis[BW, FW, Unit] = Tardis((_, lf) => (lb, lf, Lazy(())))
+
+ /** Returns a new tardis monad for modifying a backward state by the given mapping. */
+ def modifyBackward[BW, FW](f: Lazy[BW] => Lazy[BW]): Tardis[BW, FW, Unit] =
+ Tardis((lb, lf) => (f(lb), lf, Lazy(())))
+
+ /** Returns a new tardis monad holds a forward state as its result. */
+ def getForward[BW, FW]: Tardis[BW, FW, FW] = Tardis((lb, lf) => (lb, lf, lf))
+
+ /** Returns a new tardis monad for putting the specified value to a forward state. */
+ def putForward[BW, FW](lf: Lazy[FW]): Tardis[BW, FW, Unit] = Tardis((lb, _) => (lb, lf, Lazy(())))
+
+ /** Returns a new tardis monad for modifying a forward state by the given mapping. */
+ def modifyForward[BW, FW](f: Lazy[FW] => Lazy[FW]): Tardis[BW, FW, Unit] =
+ Tardis((lb, lf) => (lb, f(lf), Lazy(())))
+}
diff --git a/modules/lite-crazy/shared/src/main/scala/codes/quine/labo/lite/crazy/implicits.scala b/modules/lite-crazy/shared/src/main/scala/codes/quine/labo/lite/crazy/implicits.scala
new file mode 100644
index 0000000..d53d2a2
--- /dev/null
+++ b/modules/lite-crazy/shared/src/main/scala/codes/quine/labo/lite/crazy/implicits.scala
@@ -0,0 +1,12 @@
+package codes.quine.labo.lite.crazy
+
+/** implicits provides convenience implicit conversions. */
+object implicits {
+ import scala.language.implicitConversions
+
+ /** Wraps any value to lazy cell. */
+ implicit def any2lazy[A](x: => A): Lazy[A] = Lazy(x)
+
+ /** Unwraps any value from a lazy cell. */
+ implicit def lazy2any[A](lx: Lazy[A]): A = lx.value
+}
diff --git a/modules/lite-crazy/shared/src/test/scala/codes/quine/labo/lite/crazy/LazySuite.scala b/modules/lite-crazy/shared/src/test/scala/codes/quine/labo/lite/crazy/LazySuite.scala
new file mode 100644
index 0000000..ebbd62f
--- /dev/null
+++ b/modules/lite-crazy/shared/src/test/scala/codes/quine/labo/lite/crazy/LazySuite.scala
@@ -0,0 +1,28 @@
+package codes.quine.labo.lite.crazy
+
+class LazySuite extends munit.FunSuite {
+ test("Lazy.apply") {
+ var x = 0
+ val lx = Lazy {
+ x += 1
+ x
+ }
+ assertEquals(x, 0)
+ assertEquals(lx.value, 1)
+ assertEquals(x, 1)
+ assertEquals(lx.value, 1)
+ }
+
+ test("Lazy.fix") {
+ val lxs = Lazy.fix[LazyList[Int]](lxs => LazyList.cons(1, lxs.value).scanLeft(0)(_ + _))
+ assertEquals(lxs.value.take(10), LazyList(0, 1, 1, 2, 3, 5, 8, 13, 21, 34))
+ }
+
+ test("Lazy#map") {
+ assertEquals(Lazy(1).map(_ + 2).value, 3)
+ }
+
+ test("Lazy#flatMap") {
+ assertEquals(Lazy(1).flatMap(x => Lazy(x + 2)).value, 3)
+ }
+}
diff --git a/modules/lite-crazy/shared/src/test/scala/codes/quine/labo/lite/crazy/RStateSuite.scala b/modules/lite-crazy/shared/src/test/scala/codes/quine/labo/lite/crazy/RStateSuite.scala
new file mode 100644
index 0000000..20d41f7
--- /dev/null
+++ b/modules/lite-crazy/shared/src/test/scala/codes/quine/labo/lite/crazy/RStateSuite.scala
@@ -0,0 +1,21 @@
+package codes.quine.labo.lite.crazy
+
+class RStateSuite extends munit.FunSuite {
+ // https://lukepalmer.wordpress.com/2008/08/10/mindfuck-the-reverse-state-monad/
+ test("RState: fibs") {
+ import codes.quine.labo.lite.crazy.implicits._
+
+ def cumulativeSums(xs: LazyList[Int]): LazyList[Int] = xs.scanLeft(0)(_ + _)
+
+ def computeFibs(): RState[LazyList[Int], LazyList[Int]] = for {
+ fibs <- RState.get[LazyList[Int]]
+ _ <- RState.modify[LazyList[Int]](cumulativeSums(_))
+ _ <- RState.put[LazyList[Int]](LazyList.cons(1, fibs))
+ } yield fibs
+
+ assertEquals(
+ computeFibs().run(LazyList.empty[Int])._2.take(15),
+ LazyList(0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377)
+ )
+ }
+}
diff --git a/modules/lite-crazy/shared/src/test/scala/codes/quine/labo/lite/crazy/TardisSuite.scala b/modules/lite-crazy/shared/src/test/scala/codes/quine/labo/lite/crazy/TardisSuite.scala
new file mode 100644
index 0000000..3926acd
--- /dev/null
+++ b/modules/lite-crazy/shared/src/test/scala/codes/quine/labo/lite/crazy/TardisSuite.scala
@@ -0,0 +1,103 @@
+package codes.quine.labo.lite.crazy
+
+class TardisSuite extends munit.FunSuite {
+ // https://blog.csongor.co.uk/time-travel-in-haskell-for-dummies/
+ test("Tardis: single-pass assembler") {
+ import codes.quine.labo.lite.crazy.implicits._
+
+ type Addr = Int
+ type SymTable = Map[String, Addr]
+
+ sealed abstract class Instr extends Product with Serializable
+ case object Add extends Instr
+ case object Mov extends Instr
+ final case class ToLabel(label: String) extends Instr
+ final case class ToAddr(addr: Addr) extends Instr
+ final case class Label(label: String) extends Instr
+ case object Err extends Instr
+
+ type Assembler[A] = Tardis[SymTable, SymTable, A]
+
+ def assemble(addr: Addr, is0: List[Instr]): Assembler[List[(Addr, Instr)]] =
+ is0 match {
+ case Nil => Tardis.pure(List.empty)
+ case Label(label) :: is1 =>
+ for {
+ _ <- Tardis.modifyBackward[SymTable, SymTable](_.updated(label, addr))
+ _ <- Tardis.modifyForward[SymTable, SymTable](_.updated(label, addr))
+ is2 <- assemble(addr, is1)
+ } yield is2
+ case ToLabel(label) :: is1 =>
+ for {
+ bw <- Tardis.getBackward[SymTable, SymTable]
+ fw <- Tardis.getForward[SymTable, SymTable]
+ is2 <- assemble(addr + 1, is1)
+ } yield Lazy { // Here's `Lazy` is necessary.
+ val union = bw ++ fw
+ val i = union.get(label) match {
+ case Some(a) => (addr, ToAddr(a))
+ case None => (addr, Err)
+ }
+ i :: is2
+ }
+ case i :: is1 =>
+ for {
+ rest <- assemble(addr + 1, is1)
+ } yield (addr, i) :: rest
+ }
+
+ val input = List(
+ Add,
+ Add,
+ ToLabel("my_label"),
+ Mov,
+ Mov,
+ Label("my_label"),
+ Label("second_label"),
+ Mov,
+ ToLabel("second_label"),
+ Mov
+ )
+
+ val (lb, lf, lis) = assemble(0, input).run(Map.empty[String, Int], Map.empty[String, Int])
+ assertEquals(lb.value, Map("my_label" -> 5, "second_label" -> 5))
+ assertEquals(lf.value, Map("my_label" -> 5, "second_label" -> 5))
+ assertEquals(
+ lis.value,
+ List(
+ (0, Add),
+ (1, Add),
+ (2, ToAddr(5)),
+ (3, Mov),
+ (4, Mov),
+ (5, Mov),
+ (6, ToAddr(5)),
+ (7, Mov)
+ )
+ )
+ }
+
+ test("Tardis.putBackward") {
+ val program: Tardis[Int, Int, (Int, Int, Int)] = for {
+ s1 <- Tardis.getBackward[Int, Int]
+ _ <- Tardis.putBackward[Int, Int](Lazy(1))
+ s2 <- Tardis.getBackward[Int, Int]
+ _ <- Tardis.putBackward[Int, Int](Lazy(2))
+ s3 <- Tardis.getBackward[Int, Int]
+ } yield Lazy((s1.value, s2.value, s3.value))
+ val (_, _, ls123) = program.run(Lazy(3), Lazy(0))
+ assertEquals(ls123.value, (1, 2, 3))
+ }
+
+ test("Tardis.putForward") {
+ val program: Tardis[Int, Int, (Int, Int, Int)] = for {
+ s1 <- Tardis.getForward[Int, Int]
+ _ <- Tardis.putForward[Int, Int](Lazy(2))
+ s2 <- Tardis.getForward[Int, Int]
+ _ <- Tardis.putForward[Int, Int](Lazy(3))
+ s3 <- Tardis.getForward[Int, Int]
+ } yield Lazy((s1.value, s2.value, s3.value))
+ val (_, _, ls123) = program.run(Lazy(0), Lazy(1))
+ assertEquals(ls123.value, (1, 2, 3))
+ }
+}
diff --git a/modules/lite-pfix/shared/src/main/scala/codes/quine/labo/lite/pfix/PFixFunction.scala b/modules/lite-pfix/shared/src/main/scala/codes/quine/labo/lite/pfix/PFixFunction.scala
index af54b69..4849c94 100644
--- a/modules/lite-pfix/shared/src/main/scala/codes/quine/labo/lite/pfix/PFixFunction.scala
+++ b/modules/lite-pfix/shared/src/main/scala/codes/quine/labo/lite/pfix/PFixFunction.scala
@@ -7,7 +7,7 @@ private class PFixFunction[A, B](
) extends (A => B) { fix =>
/** A list of partial functions obtained by fixpoint functions. */
- protected val pfs = fs.map(f => f(fix))
+ protected val pfs: Vector[PartialFunction[A, B]] = fs.map(f => f(fix))
override def apply(x: A): B = {
for (pf <- pfs) {
diff --git a/modules/lite-pfix/shared/src/test/scala/codes/quine/labo/lite/pfix/PFixSuite.scala b/modules/lite-pfix/shared/src/test/scala/codes/quine/labo/lite/pfix/PFixSuite.scala
index f86ffaf..84e70f4 100644
--- a/modules/lite-pfix/shared/src/test/scala/codes/quine/labo/lite/pfix/PFixSuite.scala
+++ b/modules/lite-pfix/shared/src/test/scala/codes/quine/labo/lite/pfix/PFixSuite.scala
@@ -40,7 +40,8 @@ class PFixSuite extends munit.FunSuite {
val pfix = PFix.from[Int, Int] { case n if n > 0 => n * 2 }
val pf = pfix.toPartialFunction
assertEquals(pf(1), 2)
- interceptMessage[MatchError]("-1 (of class java.lang.Integer)")(pf(-1))
+ val err = intercept[MatchError](pf(-1))
+ assert(err.getMessage().contains("-1"))
assertEquals(pf.isDefinedAt(1), true)
assertEquals(pf.isDefinedAt(-1), false)
}