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) }