Skip to content

Commit

Permalink
Merge pull request #7 from julianpeeters/dev
Browse files Browse the repository at this point in the history
Add StoreF and basic monomial Cartesian products
  • Loading branch information
julianpeeters committed Mar 23, 2024
2 parents 83c4097 + 5d896ef commit 4df3840
Show file tree
Hide file tree
Showing 16 changed files with 655 additions and 79 deletions.
23 changes: 11 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Based on the dependent lenses described in [Niu and Spivak](https://topos.site/p

<figure>
<img src="IMAGE.jpg" alt="A mermaid.js chart depicting a wiring diagram." width=50% height=50%>
<figcaption style="text-align: justify;">A wiring diagram: &nbsp; <span style="font-family:Courier">Plant</span> ⊗ <span style="font-family:Courier">Controller</span> → <span style="font-family:Courier">System</span></figcaption>
<figcaption style="text-align: justify;">A wiring diagram: &nbsp; `<span style="font-family:Courier">Plant</span> ⊗ <span style="font-family:Courier">Controller</span> → <span style="font-family:Courier">System</span>`</figcaption>
</figure>

### Modules
Expand All @@ -14,7 +14,7 @@ Based on the dependent lenses described in [Niu and Spivak](https://topos.site/p
- libarary for Scala 3.4+ (JS, JVM, and Native platforms)

```scala
"com.julianpeeters" %% "dynamical-fsm" % "0.5.0"
"com.julianpeeters" %% "dynamical-fsm" % "0.6.0"
```

##### `Moore[P[_]]`
Expand All @@ -40,7 +40,7 @@ import polynomial.morphism.~>
import polynomial.`object`.Monomial.{Interface, Store}

def mealified[Y]: Moore[Store[Boolean, _] ~> Interface[Int, Int => Int, _]] =
Moore[Boolean, Int, Int => Int, Y](
Moore(
false,
s => x => if s then x + x else x, // if we've seen x > 1, then emit 2x
(s, x) => if x > 1 then true else s // change state upon seeing x > 1
Expand All @@ -65,7 +65,7 @@ import polynomial.morphism.~>
import polynomial.`object`.Monomial.{Interface, Store}

def moore[Y]: Moore[Store[Boolean, _] ~> Interface[Int, Int => Int, _]] =
Moore[Boolean, Int, Int => Int, Y](
Moore(
false,
s => x => if s then x + x else x, // if we've seen x > 1, then emit 2x
(s, x) => if x > 1 then true else s // change state upon seeing x > 1
Expand Down Expand Up @@ -109,16 +109,15 @@ val w: Wiring[ω] = Wiring(
b => a => b._2(a),
(b, a) => ((a, b._2), b._2(a))
)
// w: Wiring[ω] = dynamical.fsm.methods.polymap.asWiring$$anon$6@10c31863
// w: Wiring[ω] = dynamical.fsm.methods.polymap.asWiring$$anon$10@5857f9

val m: Moore[(Store[Char, _] ⊗ Store[Byte => Char, _]) ~> (PlantController)] =
(Moore[Char, (Byte, Byte => Char), Char, Nothing](" ".charAt(0), identity, (s, i) => i._2(i._1))
Moore[Byte => Char, Char, Byte => Char, Nothing](b => b.toChar, identity, (f, i) => if i != ' ' then f else b => b.toChar.toUpper))
// m: Moore[~>[⊗[[_$5 >: Nothing <: Any] =>> Store[Char, _$5], [_$6 >: Nothing <: Any] =>> Store[Function1[Byte, Char], _$6]], ⊗[Plant, Controller]]] = dynamical.fsm.methods.moore.product.tensor$$anon$1@127e7f48
def m[Y]: Moore[(Store[Char, _] ⊗ Store[Byte => Char, _]) ~> (PlantController)] =
(Moore[Char, (Byte, Byte => Char), Char, Y](" ".charAt(0), identity, (s, i) => i._2(i._1))
Moore[Byte => Char, Char, Byte => Char, Y](b => b.toChar, identity, (f, i) => if i != ' ' then f else b => b.toChar.toUpper))

val fsm: Mealy[((Store[Char, _] ⊗ Store[Byte => Char, _]) ~> (PlantController) ~> System)] =
m.andThen(w).asMealy
// fsm: Mealy[~>[~>[⊗[[_$7 >: Nothing <: Any] =>> Store[Char, _$7], [_$8 >: Nothing <: Any] =>> Store[Function1[Byte, Char], _$8]], ⊗[Plant, Controller]], System]] = dynamical.fsm.methods.moore.conversions.asMealy$$anon$3@34d91bf4
// fsm: Mealy[~>[~>[⊗[[_$7 >: Nothing <: Any] =>> Store[Char, _$7], [_$8 >: Nothing <: Any] =>> Store[Function1[Byte, Char], _$8]], ⊗[Plant, Controller]], System]] = dynamical.fsm.methods.moore.conversions.asMealy$$anon$8@2da2c5d7

val input = "hello world".getBytes().toList
// input: List[Byte] = List(
Expand All @@ -138,7 +137,7 @@ val result: String = input.mapAccumulate(fsm.init)(fsm.run)._2.mkString
- depends on fs2 3.9

```scala
"com.julianpeeters" %% "dynamical-fs2" % "0.5.0"
"com.julianpeeters" %% "dynamical-fs2" % "0.6.0"
```

The `dynamical-fs2` library provides fs2 integration, in the form of a stream
Expand All @@ -152,7 +151,7 @@ import polynomial.morphism.~>
import polynomial.`object`.Monomial.{Interface, Store}

val m: Mealy[Store[Boolean, _] ~> Interface[Int, Int => Int, _]] = Mealy(false, s => i => i + i, (s, i) => s)
// m: Mealy[~>[[_$9 >: Nothing <: Any] =>> Store[Boolean, _$9], [_$10 >: Nothing <: Any] =>> Interface[Int, Function1[Int, Int], _$10]]] = dynamical.fsm.methods.polymap.asMealy$$anon$1@34712f58
// m: Mealy[~>[[_$9 >: Nothing <: Any] =>> Store[Boolean, _$9], [_$10 >: Nothing <: Any] =>> Interface[Int, Function1[Int, Int], _$10]]] = dynamical.fsm.methods.polymap.asMealy$$anon$1@5251f58a

val l: List[Int] = Stream(1, 2, 3).through(m.transducer).compile.toList
// l: List[Int] = List(2, 4, 6)
Expand Down
14 changes: 8 additions & 6 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
val CatsV = "2.10.0"
val CatsEffectV = "3.5.4"
val Fs2V = "3.9.3"
val MUnitV = "0.7.29"
val PolynomialV = "0.5.0"
val MUnitCEV = "1.0.7"
val PolynomialV = "0.6.0"

inThisBuild(List(
crossScalaVersions := Seq(scalaVersion.value),
Expand Down Expand Up @@ -42,8 +43,9 @@ lazy val fs2 = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.settings(
name := "dynamical-fs2",
libraryDependencies ++= Seq(
"co.fs2" %%% "fs2-core" % Fs2V,
"org.scalameta" %% "munit" % MUnitV % Test,
"co.fs2" %%% "fs2-core" % Fs2V,
"org.scalameta" %% "munit" % MUnitV % Test,
"org.typelevel" %% "munit-cats-effect-3" % MUnitCEV % Test,
)
)
.dependsOn(fsm)
Expand All @@ -56,8 +58,8 @@ lazy val fsm = crossProject(JSPlatform, JVMPlatform, NativePlatform)
name := "dynamical-fsm",
libraryDependencies ++= Seq(
"com.julianpeeters" %%% "polynomial" % PolynomialV,
"org.typelevel" %%% "cats-core" % CatsV,
"org.scalameta" %% "munit" % MUnitV % Test
"org.typelevel" %%% "cats-effect" % CatsEffectV,
"org.scalameta" %% "munit" % MUnitV % Test
)
)
.jsSettings(test := {})
Expand Down
12 changes: 6 additions & 6 deletions docs/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Based on the dependent lenses described in [Niu and Spivak](https://topos.site/p

<figure>
<img src="IMAGE.jpg" alt="A mermaid.js chart depicting a wiring diagram." width=50% height=50%>
<figcaption style="text-align: justify;">A wiring diagram: &nbsp; <span style="font-family:Courier">Plant</span> ⊗ <span style="font-family:Courier">Controller</span> → <span style="font-family:Courier">System</span></figcaption>
<figcaption style="text-align: justify;">A wiring diagram: &nbsp; `<span style="font-family:Courier">Plant</span> ⊗ <span style="font-family:Courier">Controller</span> → <span style="font-family:Courier">System</span>`</figcaption>
</figure>

### Modules
Expand Down Expand Up @@ -40,7 +40,7 @@ import polynomial.morphism.~>
import polynomial.`object`.Monomial.{Interface, Store}

def mealified[Y]: Moore[Store[Boolean, _] ~> Interface[Int, Int => Int, _]] =
Moore[Boolean, Int, Int => Int, Y](
Moore(
false,
s => x => if s then x + x else x, // if we've seen x > 1, then emit 2x
(s, x) => if x > 1 then true else s // change state upon seeing x > 1
Expand All @@ -64,7 +64,7 @@ import polynomial.morphism.~>
import polynomial.`object`.Monomial.{Interface, Store}

def moore[Y]: Moore[Store[Boolean, _] ~> Interface[Int, Int => Int, _]] =
Moore[Boolean, Int, Int => Int, Y](
Moore(
false,
s => x => if s then x + x else x, // if we've seen x > 1, then emit 2x
(s, x) => if x > 1 then true else s // change state upon seeing x > 1
Expand Down Expand Up @@ -109,9 +109,9 @@ val w: Wiring[ω] = Wiring(
(b, a) => ((a, b._2), b._2(a))
)

val m: Moore[(Store[Char, _] ⊗ Store[Byte => Char, _]) ~> (PlantController)] =
(Moore[Char, (Byte, Byte => Char), Char, Nothing](" ".charAt(0), identity, (s, i) => i._2(i._1))
Moore[Byte => Char, Char, Byte => Char, Nothing](b => b.toChar, identity, (f, i) => if i != ' ' then f else b => b.toChar.toUpper))
def m[Y]: Moore[(Store[Char, _] ⊗ Store[Byte => Char, _]) ~> (PlantController)] =
(Moore[Char, (Byte, Byte => Char), Char, Y](" ".charAt(0), identity, (s, i) => i._2(i._1))
Moore[Byte => Char, Char, Byte => Char, Y](b => b.toChar, identity, (f, i) => if i != ' ' then f else b => b.toChar.toUpper))

val fsm: Mealy[((Store[Char, _] ⊗ Store[Byte => Char, _]) ~> (PlantController) ~> System)] =
m.andThen(w).asMealy
Expand Down
74 changes: 74 additions & 0 deletions modules/fs2/jvm/src/test/scala/dynamical/RefSuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package dynamical

import cats.effect.IO
import cats.effect.kernel.Ref
import dynamical.fsm.{Mealy, Moore, Wiring}
import dynamical.stream.transducer
import fs2.Stream
import munit.CatsEffectSuite
import polynomial.morphism.~>
import polynomial.`object`.Monomial.{Interface, StoreF}

class RefSuite extends CatsEffectSuite:

test("ref"):
val moore: IO[Moore[StoreF[IO, Ref[IO, Boolean], _] ~> Interface[Int, IO[Int], _]]] =
Ref.of[IO, Boolean](false).map(ref =>
Moore(
ref,
s => s.get.map(b => if b then 1 else 0),
(s, i) => s.update(b => b).as(s)
)
)
val mealy: IO[Mealy[(StoreF[IO, Ref[IO, Boolean], _] ~> Interface[Int, IO[Int], _]) ~> Interface[Int, Int => IO[Int], _]]] =
moore.map(moore => moore.andThen(Wiring(i => j => i.as(j + j), (i1, i2) => i2)).asMealy)
val obtained: IO[List[Int]] =
for
m <- mealy
r <- Stream(1, 2, 3).covary[IO].through(m.transducer).compile.toList
yield r
val expected: List[Int] = List(2, 4, 6)
assertIO(obtained, expected)

test("broadcast ref"):
def moore(ref: Ref[IO, Boolean]): Moore[StoreF[IO, Ref[IO, Boolean], _] ~> Interface[Int, IO[Int], _]] =
Moore(
ref,
s => s.get.map(b => if b then 1 else 0),
(s, i) => s.update(b => b).as(s)
)
def mealy(
m: Moore[StoreF[IO, Ref[IO, Boolean], _] ~> Interface[Int, IO[Int], _]]
): Mealy[(StoreF[IO, Ref[IO, Boolean], _] ~> Interface[Int, IO[Int], _]) ~> Interface[Int, Int => IO[Int], _]] =
m.andThen(Wiring(i => j => i.as(j + j), (i1, i2) => i2)).asMealy
val obtained: IO[List[Int]] =
for
ref <- Ref.of[IO, Boolean](false)
m1 = mealy(moore(ref))
m2 = mealy(moore(ref))
m3 = mealy(moore(ref))
r <- Stream(1, 2, 3).covary[IO].broadcastThrough(m1.transducer, m2.transducer, m3.transducer).compile.toList
yield r.sorted
val expected: List[Int] = List(2, 2, 2, 4, 4, 4, 6, 6, 6)
assertIO(obtained, expected)

test("merge and sort"):
def moore(ref: Ref[IO, Boolean]): Moore[StoreF[IO, Ref[IO, Boolean], _] ~> Interface[Int, IO[Int], _]] =
Moore(
ref,
s => s.get.map(b => if b then 1 else 0),
(s, i) => s.update(b => b).as(s)
)
def mealy(
m: Moore[StoreF[IO, Ref[IO, Boolean], _] ~> Interface[Int, IO[Int], _]]
): Mealy[(StoreF[IO, Ref[IO, Boolean], _] ~> Interface[Int, IO[Int], _]) ~> Interface[Int, Int => IO[Int], _]] =
m.andThen(Wiring(i => j => i.as(j + j), (i1, i2) => i2)).asMealy
val obtained: IO[List[Int]] =
for
ref <- Ref.of[IO, Boolean](false)
m1 = mealy(moore(ref))
m2 = mealy(moore(ref))
r <- Stream(1, 2, 3).covary[IO].through(m1.transducer).merge(Stream(11, 22, 33).covary[IO].through(m2.transducer)).compile.toList
yield r.sorted
val expected: List[Int] = List(2, 4, 6, 22, 44, 66)
assertIO(obtained, expected)
20 changes: 19 additions & 1 deletion modules/fs2/shared/src/main/scala/dynamical/stream.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package dynamical.stream

import cats.effect.kernel.Ref
import cats.effect.IO
import fs2.Pipe
import dynamical.fsm.Mealy
import dynamical.fsm.methods.types.Unify2
import polynomial.morphism.~>
import polynomial.`object`.Binomial.BiInterface
import polynomial.`object`.Monomial.{Interface, Store}
import polynomial.`object`.Monomial.{Interface, Store, StoreF}

extension [S, A, B] (m: Mealy[Store[S, _] ~> Interface[A, A => B, _]])
@scala.annotation.targetName("transducerStoreToMono")
Expand All @@ -15,6 +17,22 @@ extension [S, A, B] (m: Mealy[Store[S, _] ~> Interface[A, A => B, _]])
.mapAccumulate(m.init)(m.run)
.map(_._2)

extension [S, A, B] (m: Mealy[Store[S, _] ~> Interface[A, B, _] ~> Interface[A, A => B, _]])
@scala.annotation.targetName("transducerStoreToMonoToMono")
def transducer[F[_], Y]: Pipe[F, A, B] =
stream =>
stream
.mapAccumulate(m.init)(m.run)
.map(_._2)

extension [S, A, B] (m: Mealy[(StoreF[IO, Ref[IO, Boolean], _] ~> Interface[A, IO[B], _]) ~> Interface[A, A => IO[B], _]])
@scala.annotation.targetName("transducerFStoreToMonoToMono")
def transducer[Y]: Pipe[IO, A, B] =
stream =>
stream
.evalMapAccumulate(m.init)((s, i) => m.run(s, i))
.map(_._2)

extension [S, A1, B1, A2, B2] (m: Mealy[Store[S, _] ~> BiInterface[A1, A1 => B1, A2, A2 => B2, _]])
@scala.annotation.targetName("transducerStoreToBi")
def transducer[F[_], Y]: Pipe[F, Unify2[A1, A2], Unify2[B1, B2]] =
Expand Down
109 changes: 109 additions & 0 deletions modules/fsm/jvm/src/test/scala/dynamical/CartesianSuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package dynamical

import cats.implicits.given
import dynamical.fsm.{Mealy, Moore, Wiring}
import munit.FunSuite
import polynomial.morphism.~>
import polynomial.`object`.Monomial.{Interface, Store}
import polynomial.product.{×, }

class CartesianSuite extends FunSuite:

test("mealy cartesian product"):

val n: Moore[(Store[Boolean, _] ~> (Interface[Int, Int, _] × Interface[String, String, _]))] =
Moore(
i = false,
r = (s: Boolean) => (if s then 1 else 0, "hello"),
u = (s: Boolean, a: Either[Int, String]) => a match
case Left(value) => !s
case Right(value) => s
,
)
val w: Wiring[(Interface[Int, Int, _] × Interface[String, String, _]) ~> Interface[Either[Int, String], Either[Int, String] => (Int, String), _]] =
Wiring(
r = b => a => b,
u = (b, a) => a
)
val m: Mealy[Store[Boolean, _] ~> (Interface[Int, Int, _] × Interface[String, String, _]) ~> Interface[Either[Int, String], Either[Int, String] => (Int, String), _]] =
n.andThen(w).asMealy
val obtained: List[(Int, String)] = List(Right("foo"), Left(2), Right("bar")).mapAccumulate(m.init)(m.run)._2
val expected: List[(Int, String)] = List((0, "hello"), (0, "hello"), (1, "hello"))
assertEquals(obtained, expected)

test("mealy cartesian tensored"):
val n: Moore[Store[String, _] ~> ((Interface[Int, Int, _] × Interface[String, String, _]) ⊗ Interface[Boolean, Boolean, _])] =
Moore(
i = "",
r = s => ((s.length, s), s.length >= 2),
u = (s, a) => if a._2 then s else a._1 match
case Left(value) => value.toString
case Right(value) => s + value,
)
val w: Wiring[
((Interface[Int, Int, _] × Interface[String, String, _]) ⊗ Interface[Boolean, Boolean, _]) ~>
Interface[((Either[Int, String], Boolean)), ((Either[Int, String], Boolean)) => ((Int, String), Boolean), _]
] =
Wiring(
r = b => a => b,
(b, a) => a
)
val m: Mealy[Store[String, _] ~> ((Interface[Int, Int, _] × Interface[String, String, _]) ⊗ Interface[Boolean, Boolean, _]) ~> Interface[((Either[Int, String], Boolean)), ((Either[Int, String], Boolean)) => ((Int, String), Boolean), _]] =
n.andThen(w).asMealy
val obtained: List[((Int, String), Boolean)] = List((Right("foo"), true), (Left(2), false), (Right("bar"), true)).mapAccumulate(m.init)(m.run)._2
val expected: List[((Int, String), Boolean)] = List(((0, ""), false), ((0, ""), false), ((1, "2"), false))
assertEquals(obtained, expected)

test("mealy tensored cartesian"):
val n: Moore[Store[String, _] ~> (Interface[Int, Int, _] × (Interface[String, String, _] ⊗ Interface[Boolean, Boolean, _]))] =
Moore(
i = "",
r = s => (s.length, (s, s.length > 3)),
u = (s, a) => a match
case Left(value) => s + "!"
case Right(value) => s + value._1
)
val w: Wiring[
(Interface[Int, Int, _] × (Interface[String, String, _] ⊗ Interface[Boolean, Boolean, _])) ~>
Interface[Either[Int, (String, Boolean)], Either[Int, (String, Boolean)] => (Int, (String, Boolean)), _]
] =
Wiring(
r = b => a => b,
(b, a) => a
)
val m: Mealy[
Store[String, _] ~>
(Interface[Int, Int, _] × (Interface[String, String, _] ⊗ Interface[Boolean, Boolean, _])) ~>
Interface[Either[Int, (String, Boolean)], Either[Int, (String, Boolean)] => (Int, (String, Boolean)), _]
] =
n.andThen(w).asMealy
val obtained: List[(Int, (String, Boolean))] = List(Right(("foo", true)), Left(2), Right(("bar", false))).mapAccumulate(m.init)(m.run)._2
val expected: List[(Int, (String, Boolean))] = List((0, ("", false)), (3, ("foo", false)), (4, ("foo!", true)))
assertEquals(obtained, expected)

test("mealy linearly distributive"):
type P[Y] = Interface[Int, Double, Y]
type Q[Y] = Interface[String, Char, Y]
type R[Y] = Interface[Boolean, Unit, Y]
type S[Y] = Store[String, Y]
type T[Y] = Interface[Either[Int, (String, Boolean)], Either[Int, (String, Boolean)] => (Double, (Char, Unit)), Y]
val n: Moore[S ~> ((P × Q) ⊗ R)] =
Moore(
i = "",
r = s => ((s.length, s.headOption.getOrElse('!')), ()),
u = (s, a) => if a._2 then s else a._1 match
case Left(value) => value.toString
case Right(value) => s + value,
)
val w: Wiring[((P × Q) ⊗ R) ~> (P × (QR))] =
Wiring(
b => (b._1._1, (b._1._2, b._2)), // re-tuple,
(b, a) => a match // exhibit strength:
case Left(value) => (Left(value), value < 1)
case Right(value) => (Right(b._1._2+:value._1), value._2)
)
val z: Wiring[(P × (QR)) ~> T] = Wiring(b => a => b, (b, a) => a)
val m: Mealy[((S ~> ((P × Q) ⊗ R)) ~> (P × (QR))) ~> T] = n.andThen(w).andThen(z).asMealy
val obtained: List[(Double, (Char, Unit))] = List(Right(("foo", true)), Left(2), Right(("bar", false))).mapAccumulate(m.init)(m.run)._2
val expected: List[(Double, (Char, Unit))] = List((0, ('!', ())), (0, ('!', ())), (1, ('2', ())))
assertEquals(obtained, expected)
4 changes: 2 additions & 2 deletions modules/fsm/jvm/src/test/scala/dynamical/MonoSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class MonoSuite extends FunSuite:
assertEquals(obtained, expected)

test("wrapper"):
def w[Y]: Wiring[Interface[Int, Int, _] ~> Interface[Int, Int => Int, _]] = Wiring[Id, Int, Int, Int, Int, Y](i => j => j + j, (i1, i2) => i2)
def w[Y]: Wiring[Interface[Int, Int, _] ~> Interface[Int, Int => Int, _]] = Wiring(i => j => j + j, (i1, i2) => i2)
val l: Moore[Store[Boolean, _] ~> Interface[Int, Int, _]] = Moore(false, s => if s then 1 else 0, (s, i) => s)
val m: Mealy[Store[Boolean, _] ~> Interface[Int, Int, _] ~> Interface[Int, Int => Int, _]] = l.andThen(w).asMealy
val obtained: List[Int] = List(1, 2, 3).mapAccumulate(m.init)((s, i) => m.run(s, i))._2
Expand All @@ -45,7 +45,7 @@ class MonoSuite extends FunSuite:
assertEquals(obtained, expected)

test("wrapped semi-choice"):
def w[Y]: Wiring[Interface[Int, Int, _] ~> Interface[Int, Int => Int, _]] = Wiring[Id, Int, Int, Int, Int, Y](i => j => j + j, (i1, i2) => i2)
val w: Wiring[Interface[Int, Int, _] ~> Interface[Int, Int => Int, _]] = Wiring(i => j => j + j, (i1, i2) => i2)
val n: Moore[Store[Option[Boolean], _] ~> Interface[Int, Int, _]] =
Moore(Some(true), s => s.fold(0)(b => if b then 1 else 0), (s, i) => if i > 1 then None else s.map(b => !b))
val m = n.andThen(w)
Expand Down

0 comments on commit 4df3840

Please sign in to comment.