Skip to content

Commit

Permalink
Add syntax (fixes #3), improve test coverage (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidGregory084 committed Mar 8, 2018
1 parent e88eb0e commit 069ae69
Show file tree
Hide file tree
Showing 7 changed files with 247 additions and 13 deletions.
47 changes: 46 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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",

Expand Down
39 changes: 36 additions & 3 deletions core/shared/src/main/scala/schemes/data/ListF.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
}
41 changes: 41 additions & 0 deletions core/shared/src/main/scala/schemes/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
56 changes: 47 additions & 9 deletions core/shared/src/test/scala/schemes/SchemesSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,46 +23,84 @@ 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
Add(1, i - 1)
}

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
}
Expand All @@ -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
}
Expand All @@ -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`
}
Expand Down
44 changes: 44 additions & 0 deletions core/shared/src/test/scala/schemes/SyntaxSpec.scala
Original file line number Diff line number Diff line change
@@ -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)
()
}
}
30 changes: 30 additions & 0 deletions core/shared/src/test/scala/schemes/data/ListFSpec.scala
Original file line number Diff line number Diff line change
@@ -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
}
}
3 changes: 3 additions & 0 deletions docs/src/main/resources/rootdoc.txt
Original file line number Diff line number Diff line change
@@ -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.

0 comments on commit 069ae69

Please sign in to comment.