From 661062945339b1805cf5a3a24acf0a9eefb5737f Mon Sep 17 00:00:00 2001 From: Greg Wogan-Browne Date: Fri, 6 Dec 2019 10:12:28 +1100 Subject: [PATCH] Upgrade to scala 2.13 Use cats typeclasses Use cats-laws to verify typeclasses --- build.sbt | 20 +- src/main/scala/conway/Console.scala | 1 - src/main/scala/conway/Game.scala | 6 +- src/main/scala/conway/Main.scala | 6 +- src/main/scala/conway/Patterns.scala | 2 - src/main/scala/conway/Renderer.scala | 7 +- src/main/scala/conway/Visualization.scala | 1 - .../data/GridZipper.scala | 56 +++--- .../{typeclasses => conway}/data/Zipper.scala | 54 +++--- src/main/scala/conway/package.scala | 12 +- src/main/scala/typeclasses/Comonad.scala | 25 --- src/main/scala/typeclasses/Functor.scala | 5 - src/main/scala/typeclasses/Monad.scala | 7 - .../scala/typeclasses/syntax/gridZipper.scala | 16 -- .../scala/typeclasses/syntax/zipper.scala | 16 -- src/test/scala/conway/GameSpec.scala | 9 +- .../scala/conway/data/GridZipperSpec.scala | 174 ++++++++++++++++++ .../scala/conway/data/ZipperComonadSpec.scala | 42 +++++ .../scala/conway/data/ZipperLawsSpec.scala | 10 + .../data/ZipperSpec.scala | 24 +-- .../typeclasses/data/GridZipperSpec.scala | 174 ------------------ .../typeclasses/data/ZipperComonadSpec.scala | 41 ----- 22 files changed, 325 insertions(+), 383 deletions(-) rename src/main/scala/{typeclasses => conway}/data/GridZipper.scala (52%) rename src/main/scala/{typeclasses => conway}/data/Zipper.scala (54%) delete mode 100644 src/main/scala/typeclasses/Comonad.scala delete mode 100644 src/main/scala/typeclasses/Functor.scala delete mode 100644 src/main/scala/typeclasses/Monad.scala delete mode 100644 src/main/scala/typeclasses/syntax/gridZipper.scala delete mode 100644 src/main/scala/typeclasses/syntax/zipper.scala create mode 100644 src/test/scala/conway/data/GridZipperSpec.scala create mode 100644 src/test/scala/conway/data/ZipperComonadSpec.scala create mode 100644 src/test/scala/conway/data/ZipperLawsSpec.scala rename src/test/scala/{typeclasses => conway}/data/ZipperSpec.scala (53%) delete mode 100644 src/test/scala/typeclasses/data/GridZipperSpec.scala delete mode 100644 src/test/scala/typeclasses/data/ZipperComonadSpec.scala diff --git a/build.sbt b/build.sbt index 331a47d..3cf7a6b 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ name := "comonadic_life" version := "0.1" -scalaVersion := "2.12.9" +scalaVersion := "2.13.1" scalacOptions ++= Seq( "-deprecation", @@ -12,17 +12,19 @@ scalacOptions ++= Seq( "-language:higherKinds", "-language:implicitConversions", "-unchecked", - "-Ypartial-unification", "-Ywarn-numeric-widen" ) -scalacOptions in (Compile, console) --= Seq("-Ywarn-unused:imports", "-Xfatal-warnings") +scalacOptions in(Compile, console) --= Seq("-Ywarn-unused:imports", "-Xfatal-warnings") libraryDependencies ++= Seq( - "org.scalatest" %% "scalatest" % "3.0.8" % Test, - "org.scalactic" %% "scalactic" % "3.0.8", - "org.typelevel" %% "cats-effect" % "2.0.0", - "co.fs2" %% "fs2-core" % "2.1.0", - "com.lihaoyi" %% "pprint" % "0.5.6" + "org.scalactic" %% "scalactic" % "3.0.8", + "org.typelevel" %% "cats-core" % "2.0.0", + "org.typelevel" %% "cats-effect" % "2.0.0", + "co.fs2" %% "fs2-core" % "2.1.0", + "com.lihaoyi" %% "pprint" % "0.5.6", + "org.scalatest" %% "scalatest" % "3.0.8" % Test, + "com.github.alexarchambault" %% "scalacheck-shapeless_1.14" % "1.2.3" % Test, + "org.typelevel" %% "cats-testkit-scalatest" % "1.0.0-RC1" % Test ) -mainClass in (Compile, run) := Some("conway.Main") +mainClass in(Compile, run) := Some("conway.Main") diff --git a/src/main/scala/conway/Console.scala b/src/main/scala/conway/Console.scala index e284d55..9475d16 100644 --- a/src/main/scala/conway/Console.scala +++ b/src/main/scala/conway/Console.scala @@ -2,7 +2,6 @@ package conway import cats.effect.Sync import cats.syntax.all._ -import conway.Main.Coordinates import scala.util.Try diff --git a/src/main/scala/conway/Game.scala b/src/main/scala/conway/Game.scala index 95cd7ce..f805cfd 100644 --- a/src/main/scala/conway/Game.scala +++ b/src/main/scala/conway/Game.scala @@ -1,8 +1,8 @@ package conway -import typeclasses.data.GridZipper -import typeclasses.data.GridZipper._ -import typeclasses.syntax.gridZipper._ +import conway.data.GridZipper +import cats.syntax.comonad._ +import cats.syntax.coflatMap._ object Game { diff --git a/src/main/scala/conway/Main.scala b/src/main/scala/conway/Main.scala index f40592f..d952067 100644 --- a/src/main/scala/conway/Main.scala +++ b/src/main/scala/conway/Main.scala @@ -1,7 +1,6 @@ package conway -import typeclasses.data.GridZipper -import typeclasses.syntax.gridZipper._ +import conway.data.GridZipper import cats.effect.{ExitCode, IO, IOApp, Sync, Timer} import cats.syntax.all._ import conway.Game._ @@ -11,8 +10,6 @@ import scala.concurrent.duration._ object Main extends IOApp { - type Coordinates = (Int, Int) - def createCoordinateLists(width: Int): List[List[Coordinates]] = { val coords: List[Coordinates] = (for { x <- 0 until width @@ -27,7 +24,6 @@ object Main extends IOApp { gridZipperCoordinates.map(setCellValue) } - def setCellValue(coord: (Int, Int), initialStateMap: Map[Coordinates, Int]): Int = { initialStateMap.getOrElse(coord, 0) } diff --git a/src/main/scala/conway/Patterns.scala b/src/main/scala/conway/Patterns.scala index 49ec5e8..b9fcc87 100644 --- a/src/main/scala/conway/Patterns.scala +++ b/src/main/scala/conway/Patterns.scala @@ -1,7 +1,5 @@ package conway -import conway.Main.Coordinates - sealed trait Patterns { val shape: Map[Coordinates, Int] val value: Int diff --git a/src/main/scala/conway/Renderer.scala b/src/main/scala/conway/Renderer.scala index 67850eb..ca8d64e 100644 --- a/src/main/scala/conway/Renderer.scala +++ b/src/main/scala/conway/Renderer.scala @@ -1,10 +1,9 @@ package conway -import typeclasses.data.GridZipper -import typeclasses.syntax.gridZipper._ -import typeclasses.syntax.zipper._ +import conway.data.GridZipper import cats.effect.Sync -import cats.syntax.all._ +import cats.syntax.functor._ +import cats.syntax.flatMap._ import scala.sys.process._ diff --git a/src/main/scala/conway/Visualization.scala b/src/main/scala/conway/Visualization.scala index 0173288..2a72af6 100644 --- a/src/main/scala/conway/Visualization.scala +++ b/src/main/scala/conway/Visualization.scala @@ -1,6 +1,5 @@ package conway - trait Visualization { val alive: String val background: String diff --git a/src/main/scala/typeclasses/data/GridZipper.scala b/src/main/scala/conway/data/GridZipper.scala similarity index 52% rename from src/main/scala/typeclasses/data/GridZipper.scala rename to src/main/scala/conway/data/GridZipper.scala index 0ab0f29..87f3339 100644 --- a/src/main/scala/typeclasses/data/GridZipper.scala +++ b/src/main/scala/conway/data/GridZipper.scala @@ -1,9 +1,8 @@ -package typeclasses.data +package conway.data -import typeclasses.Comonad -import typeclasses.data.Zipper._ -import typeclasses.syntax.gridZipper._ -import typeclasses.syntax.zipper._ +import cats.{Comonad, Eq} +import cats.syntax.comonad._ +import cats.syntax.functor._ // 2 dimensions represented by nested Zippers case class GridZipper[A](value: Zipper[Zipper[A]]) { @@ -13,25 +12,20 @@ case class GridZipper[A](value: Zipper[Zipper[A]]) { GridZipper(value.setFocus(inner)) } - def prettyPrint: String = { + def prettyPrint: String = value.toList.map(x => x.prettyPrint).mkString("\n") - } - def north: GridZipper[A] = { + def north: GridZipper[A] = GridZipper(value.moveLeft) - } - def south: GridZipper[A] = { + def south: GridZipper[A] = GridZipper(value.moveRight) - } - def east: GridZipper[A] = { + def east: GridZipper[A] = GridZipper(value.map(xAxis => xAxis.moveRight)) - } - def west: GridZipper[A] = { + def west: GridZipper[A] = GridZipper(value.map(xAxis => xAxis.moveLeft)) - } def getNeighbors: List[A] = { List( @@ -49,15 +43,18 @@ case class GridZipper[A](value: Zipper[Zipper[A]]) { object GridZipper { - def fromLists[A](lists: List[List[A]]): GridZipper[A] = { + def fromLists[A](lists: List[List[A]]): GridZipper[A] = GridZipper(Zipper.fromList(lists.map(Zipper.fromList))) - } - implicit def gridZipperComonad: Comonad[GridZipper] = { + implicit def gridZipperEq[A: Eq]: Eq[GridZipper[A]] = + Eq.by(_.value) + + implicit val gridZipperComonad: Comonad[GridZipper] = new Comonad[GridZipper] { - override def extract[A](w: GridZipper[A]): A = w.value.focus.focus + override def extract[A](w: GridZipper[A]): A = + w.value.focus.focus - override def duplicate[A](w: GridZipper[A]): GridZipper[GridZipper[A]] = { + override def coflatten[A](w: GridZipper[A]): GridZipper[GridZipper[A]] = { val s1: Zipper[Zipper[Zipper[A]]] = nest(w.value) val s2: Zipper[Zipper[Zipper[Zipper[A]]]] = nest(s1) val g1: GridZipper[Zipper[Zipper[A]]] = GridZipper(s2) @@ -65,20 +62,24 @@ object GridZipper { g2 } - override def map[A, B](fa: GridZipper[A])(f: A => B): GridZipper[B] = GridZipper(fa.value.map(s => s.map(f))) + override def coflatMap[A, B](w: GridZipper[A])(f: GridZipper[A] => B): GridZipper[B] = + map(coflatten(w))(f) + + override def map[A, B](gz: GridZipper[A])(f: A => B): GridZipper[B] = + GridZipper(gz.value.map(z => z map f)) private def nest[A](s: Zipper[Zipper[A]]): Zipper[Zipper[Zipper[A]]] = { - val duplicateLefts: Stream[Zipper[Zipper[A]]] = { -// Zipper.unfold(s)(z => z.maybeLeft.flatMap(y => y.maybeLeft.map(x => (x,x)))) - Stream.iterate(s)(current => current.map(_.moveLeft)) + val duplicateLefts: LazyList[Zipper[Zipper[A]]] = { + // Zipper.unfold(s)(z => z.maybeLeft.flatMap(y => y.maybeLeft.map(x => (x,x)))) + LazyList.iterate(s)(current => current.map(_.moveLeft)) .tail .zip(s.left) .map(_._1) } - val duplicateRights: Stream[Zipper[Zipper[A]]] = -// Zipper.unfold(s)(z => z.maybeRight.flatMap(y => y.maybeRight.map(x => (x,x)))) - Stream.iterate(s)(current => current.map(_.moveRight)) + val duplicateRights: LazyList[Zipper[Zipper[A]]] = + // Zipper.unfold(s)(z => z.maybeRight.flatMap(y => y.maybeRight.map(x => (x,x)))) + LazyList.iterate(s)(current => current.map(_.moveRight)) .tail .zip(s.right) .map(_._1) @@ -86,5 +87,4 @@ object GridZipper { Zipper(duplicateLefts, s, duplicateRights) } } - } } diff --git a/src/main/scala/typeclasses/data/Zipper.scala b/src/main/scala/conway/data/Zipper.scala similarity index 54% rename from src/main/scala/typeclasses/data/Zipper.scala rename to src/main/scala/conway/data/Zipper.scala index 18684f5..a97344d 100644 --- a/src/main/scala/typeclasses/data/Zipper.scala +++ b/src/main/scala/conway/data/Zipper.scala @@ -1,9 +1,10 @@ -package typeclasses.data +package conway.data -import typeclasses.Comonad -import typeclasses.data.Zipper.unfold +import cats.{Comonad, Eq} +import cats.instances.lazyList._ +import conway.data.Zipper.unfold -case class Zipper[A](left: Stream[A], focus: A, right: Stream[A]) { +case class Zipper[A](left: LazyList[A], focus: A, right: LazyList[A]) { def maybeRight: Option[Zipper[A]] = right match { case nextRight #:: rights => Some(Zipper(focus #:: left, nextRight, rights)) @@ -21,7 +22,7 @@ case class Zipper[A](left: Stream[A], focus: A, right: Stream[A]) { def moveRight: Zipper[A] = { if (right.isEmpty) this - else Zipper(focus #:: left , right.head, right.tail) + else Zipper(focus #:: left, right.head, right.tail) } def moveLeft: Zipper[A] = { @@ -36,42 +37,45 @@ case class Zipper[A](left: Stream[A], focus: A, right: Stream[A]) { leftValues ++ focus ++ rightValues } - def toList: List[A] = { + def toList: List[A] = left.toList.reverse ++ (focus +: right.toList) - } - def toStream: Stream[A] = { + def toStream: LazyList[A] = left.reverse #::: (focus #:: right) - } - def duplicateRight[B](f:Zipper[A] => B): Stream[B] = + def duplicateRight[B](f: Zipper[A] => B): LazyList[B] = unfold(this)(z => z.maybeRight.map(x => (f(x), x))) - def duplicateLeft[B](f:Zipper[A] => B): Stream[B] = + def duplicateLeft[B](f: Zipper[A] => B): LazyList[B] = unfold(this)(z => z.maybeLeft.map(x => (f(x), x))) } object Zipper { - def fromList[A](items: List[A]): Zipper[A] = { - // Will throw if items is empty, so beware! - Zipper(items.tail.toStream, items.head, Stream.empty) - } + def fromList[A](items: List[A]): Zipper[A] = + Zipper(LazyList.from(items.tail), items.head, LazyList.empty) - def unfold[A, B](a: A)(f: A => Option[(B, A)]): Stream[B] = f(a) match { - case Some((b, a)) => b #:: unfold(a)(f) - case None => Stream.empty + + def unfold[A, B](a: A)(f: A => Option[(B, A)]): LazyList[B] = + f(a) match { + case Some((b, a)) => b #:: unfold(a)(f) + case None => LazyList.empty + } + + + implicit def zipperEq[A: Eq]: Eq[Zipper[A]] = { + import Eq._ + and(and(by(_.left),by(_.focus)), by(_.right)) } implicit def zipperComonad: Comonad[Zipper] = new Comonad[Zipper] { - override def extract[A](w: Zipper[A]): A = w.focus + override def extract[A](w: Zipper[A]): A = + w.focus - override def duplicate[A](w: Zipper[A]): Zipper[Zipper[A]] = { - Zipper(w.duplicateLeft(identity), w, w.duplicateRight(identity)) - } + override def map[A, B](fa: Zipper[A])(f: A => B): Zipper[B] = + Zipper(fa.left.map(f), f(fa.focus), fa.right.map(f)) - override def map[A, B](fa: Zipper[A])(f: A => B): Zipper[B] = { - Zipper(fa.left.map(f) ,f(fa.focus), fa.right.map(f)) - } + override def coflatMap[A, B](fa: Zipper[A])(f: Zipper[A] => B): Zipper[B] = + Zipper(fa.duplicateLeft(f), f(fa), fa.duplicateRight(f)) } } diff --git a/src/main/scala/conway/package.scala b/src/main/scala/conway/package.scala index 093d4d2..b5dde4b 100644 --- a/src/main/scala/conway/package.scala +++ b/src/main/scala/conway/package.scala @@ -1,9 +1,13 @@ -import conway.Main.Coordinates package object conway { + + type Coordinates = (Int, Int) + implicit class InitOps(presetShapes: Map[Coordinates, Int]) { - def at(coordinates: Coordinates): Map[Coordinates, Int] = presetShapes.map { - case ((x, y), v) => ((x + coordinates._2, y + coordinates._1), v) - } + def at(coordinates: Coordinates): Map[Coordinates, Int] = + presetShapes.map { + case ((x, y), v) => ((x + coordinates._2, y + coordinates._1), v) + } } + } diff --git a/src/main/scala/typeclasses/Comonad.scala b/src/main/scala/typeclasses/Comonad.scala deleted file mode 100644 index a5ca409..0000000 --- a/src/main/scala/typeclasses/Comonad.scala +++ /dev/null @@ -1,25 +0,0 @@ -package typeclasses - -trait Comonad[W[_]] extends Functor[W] { - // extracts a value from the context - also sometimes called counit - def extract[A](w: W[A]): A - // duplicates the context's structure - also called coflatten, - // replaces every value in the data structure with its corresponding context - def duplicate[A](w: W[A]): W[W[A]] - - /** - * Zipper - * [1,(2),3] => duplicate => - * [ - * [(1),2,3], - * [1,(2),3], - * [1,2,(3)] - * ] - */ - - // given a value in a context and a function which transforms and extracts the value, - // return the transformed value in the context - def coflatMap[A,B](w: W[A])(f: W[A] => B): W[B] = { - map(duplicate(w))(f) - } -} diff --git a/src/main/scala/typeclasses/Functor.scala b/src/main/scala/typeclasses/Functor.scala deleted file mode 100644 index da0de77..0000000 --- a/src/main/scala/typeclasses/Functor.scala +++ /dev/null @@ -1,5 +0,0 @@ -package typeclasses - -trait Functor[F[_]] { - def map[A, B](fa: F[A])(f: A => B ): F[B] -} diff --git a/src/main/scala/typeclasses/Monad.scala b/src/main/scala/typeclasses/Monad.scala deleted file mode 100644 index 7256953..0000000 --- a/src/main/scala/typeclasses/Monad.scala +++ /dev/null @@ -1,7 +0,0 @@ -package typeclasses - -trait Monad[M[_]] extends Functor[M] { - def pure[A](a: A): M[A] - def flatten[A](m: M[M[A]]): M[A] - def flatMap[A, B](m: M[A])(f: A => M[B]): M[B] -} diff --git a/src/main/scala/typeclasses/syntax/gridZipper.scala b/src/main/scala/typeclasses/syntax/gridZipper.scala deleted file mode 100644 index 6722de9..0000000 --- a/src/main/scala/typeclasses/syntax/gridZipper.scala +++ /dev/null @@ -1,16 +0,0 @@ -package typeclasses.syntax - -import typeclasses.Comonad -import typeclasses.data.GridZipper - -object gridZipper { - implicit class gridZipperSyntax[A](self: GridZipper[A])(implicit c: Comonad[GridZipper]) { - def extract: A = c.extract(self) - - def duplicate: GridZipper[GridZipper[A]] = c.duplicate(self) - - def coflatMap[B](f: GridZipper[A] => B): GridZipper[B] = c.coflatMap(self)(f) - - def map[B](f: A => B): GridZipper[B] = c.map(self)(f) - } -} diff --git a/src/main/scala/typeclasses/syntax/zipper.scala b/src/main/scala/typeclasses/syntax/zipper.scala deleted file mode 100644 index c99705a..0000000 --- a/src/main/scala/typeclasses/syntax/zipper.scala +++ /dev/null @@ -1,16 +0,0 @@ -package typeclasses.syntax - -import typeclasses.Comonad -import typeclasses.data.Zipper - -object zipper { - implicit class zipperSyntax[A](self: Zipper[A])(implicit c: Comonad[Zipper]) { - def extract: A = c.extract(self) - - def duplicate: Zipper[Zipper[A]] = c.duplicate(self) - - def coflatMap[B](f: Zipper[A] => B): Zipper[B] = c.coflatMap(self)(f) - - def map[B](f: A => B): Zipper[B] = c.map(self)(f) - } -} diff --git a/src/test/scala/conway/GameSpec.scala b/src/test/scala/conway/GameSpec.scala index 5aa7902..8429e30 100644 --- a/src/test/scala/conway/GameSpec.scala +++ b/src/test/scala/conway/GameSpec.scala @@ -1,14 +1,13 @@ package conway -import typeclasses.data.GridZipper +import conway.data.GridZipper import conway.Game.cellLifecycle -import conway.Main.Coordinates -import org.scalatest._ -import typeclasses.syntax.gridZipper._ +import org.scalatest.{FlatSpec, Matchers} +import cats.syntax.coflatMap._ class GameSpec extends FlatSpec with Matchers { - def setGridState(width: Int, desiredActiveCells: List[Coordinates])(test: (GridZipper[Int]) => Unit): Unit = { + def setGridState(width: Int, desiredActiveCells: List[Coordinates])(test: GridZipper[Int] => Unit): Unit = { val coordMap: Map[(Int, Int), Int] = desiredActiveCells.map(coordinates => coordinates -> 1).toMap val initialGridState: GridZipper[Int] = Main.buildGrid((coords) => coordMap.getOrElse(coords, 0), width) println("INITIAL GRID STATE") diff --git a/src/test/scala/conway/data/GridZipperSpec.scala b/src/test/scala/conway/data/GridZipperSpec.scala new file mode 100644 index 0000000..afdd1a4 --- /dev/null +++ b/src/test/scala/conway/data/GridZipperSpec.scala @@ -0,0 +1,174 @@ +package conway.data + +import org.scalatest._ +import conway.data.Zipper._ +import cats.syntax.coflatMap._ +import cats.syntax.comonad._ + + +class GridZipperSpec extends FlatSpec with Matchers { + + "extract" should "extract the focus" in { + val streamZipper = Zipper(LazyList(1), 2, LazyList(3)) + val largeStreamZipper: Zipper[Zipper[Int]] = streamZipper.coflatten + val gridZipper = GridZipper(largeStreamZipper) + + gridZipper.extract shouldBe 2 + } + it should "shift left and focus" in { + val streamZipper = Zipper(LazyList(1), 2, LazyList(3)) + val largeStreamZipper: Zipper[Zipper[Int]] = streamZipper.coflatten + val gridZipper = GridZipper(largeStreamZipper) + + GridZipper(gridZipper.value.moveLeft).extract shouldBe 1 + } + "duplicate" should "handle basic case" in { + val streamZipper = Zipper(LazyList(), 2, LazyList()) + val largeStreamZipper: Zipper[Zipper[Int]] = streamZipper.coflatten + val gridZipper = GridZipper(largeStreamZipper) + + val streamOfGrid: Zipper[Zipper[GridZipper[Int]]] = Zipper( + LazyList(), + Zipper( + LazyList(), + gridZipper, + LazyList() + ), + LazyList() + ) + val expectedGridZipper: GridZipper[GridZipper[Int]] = GridZipper( + streamOfGrid + ) + + gridZipper.coflatten shouldBe expectedGridZipper + } + it should "duplicate more complicated zippers" in pendingUntilFixed { + val streamZipper = Zipper(LazyList(1), 2, LazyList()) + val largeStreamZipper: Zipper[Zipper[Int]] = streamZipper.coflatten + val gridZipper = GridZipper(largeStreamZipper) + + val expectedGridZipper: GridZipper[GridZipper[Int]] = GridZipper( + Zipper( + LazyList( + Zipper( + LazyList(), + GridZipper( + Zipper( + LazyList(), + Zipper(LazyList(), 1, LazyList(2)), + LazyList(Zipper(LazyList(1), 2, LazyList())) + ) + ), + LazyList( + GridZipper( + Zipper( + LazyList(Zipper(LazyList(), 1, LazyList(2))), + Zipper(LazyList(1), 2, LazyList()), + LazyList() + ) + ) + ) + ) + ), + Zipper( + LazyList( + GridZipper( + Zipper( + LazyList(), + Zipper(LazyList(), 1, LazyList(2)), + LazyList(Zipper(LazyList(1), 2, LazyList())) + ) + ) + ), + GridZipper( + Zipper( + LazyList(Zipper(LazyList(), 1, LazyList(2))), + Zipper(LazyList(1), 2, LazyList()), + LazyList() + ) + ), + LazyList() + ), + LazyList() + ) + ) + + gridZipper.coflatten shouldBe expectedGridZipper + } + it should "have right identity" in { + val streamZipper = Zipper(LazyList(), 2, LazyList()) + val largeStreamZipper: Zipper[Zipper[Int]] = streamZipper.coflatten + val gridZipper = GridZipper(largeStreamZipper) + + gridZipper.coflatMap(_.extract) shouldBe gridZipper + } + it should "have valid right identity with >1 elements" in pendingUntilFixed { + val streamZipper = Zipper(LazyList(1, 2, 3, 4), 5, LazyList()) + val largeStreamZipper: Zipper[Zipper[Int]] = streamZipper.coflatten + val gridZipper = GridZipper(largeStreamZipper) + + val result = gridZipper.coflatMap(_.extract) + + result shouldBe gridZipper + } + it should "have valid left identity" in { + val streamZipper = Zipper(LazyList(), 2, LazyList()) + val largeStreamZipper: Zipper[Zipper[Int]] = streamZipper.coflatten + val gridZipper = GridZipper(largeStreamZipper) + + gridZipper.coflatten.extract shouldBe gridZipper + } + it should "have valid left identity with >1 elements" in { + val streamZipper = Zipper(LazyList(1, 2, 3, 4), 5, LazyList(6,7)) + val largeStreamZipper: Zipper[Zipper[Int]] = streamZipper.coflatten + val gridZipper = GridZipper(largeStreamZipper) + + val result = gridZipper.coflatten.extract + + result shouldBe gridZipper + } + it should "have associativity" in { + val streamZipper = Zipper(LazyList(), 2, LazyList()) + val largeStreamZipper: Zipper[Zipper[Int]] = streamZipper.coflatten + val gridZipper = GridZipper(largeStreamZipper) + + gridZipper.coflatten.coflatten shouldBe gridZipper.coflatMap(_.coflatten) + } + it should "have associativity with > 1 elements" in pendingUntilFixed { + val streamZipper = Zipper(LazyList(2,1), 3, LazyList(4,5)) + val largeStreamZipper: Zipper[Zipper[Int]] = streamZipper.coflatten + val gridZipper = GridZipper(largeStreamZipper) + + gridZipper.coflatten.coflatten shouldBe gridZipper.coflatMap(_.coflatten) + } + it should "have valid coflatmap" in { + val streamZipper = Zipper(LazyList(), 2, LazyList()) + val largeStreamZipper: Zipper[Zipper[Int]] = streamZipper.coflatten + val gridZipper = GridZipper(largeStreamZipper) + + def f(gridZipper: GridZipper[Int]): Int = { + gridZipper.extract + 1 + } + + gridZipper.coflatMap(f) shouldBe GridZipper(Zipper(LazyList(), 3, LazyList()).coflatten) + } + it should "handle more complicated coflatmap" in pendingUntilFixed { + val streamZipper = Zipper(LazyList(1), 2, LazyList()) + val largeStreamZipper: Zipper[Zipper[Int]] = streamZipper.coflatten + val gridZipper = GridZipper(largeStreamZipper) + + val expectedGridZipper: GridZipper[Int] = GridZipper( + Zipper( + LazyList(Zipper(LazyList(), 3, LazyList(4))), + Zipper(LazyList(3), 4, LazyList()), + LazyList() + ) + ) + + def f(gridZipper: GridZipper[Int]): Int = { + gridZipper.extract + 2 + } + + gridZipper.coflatMap(f) shouldBe expectedGridZipper + } +} diff --git a/src/test/scala/conway/data/ZipperComonadSpec.scala b/src/test/scala/conway/data/ZipperComonadSpec.scala new file mode 100644 index 0000000..6c76640 --- /dev/null +++ b/src/test/scala/conway/data/ZipperComonadSpec.scala @@ -0,0 +1,42 @@ +package conway.data + +import org.scalatest._ +import cats.syntax.comonad._ +import cats.syntax.coflatMap._ +import cats.syntax.functor._ + +class ZipperComonadSpec extends FlatSpec with Matchers { + + val initialZipper: Zipper[Int] = Zipper(LazyList(4, 3, 2, 1), 5, LazyList(6, 7, 8)) + + "ZipperComonad" should "have a valid extract" in { + initialZipper.extract shouldBe 5 + } + it should "have a valid duplicate" in { + val smallZipper = Zipper(LazyList(1), 2, LazyList(3)) + val expected = Zipper( + LazyList(Zipper(LazyList.empty, 1, LazyList(2, 3))), + smallZipper, + LazyList(Zipper(LazyList(2, 1), 3, LazyList.empty)) + ) + smallZipper.coflatten shouldBe expected + } + it should "have valid right identity" in { + initialZipper.coflatMap(_.extract) shouldBe initialZipper + } + it should "have valid left identity" in { + initialZipper.coflatten.extract shouldBe initialZipper + } + it should "have associativity" in { + initialZipper.coflatten.coflatten shouldBe initialZipper.coflatMap(_.coflatten) + } + it should "have valid coflatMap" in { + def sumLeftRight(z: Zipper[Int]): Int = + z.focus + z.moveLeft.focus + z.moveRight.focus + + initialZipper.coflatMap(sumLeftRight).toList shouldBe List(4, 6, 9, 12, 15, 18, 21, 23) + } + it should "have a valid map" in { + initialZipper.map(_.toString).toList shouldBe List("1", "2", "3", "4", "5", "6", "7", "8") + } +} diff --git a/src/test/scala/conway/data/ZipperLawsSpec.scala b/src/test/scala/conway/data/ZipperLawsSpec.scala new file mode 100644 index 0000000..7341846 --- /dev/null +++ b/src/test/scala/conway/data/ZipperLawsSpec.scala @@ -0,0 +1,10 @@ +package conway.data + +import cats.laws.discipline.ComonadTests +import cats.tests.CatsSuite +import org.scalacheck.ScalacheckShapeless + +class ZipperLawsSpec extends CatsSuite with ScalacheckShapeless { + checkAll("Zipper.ComonadLaws", ComonadTests[Zipper].comonad[Int, Int, String]) + checkAll("GridZipper.ComonadLaws", ComonadTests[GridZipper].comonad[Int, Int, String]) +} diff --git a/src/test/scala/typeclasses/data/ZipperSpec.scala b/src/test/scala/conway/data/ZipperSpec.scala similarity index 53% rename from src/test/scala/typeclasses/data/ZipperSpec.scala rename to src/test/scala/conway/data/ZipperSpec.scala index 9f8dff5..937d260 100644 --- a/src/test/scala/typeclasses/data/ZipperSpec.scala +++ b/src/test/scala/conway/data/ZipperSpec.scala @@ -1,44 +1,44 @@ -package typeclasses.data +package conway.data import org.scalatest._ class ZipperSpec extends FlatSpec with Matchers { - val initialZipper: Zipper[Int] = Zipper(Stream(4,3,2,1), 5, Stream(6,7,8)) + val initialZipper: Zipper[Int] = Zipper(LazyList(4,3,2,1), 5, LazyList(6,7,8)) "Zipper" should "have valid focus" in { initialZipper.focus shouldBe 5 } it should "shift focus left" in { - initialZipper.moveLeft shouldBe Zipper(Stream(3,2,1), 4, Stream(5,6,7,8)) + initialZipper.moveLeft shouldBe Zipper(LazyList(3,2,1), 4, LazyList(5,6,7,8)) } it should "stop shifting left when no more lefts present" in { - val leftEmpty = Zipper(Stream.empty, 1, Stream(2,3)) + val leftEmpty = Zipper(LazyList.empty, 1, LazyList(2,3)) leftEmpty.moveLeft shouldBe leftEmpty } it should "shift focus right" in { - initialZipper.moveRight shouldBe Zipper(Stream(5,4,3,2,1), 6, Stream(7,8)) + initialZipper.moveRight shouldBe Zipper(LazyList(5,4,3,2,1), 6, LazyList(7,8)) } it should "stop shifting right when no more right's present" in { - val rightEmpty = Zipper(Stream(2), 1, Stream.empty) + val rightEmpty = Zipper(LazyList(2), 1, LazyList.empty) rightEmpty.moveRight shouldBe rightEmpty } it should "create a list from the zipper intuitively" in { initialZipper.toList shouldBe List(1,2,3,4,5,6,7,8) } it should "duplicateLeft" in { - val smallerZipper: Zipper[Int] = Zipper(Stream(2,1), 3, Stream(4)) + val smallerZipper: Zipper[Int] = Zipper(LazyList(2,1), 3, LazyList(4)) smallerZipper.duplicateLeft(identity).toList shouldBe List( - Zipper(Stream(1), 2, Stream(3,4)), - Zipper(Stream(), 1, Stream(2,3,4)) + Zipper(LazyList(1), 2, LazyList(3,4)), + Zipper(LazyList(), 1, LazyList(2,3,4)) ) } it should "duplicateRight" in { - val smallerZipper: Zipper[Int] = Zipper(Stream(1), 2, Stream(3,4)) + val smallerZipper: Zipper[Int] = Zipper(LazyList(1), 2, LazyList(3,4)) smallerZipper.duplicateRight(identity).toList shouldBe List( - Zipper(Stream(2,1), 3, Stream(4)), - Zipper(Stream(3,2,1),4,Stream()) + Zipper(LazyList(2,1), 3, LazyList(4)), + Zipper(LazyList(3,2,1),4,LazyList()) ) } } diff --git a/src/test/scala/typeclasses/data/GridZipperSpec.scala b/src/test/scala/typeclasses/data/GridZipperSpec.scala deleted file mode 100644 index 48306a8..0000000 --- a/src/test/scala/typeclasses/data/GridZipperSpec.scala +++ /dev/null @@ -1,174 +0,0 @@ -package typeclasses.data - -import org.scalatest._ -import typeclasses.data.Zipper._ -import typeclasses.syntax.gridZipper._ -import typeclasses.syntax.zipper._ - -class GridZipperSpec extends FlatSpec with Matchers { - - "extract" should "extract the focus" in { - val streamZipper = Zipper(Stream(1), 2, Stream(3)) - val largeStreamZipper: Zipper[Zipper[Int]] = streamZipper.duplicate - val gridZipper = GridZipper(largeStreamZipper) - - gridZipper.extract shouldBe 2 - } - it should "shift left and focus" in { - val streamZipper = Zipper(Stream(1), 2, Stream(3)) - val largeStreamZipper: Zipper[Zipper[Int]] = streamZipper.duplicate - val gridZipper = GridZipper(largeStreamZipper) - - GridZipper(gridZipper.value.moveLeft).extract shouldBe 1 - } - "duplicate" should "handle basic case" in { - val streamZipper = Zipper(Stream(), 2, Stream()) - val largeStreamZipper: Zipper[Zipper[Int]] = streamZipper.duplicate - val gridZipper = GridZipper(largeStreamZipper) - - val streamOfGrid: Zipper[Zipper[GridZipper[Int]]] = Zipper( - Stream(), - Zipper( - Stream(), - gridZipper, - Stream() - ), - Stream() - ) - val expectedGridZipper: GridZipper[GridZipper[Int]] = GridZipper( - streamOfGrid - ) - - gridZipper.duplicate shouldBe expectedGridZipper - } - it should "duplicate more complicated zippers" in pendingUntilFixed { - val streamZipper = Zipper(Stream(1), 2, Stream()) - val largeStreamZipper: Zipper[Zipper[Int]] = streamZipper.duplicate - val gridZipper = GridZipper(largeStreamZipper) - - val expectedGridZipper: GridZipper[GridZipper[Int]] = GridZipper( - Zipper( - Stream( - Zipper( - Stream(), - GridZipper( - Zipper( - Stream(), - Zipper(Stream(), 1, Stream(2)), - Stream(Zipper(Stream(1), 2, Stream())) - ) - ), - Stream( - GridZipper( - Zipper( - Stream(Zipper(Stream(), 1, Stream(2))), - Zipper(Stream(1), 2, Stream()), - Stream() - ) - ) - ) - ) - ), - Zipper( - Stream( - GridZipper( - Zipper( - Stream(), - Zipper(Stream(), 1, Stream(2)), - Stream(Zipper(Stream(1), 2, Stream())) - ) - ) - ), - GridZipper( - Zipper( - Stream(Zipper(Stream(), 1, Stream(2))), - Zipper(Stream(1), 2, Stream()), - Stream() - ) - ), - Stream() - ), - Stream() - ) - ) - - // GridZ[StreamZ[StreamZ[GridZ[StreamZ[StreamZ[Int]]]]]]] - gridZipper.duplicate shouldBe expectedGridZipper - } - it should "have right identity" in { - val streamZipper = Zipper(Stream(), 2, Stream()) - val largeStreamZipper: Zipper[Zipper[Int]] = streamZipper.duplicate - val gridZipper = GridZipper(largeStreamZipper) - - gridZipper.coflatMap(_.extract) shouldBe gridZipper - } - it should "have valid right identity with >1 elements" in pendingUntilFixed { - val streamZipper = Zipper(Stream(1, 2, 3, 4), 5, Stream()) - val largeStreamZipper: Zipper[Zipper[Int]] = streamZipper.duplicate - val gridZipper = GridZipper(largeStreamZipper) - - val result = gridZipper.coflatMap(_.extract) - - result shouldBe gridZipper - } - it should "have valid left identity" in { - val streamZipper = Zipper(Stream(), 2, Stream()) - val largeStreamZipper: Zipper[Zipper[Int]] = streamZipper.duplicate - val gridZipper = GridZipper(largeStreamZipper) - - gridZipper.duplicate.extract shouldBe gridZipper - } - it should "have valid left identity with >1 elements" in { - val streamZipper = Zipper(Stream(1, 2, 3, 4), 5, Stream(6,7)) - val largeStreamZipper: Zipper[Zipper[Int]] = streamZipper.duplicate - val gridZipper = GridZipper(largeStreamZipper) - - val result = gridZipper.duplicate.extract - - result shouldBe gridZipper - } - it should "have associativity" in { - val streamZipper = Zipper(Stream(), 2, Stream()) - val largeStreamZipper: Zipper[Zipper[Int]] = streamZipper.duplicate - val gridZipper = GridZipper(largeStreamZipper) - - gridZipper.duplicate.duplicate shouldBe gridZipper.coflatMap(_.duplicate) - } - it should "have associativity with > 1 elements" in pendingUntilFixed { - val streamZipper = Zipper(Stream(2,1), 3, Stream(4,5)) - val largeStreamZipper: Zipper[Zipper[Int]] = streamZipper.duplicate - val gridZipper = GridZipper(largeStreamZipper) - - gridZipper.duplicate.duplicate shouldBe gridZipper.coflatMap(_.duplicate) - } - it should "have valid coflatmap" in { - val streamZipper = Zipper(Stream(), 2, Stream()) - val largeStreamZipper: Zipper[Zipper[Int]] = streamZipper.duplicate - val gridZipper = GridZipper(largeStreamZipper) - - def f(gridZipper: GridZipper[Int]): Int = { - gridZipper.extract + 1 - } - - gridZipper.coflatMap(f) shouldBe GridZipper(Zipper(Stream(), 3, Stream()).duplicate) - } - it should "handle more complicated coflatmap" in pendingUntilFixed { - val streamZipper = Zipper(Stream(1), 2, Stream()) - val largeStreamZipper: Zipper[Zipper[Int]] = streamZipper.duplicate - val gridZipper = GridZipper(largeStreamZipper) - - val expectedGridZipper: GridZipper[Int] = GridZipper( - Zipper( - Stream(Zipper(Stream(), 3, Stream(4))), - Zipper(Stream(3), 4, Stream()), - Stream() - ) - ) - - def f(gridZipper: GridZipper[Int]): Int = { - gridZipper.extract + 2 - } - - gridZipper.coflatMap(f) shouldBe expectedGridZipper - } -} diff --git a/src/test/scala/typeclasses/data/ZipperComonadSpec.scala b/src/test/scala/typeclasses/data/ZipperComonadSpec.scala deleted file mode 100644 index f532488..0000000 --- a/src/test/scala/typeclasses/data/ZipperComonadSpec.scala +++ /dev/null @@ -1,41 +0,0 @@ -package typeclasses.data - -import org.scalatest._ -import typeclasses.syntax.zipper._ - -class ZipperComonadSpec extends FlatSpec with Matchers { - - val initialZipper: Zipper[Int] = Zipper(Stream(4,3,2,1), 5, Stream(6,7,8)) - - "ZipperComonad" should "have a valid extract" in { - initialZipper.extract shouldBe 5 - } - it should "have a valid duplicate" in { - val smallZipper: Zipper[Int] = Zipper(Stream(1), 2, Stream(3)) - - val expected = Zipper( - Stream(Zipper(Stream.empty, 1, Stream(2,3))), - smallZipper, - Stream(Zipper(Stream(2,1), 3, Stream.empty)) - ) - smallZipper.duplicate shouldBe expected - } - it should "have valid right identity" in { - initialZipper.coflatMap(_.extract) shouldBe initialZipper - } - it should "have valid left identity" in { - initialZipper.duplicate.extract shouldBe initialZipper - } - it should "have associativity" in { - initialZipper.duplicate.duplicate shouldBe initialZipper.coflatMap(_.duplicate) - } - it should "have valid coflatMap" in { - def sumLeftRight(streamZ: Zipper[Int]): Int = { - streamZ.focus + streamZ.moveLeft.focus + streamZ.moveRight.focus - } - initialZipper.coflatMap(sumLeftRight).toList shouldBe List(4,6,9,12,15,18,21,23) - } - it should "have a valid map" in { - initialZipper.map(_.toString).toStream shouldBe Stream("1", "2", "3", "4", "5", "6", "7", "8") - } -}