From 069ae69c050fc687d4216ba226109f01a8def750 Mon Sep 17 00:00:00 2001 From: David Gregory Date: Thu, 8 Mar 2018 21:49:25 +0000 Subject: [PATCH] Add syntax (fixes #3), improve test coverage (#1) --- build.sbt | 47 +++++++++++++++- .../src/main/scala/schemes/data/ListF.scala | 39 ++++++++++++- .../src/main/scala/schemes/package.scala | 41 ++++++++++++++ .../src/test/scala/schemes/SchemesSpec.scala | 56 ++++++++++++++++--- .../src/test/scala/schemes/SyntaxSpec.scala | 44 +++++++++++++++ .../test/scala/schemes/data/ListFSpec.scala | 30 ++++++++++ docs/src/main/resources/rootdoc.txt | 3 + 7 files changed, 247 insertions(+), 13 deletions(-) create mode 100644 core/shared/src/test/scala/schemes/SyntaxSpec.scala create mode 100644 core/shared/src/test/scala/schemes/data/ListFSpec.scala create mode 100644 docs/src/main/resources/rootdoc.txt diff --git a/build.sbt b/build.sbt index 87489e8..79addbd 100644 --- a/build.sbt +++ b/build.sbt @@ -7,7 +7,7 @@ lazy val schemes = project.in(file(".")) .settings(commonSettings) .settings(publishSettings) .settings(noPublishSettings) - .aggregate(coreJS, coreJVM) + .aggregate(coreJS, coreJVM, docs) lazy val core = crossProject.in(file("core")) .enablePlugins(TutPlugin) @@ -22,6 +22,51 @@ lazy val core = crossProject.in(file("core")) lazy val coreJS = core.js lazy val coreJVM = core.jvm +lazy val docs = project.in(file("docs")) + .enablePlugins(MicrositesPlugin) + .enablePlugins(ScalaUnidocPlugin) + .settings(commonSettings) + .settings(publishSettings) + .settings(noPublishSettings) + .settings( + moduleName := "schemes-docs", + + ghpagesNoJekyll := false, + git.remoteRepo := "git@github.com:DavidGregory084/schemes.git", + addMappingsToSiteDir(mappings.in(ScalaUnidoc, packageDoc), micrositeDocumentationUrl), + + scalacOptions.in(Tut) ~= filterConsoleScalacOptions, + + scalacOptions in (ScalaUnidoc, unidoc) ++= Seq( + "-groups", + "-implicits", + "-sourcepath", baseDirectory.in(LocalRootProject).value.getAbsolutePath, + "-doc-source-url", scmInfo.value.get.browseUrl + "/tree/master€{FILE_PATH}.scala", + "-doc-root-content", (resourceDirectory.in(Compile).value / "rootdoc.txt").getAbsolutePath + ), + + unidocProjectFilter.in(ScalaUnidoc, unidoc) := inAnyProject -- inProjects(coreJS), + + micrositeName := "schemes", + micrositeDescription := "A recursion schemes micro-library", + micrositeAuthor := "David Gregory", + micrositeHomepage := "https://DavidGregory084.github.io/schemes", + micrositeBaseUrl := "/schemes", + micrositeDocumentationUrl := "api", + micrositeGithubOwner := "DavidGregory084", + micrositeGithubRepo := "schemes", + micrositeExtraMdFiles := Map(file("README.md") -> microsites.ExtraMdFileConfig("index.md", "home")), + micrositePalette := Map( + "brand-primary" -> "#424242", + "brand-secondary" -> "#7A7A7A", + "brand-tertiary" -> "#3F3F3F", + "gray-dark" -> "#453E46", + "gray" -> "#837F84", + "gray-light" -> "#E3E2E3", + "gray-lighter" -> "#F4F3F4", + "white-color" -> "#FFFFFF"), + includeFilter in makeSite := "*.html" | "*.css" | "*.png" | "*.jpg" | "*.gif" | "*.js" | "*.swf" | "*.yml" | "*.md" + ) lazy val commonSettings = Def.settings( organization := "io.github.davidgregory084", diff --git a/core/shared/src/main/scala/schemes/data/ListF.scala b/core/shared/src/main/scala/schemes/data/ListF.scala index f9be46d..c880e23 100644 --- a/core/shared/src/main/scala/schemes/data/ListF.scala +++ b/core/shared/src/main/scala/schemes/data/ListF.scala @@ -24,22 +24,55 @@ final case class ConsF[A, B](head: A, tail: B) extends ListF[A, B] final case class NilF[A, B]() extends ListF[A, B] object ListF { + def cons[A](head: A, tail: Fix[ListF[A, ?]]): Fix[ListF[A, ?]] = + Fix(ConsF(head, tail)) + + def nil[A]: Fix[ListF[A, ?]] = + Fix(NilF()) + def apply[A](as: A*): Fix[ListF[A, ?]] = Schemes.ana[ListF[A, ?], List[A]](as.toList) { case Nil => NilF() case h :: t => ConsF(h, t) } - def toList[A](list: Fix[ListF[A, ?]]) = + def toList[A](list: Fix[ListF[A, ?]]): List[A] = Schemes.cata[ListF[A, ?], List[A]](list) { case NilF() => Nil case ConsF(h, t) => h :: t } - implicit def schemesListFFunctor[A]: Functor[ListF[A, ?]] = new Functor[ListF[A, ?]] { - def map[B, C](fa: ListF[A, B])(f: B => C): ListF[A, C] = fa match { + implicit def schemesListFEq[A, B](implicit A: Eq[A], B: Eq[B]) = new Eq[ListF[A, B]] { + def eqv(l: ListF[A, B], r: ListF[A, B]) = (l, r) match { + case (NilF(), NilF()) => true + case (NilF(), _) => false + case (_, NilF()) => false + case (ConsF(la, lb), ConsF(ra, rb)) => + A.eqv(la, ra) && B.eqv(lb, rb) + } + } + + implicit def schemesListFTraverse[A]: Traverse[ListF[A, ?]] = new Traverse[ListF[A, ?]] { + override def map[B, C](fa: ListF[A, B])(f: B => C): ListF[A, C] = fa match { case ConsF(head, tail) => ConsF(head, f(tail)) case nil @ NilF() => nil.asInstanceOf[ListF[A, C]] } + + def foldLeft[B, C](fa: ListF[A, B], c: C)(f: (C, B) => C): C = + fa match { + case ConsF(_, b) => f(c, b) + case NilF() => c + } + + def foldRight[B, C](fa: ListF[A, B], ec: Eval[C])(f: (B, Eval[C]) => Eval[C]): Eval[C] = + fa match { + case ConsF(_, b) => f(b, ec) + case NilF() => ec + } + + def traverse[F[_], B, C](fa: ListF[A, B])(f: B => F[C])(implicit F: Applicative[F]): F[ListF[A, C]] = fa match { + case ConsF(a, b) => F.map(f(b))(c => ConsF(a, c)) + case NilF() => F.pure(NilF()) + } } } diff --git a/core/shared/src/main/scala/schemes/package.scala b/core/shared/src/main/scala/schemes/package.scala index 0a04f84..687373e 100644 --- a/core/shared/src/main/scala/schemes/package.scala +++ b/core/shared/src/main/scala/schemes/package.scala @@ -14,11 +14,52 @@ * limitations under the License. */ +import cats.{ ~>, Functor, Monad, Traverse } +import cats.free.Cofree + package object schemes { val Fix: FixModule = FixImpl type Fix[F[_]] = Fix.Fix[F] + implicit class AnyOps[A](private val a: A) extends AnyVal { + def ana[F[_]](coalgebra: A => F[A])(implicit F: Functor[F]): Fix[F] = + Schemes.ana(a)(coalgebra) + + def anaM[M[_], F[_]](coalgebra: A => M[F[A]])(implicit M: Monad[M], T: Traverse[F]): M[Fix[F]] = + Schemes.anaM(a)(coalgebra) + + def hylo[F[_], B](coalgebra: A => F[A], algebra: F[B] => B)(implicit F: Functor[F]): B = + Schemes.hylo(a)(coalgebra, algebra) + + def hyloM[M[_], F[_], B](coalgebra: A => M[F[A]], algebra: F[B] => M[B])(implicit M: Monad[M], T: Traverse[F]): M[B] = + Schemes.hyloM(a)(coalgebra, algebra) + + def postpro[F[_]](coalgebra: A => F[A], post: F ~> F)(implicit F: Functor[F]): Fix[F] = + Schemes.postpro(a)(coalgebra, post) + + def elgot[F[_], B](elgotCoalgebra: A => Either[B, F[A]], algebra: F[B] => B)(implicit F: Functor[F]): B = + Schemes.elgot(a)(elgotCoalgebra, algebra) + + def coelgot[F[_], B](coalgebra: A => F[A], elgotAlgebra: (A, () => F[B]) => B)(implicit F: Functor[F]): B = + Schemes.coelgot(a)(coalgebra, elgotAlgebra) + + def apo[F[_]](coalgebra: A => F[Either[Fix[F], A]])(implicit F: Functor[F]): Fix[F] = + Schemes.apo(a)(coalgebra) + } + implicit class FixOps[F[_]](private val fix: Fix[F]) extends AnyVal { def unfix: F[Fix[F]] = Fix.unfix(fix) + + def cata[A](algebra: F[A] => A)(implicit F: Functor[F]): A = + Schemes.cata(fix)(algebra) + + def cataM[M[_], A](algebra: F[A] => M[A])(implicit M: Monad[M], T: Traverse[F]): M[A] = + Schemes.cataM(fix)(algebra) + + def prepro[A](pre: F ~> F, algebra: F[A] => A)(implicit F: Functor[F]): A = + Schemes.prepro(fix)(pre, algebra) + + def histo[A](algebra: F[Cofree[F, A]] => A)(implicit F: Functor[F]): A = + Schemes.histo(fix)(algebra) } } diff --git a/core/shared/src/test/scala/schemes/SchemesSpec.scala b/core/shared/src/test/scala/schemes/SchemesSpec.scala index edb08ff..4b34cf2 100644 --- a/core/shared/src/test/scala/schemes/SchemesSpec.scala +++ b/core/shared/src/test/scala/schemes/SchemesSpec.scala @@ -23,33 +23,65 @@ object MathExpr { case Add(l, r) => l + r } - implicit val mathExprFunctor: Functor[MathExpr] = new Functor[MathExpr] { - def map[A, B](expr: MathExpr[A])(f: A => B) = expr match { + implicit val mathExprTraverse: Traverse[MathExpr] = new Traverse[MathExpr] { + override def map[A, B](expr: MathExpr[A])(f: A => B) = expr match { case num @ Num(_) => num.retag[B] case Mul(l, r) => Mul(f(l), f(r)) case Add(l, r) => Add(f(l), f(r)) } + + def foldLeft[A, B](expr: MathExpr[A], b: B)(f: (B, A) => B): B = expr match { + case Num(_) => b + case Mul(l, r) => f(f(b, l), r) + case Add(l, r) => f(f(b, l), r) + } + + def foldRight[A, B](expr: MathExpr[A], eb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = expr match { + case Num(_) => eb + case Mul(l, r) => f(l, f(r, eb)) + case Add(l, r) => f(l, f(r, eb)) + } + + def traverse[F[_], A, B](expr: MathExpr[A])(f: A => F[B])(implicit F: Applicative[F]): F[MathExpr[B]] = expr match { + case num @ Num(_) => F.pure(num.retag[B]) + case Mul(l, r) => F.map2(f(l), f(r))(Mul(_, _)) + case Add(l, r) => F.map2(f(l), f(r))(Add(_, _)) + } } } class SchemesSpec extends FlatSpec with Matchers { + "unfix" should "unwrap a single layer of Fix" in { + import MathExpr._ + add(num(1), num(1)).unfix shouldBe Add[Fix[MathExpr]](num(1), num(1)) + } + "cata" should "evaluate MathExprs" in { import MathExpr._ val two = add(num(1), num(1)) - Schemes.cata(two)(evalAlgebra) shouldBe 2 + two.cata(evalAlgebra) shouldBe 2 val four = mul(num(2), num(2)) - Schemes.cata(four)(evalAlgebra) shouldBe 4 + four.cata(evalAlgebra) shouldBe 4 val sixteen = add(num(2), add(num(3), num(11))) - Schemes.cata(sixteen)(evalAlgebra) shouldBe 16 + sixteen.cata(evalAlgebra) shouldBe 16 + + two.cataM[Id, Int](evalAlgebra) shouldBe 2 } "ana" should "unfold MathExprs" in { import MathExpr._ - val unfoldAdd = Schemes.ana[MathExpr, Int](5) { i => + val unfoldAdd = 5.ana[MathExpr] { i => + if (i < 2) + Num(i) + else + Add(1, i - 1) + } + + val unfoldAddM = 5.anaM[Id, MathExpr] { i => if (i < 2) Num(i) else @@ -57,12 +89,18 @@ class SchemesSpec extends FlatSpec with Matchers { } unfoldAdd shouldBe add(num(1), add(num(1), add(num(1), add(num(1), num(1))))) + + unfoldAddM shouldBe add(num(1), add(num(1), add(num(1), add(num(1), num(1))))) } "hylo" should "unfold and then evaluate MathExprs" in { import MathExpr._ - Schemes.hylo[MathExpr, Int, Int](5)( + 5.hylo[MathExpr, Int]( + i => if (i < 2) Num(i) else Add(1, i - 1), + evalAlgebra) shouldBe 5 + + 5.hyloM[Id, MathExpr, Int]( i => if (i < 2) Num(i) else Add(1, i - 1), evalAlgebra) shouldBe 5 } @@ -80,7 +118,7 @@ class SchemesSpec extends FlatSpec with Matchers { val `1 to 10` = ListF(1 to 10: _*) - Schemes.prepro[ListF[Int, ?], Int](`1 to 10`)( + `1 to 10`.prepro[Int]( stopAtFive, sum) shouldBe 15 } @@ -93,7 +131,7 @@ class SchemesSpec extends FlatSpec with Matchers { val `1 to 5` = ListF(1 to 5: _*) - Schemes.postpro[ListF[Int, ?], Int](1)( + 1.postpro[ListF[Int, ?]]( i => if (i > 100) NilF() else ConsF(i, i + 1), stopAtFive) shouldBe `1 to 5` } diff --git a/core/shared/src/test/scala/schemes/SyntaxSpec.scala b/core/shared/src/test/scala/schemes/SyntaxSpec.scala new file mode 100644 index 0000000..b0463f3 --- /dev/null +++ b/core/shared/src/test/scala/schemes/SyntaxSpec.scala @@ -0,0 +1,44 @@ +package schemes + +import cats.{ ~>, Monad, Traverse } +import cats.free.Cofree + +/* Check that usages of extension methods compile */ +object SyntaxSpec { + def mock[A]: A = ??? + + def testAnySyntax[F[_]: Traverse, M[_]: Monad, A, B]: Unit = { + val a = mock[A] + val coalgebra = mock[A => F[A]] + val coalgebraM = mock[A => M[F[A]]] + val algebra = mock[F[B] => B] + val algebraM = mock[F[B] => M[B]] + val natTrans = mock[F ~> F] + val elgotCoalgebra = mock[A => Either[B, F[A]]] + val elgotAlgebra = mock[(A, () => F[B]) => B] + val rCoalgebra = mock[A => F[Either[Fix[F], A]]] + a.ana(coalgebra) + a.anaM(coalgebraM) + a.hylo(coalgebra, algebra) + a.hyloM(coalgebraM, algebraM) + a.postpro(coalgebra, natTrans) + a.elgot(elgotCoalgebra, algebra) + a.coelgot(coalgebra, elgotAlgebra) + a.apo(rCoalgebra) + () + } + + def testFixSyntax[F[_]: Traverse, M[_]: Monad, A, B]: Unit = { + val fix = mock[Fix[F]] + val algebra = mock[F[B] => B] + val algebraM = mock[F[B] => M[B]] + val natTrans = mock[F ~> F] + val cvAlgebra = mock[F[Cofree[F, A]] => A] + fix.unfix + fix.cata(algebra) + fix.cataM(algebraM) + fix.prepro(natTrans, algebra) + fix.histo(cvAlgebra) + () + } +} diff --git a/core/shared/src/test/scala/schemes/data/ListFSpec.scala b/core/shared/src/test/scala/schemes/data/ListFSpec.scala new file mode 100644 index 0000000..b5204e8 --- /dev/null +++ b/core/shared/src/test/scala/schemes/data/ListFSpec.scala @@ -0,0 +1,30 @@ +package schemes +package data + +import cats.laws.discipline._ +import cats.tests.CatsSuite +import org.scalacheck.{ Arbitrary, Gen } +import org.scalacheck.Arbitrary.arbitrary + +class ListFSpec extends CatsSuite { + implicit def arbListF[A, B](implicit A: Arbitrary[A], B: Arbitrary[B]): Arbitrary[ListF[A, B]] = + Arbitrary(Gen.oneOf( + Gen.const(NilF[A, B]()), + for { + a <- arbitrary[A] + b <- arbitrary[B] + } yield ConsF(a, b))) + + checkAll("ListF[Int, Int]", TraverseTests[ListF[Int, ?]].traverse[Int, Int, Int, Set[Int], Option, Option]) + + val list = List(1, 2, 3, 4, 5) + val listF = ListF(1 to 5: _*) + + test("ListF.apply") { + listF shouldBe ListF.cons(1, ListF.cons(2, ListF.cons(3, ListF.cons(4, ListF.cons(5, ListF.nil[Int]))))) + } + + test("ListF.toList") { + ListF.toList(listF) shouldBe list + } +} diff --git a/docs/src/main/resources/rootdoc.txt b/docs/src/main/resources/rootdoc.txt new file mode 100644 index 0000000..d0a55b1 --- /dev/null +++ b/docs/src/main/resources/rootdoc.txt @@ -0,0 +1,3 @@ +This is the API documentation for [[https://davidgregory084.github.io/schemes/ schemes]], a Scala library which provides utilities for working with recursion schemes. + +Please refer to the [[https://davidgregory084.github.io/schemes/ documentation]] for further explanation and usage examples.