diff --git a/CHANGELOG/breaking.md b/CHANGELOG/breaking.md new file mode 100644 index 00000000..64707778 --- /dev/null +++ b/CHANGELOG/breaking.md @@ -0,0 +1,9 @@ +- removed `FunctorT.translate` (use either `.transCata` or `.transAna` instead) +- fixed the type param order on `IdOps.ghylo` + +Non-breaking changes: +- added `ghyloM`, `dyna`, `codyna`, `codynaM` refolds +- added a `ganaM` and `gapo` unfolds +- added `Nat` to `matryoshka.fixedpoint` +- added transform type aliases +- added optics for algebras and folds diff --git a/README.md b/README.md index f19d9b07..220f841d 100644 --- a/README.md +++ b/README.md @@ -84,14 +84,70 @@ someExpr[Mu].cata(eval) // ⇒ 24 The `.embed` calls in `someExpr` wrap the nodes in the fixed point type. `embed` is generic, and we abstract `someExpr` over the fixed point type (only requiring that it has an instance of `Corecursive`), so we can postpone the choice of the fixed point as long as possible. -### Folds - -Those algebras can be applied recursively to your structures using many different folds. `cata` in the example above is the simplest fold. It traverses the structure bottom-up, applying the algebra to each node. That is the general behavior of a fold, but more complex ones allow for various comonads and monads to affect the result. +### Recursion Schemes Here is a cheat-sheet (also available [in PDF](resources/recursion-schemes.pdf)) for some of them. ![folds and unfolds](resources/recursion-schemes.png) +#### Folds + +Those algebras can be applied recursively to your structures using many different folds. `cata` in the example above is the simplest fold. It traverses the structure bottom-up, applying the algebra to each node. That is the general behavior of a fold, but more complex ones allow for various comonads and monads to affect the result. + +#### Unfolds + +These are the dual of folds – using coalgebras to deconstruct values into parts, top-down. They are defined in the `Corecursive` type class. + +#### Refolds + +Refolds compose an unfold with a fold, never actually constructing the intermediate fixed-point structure. Therefore, they are available on any value, and are not part of a type class. + +#### Transformations + +The structure of these type classes is similar to `Recursive` and `Corecursive`, but rather than separating them between bottom-up and top-down traversals, `FunctorT` has both bottom-up and top-down traversals (and refold), while `TraverseT` has all the Kleisli variants (paralleling how `Traverse` extends `Functor`). A fixed-point type that has both `Recursive` and `Corecursive` instances has an implied `TraverseT` instance. + +The benefits of these classes is that it is possible to define the required `map` and `traverse` operations on fixed-point types that lack either a `project` or an `embed` (e.g., `Cofree[?[_], A]` lacks `embed` unless `A` has a `Monoid` instance, but can easily be `map`ped over). + +The tradeoff is that these operations can only transform between one fixed-point functor and another (or, in some cases, need to maintain the same functor). + +The names of these operations are the same as those in `Recursive` and `Corecursive`, but prefixed with `trans`. + +There is an additional (restricted) set of operations that also have a `T` suffix (e.g., `transCataT`). These only generalize in “the Elgot position” and require you to maintain the same functor. However, it can be the most natural way to write certain transformations, like `matryoshka.algebras.substitute`. + +### Generalization + +There are generalized forms of most recursion schemes. From the basic `cata` (and its dual, `ana`), we can generalize in a few ways. We name them using either a prefix or suffix, depending on how they’re generalized. + +#### G… + +Most well known (in fact, even referred to as “generalized recursion schemes”) is generalizing over a `Comonad` (or `Monad`), converting an algebra like `F[A] => A` to `F[W[A]] => A`. Many of the other named folds are instances of this – + +- when `W[A] = (T[F], A)`, it’s `para`, +- when `W[A] = (B, A)`, it’s `zygo`, and +- when `W[A] = Cofree[F, A]`, it’s `histo`. + +These specializations can give rise to other generalizations. `zygoT` uses `EnvT[B, ?[_], A]` and `ghisto` uses `Cofree[?[_], A]`. + +#### …M + +Less unique to recursion schemes, there are Kleisli variants that return the result in any monad. + +#### Elgot… + +This generalization, stolen from the “Elgot algebra”, is similar to standard generalization, except it uses `W[F[A]] => A` rather than `F[W[A]] => A`, with the `Comonad` outside the functor. Not all of the forms seem to be as useful as the `G` variants, but in some cases, like `elgotZygo`, it offers benefits of its own. + +#### GElgot…M + +Any of these generalizations can be combined, so you can have an algebra that is generalized along two or three dimensions. A fold like `cofPara` takes an algebra that’s generalized like `zygo` (`(B, ?)`) in the “Elgot” dimension and like `para` (`(T[F], ?)`) in the “G” dimension, which looks like `(B, F[(T[F], A)]) => A`. It’s honestly useful. I swear. + +### Implementation + +Since we can actually derive almost everything from a fairly small number of operations, why don’t we? Well, there are a few reasons, enumerated here in descending order of how valid I think they are: + +1. Reducing constraints. In the case of `para`, using `gcata(distPara, …)` would introduce a `Corecursive` constraint, and all of the Kleisli variants require `Traverse` for the functor, not just `Functor`. +2. Improving performance. `cata` implemented directly (presumably) performs better than `gcata[Id, …]`. We should have some benchmarks added eventually to actually determine when this is worth doing. +3. Helping inference. Whle we are (planning to) used kinda-curried type parameters to help with this, it’s still the case that `gcata` generally requires all the type parameters to be specified, while, say, `zygo` doesn’t. You can notice these instances because their definition actually is just to call the generalized version, rather than being implemented directly. + ## Contributing Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. diff --git a/build.sbt b/build.sbt index 10e42274..b03f4b80 100644 --- a/build.sbt +++ b/build.sbt @@ -14,13 +14,15 @@ val scalazVersion = "7.2.1" val specs2Version = "3.7" val testDependencies = libraryDependencies ++= Seq( - "com.github.julien-truffaut" %% "monocle-law" % monocleVersion % "test", - "org.typelevel" %% "discipline" % "0.4" % "test", - "org.scalaz" %% "scalaz-scalacheck-binding" % scalazVersion % "test", - "org.specs2" %% "specs2-core" % specs2Version % "test" force(), - "org.specs2" %% "specs2-scalacheck" % specs2Version % "test" force(), - // `scalaz-scalacheck-binding` is built with `scalacheck` 1.12.5 so we are stuck with that version - "org.scalacheck" %% "scalacheck" % "1.12.5" % "test" force() + "org.typelevel" %% "discipline" % "0.4" % "test", + "com.github.julien-truffaut" %% "monocle-law" % monocleVersion % "test", + "org.scalaz" %% "scalaz-scalacheck-binding" % scalazVersion % "test", + "org.typelevel" %% "scalaz-specs2" % "0.4.0" % "test", + "org.specs2" %% "specs2-core" % specs2Version % "test" force(), + "org.specs2" %% "specs2-scalacheck" % specs2Version % "test" force(), + // `scalaz-scalack-binding` is built with `scalacheck` 1.12.5 so we are stuck + // with that version + "org.scalacheck" %% "scalacheck" % "1.12.5" % "test" force() ) lazy val standardSettings = Seq( @@ -64,19 +66,18 @@ lazy val standardSettings = Seq( "-Ywarn-numeric-widen", "-Ywarn-unused-import", "-Ywarn-value-discard"), + scalacOptions in (Compile,doc) ++= Seq("-groups", "-implicits"), scalacOptions in (Test, console) --= Seq( "-Yno-imports", - "-Ywarn-unused-import" - ), + "-Ywarn-unused-import"), wartremoverErrors in (Compile, compile) ++= warts, // Warts.all, console <<= console in Test, // console alias test:console libraryDependencies ++= Seq( - "com.github.julien-truffaut" %%% "monocle-core" % monocleVersion % "compile, test", - "com.github.mpilquist" %%% "simulacrum" % "0.7.0" % "compile, test", - "org.scalaz" %%% "scalaz-core" % scalazVersion % "compile, test" - ), + "com.github.julien-truffaut" %%% "monocle-core" % monocleVersion % "compile, test", + "org.scalaz" %%% "scalaz-core" % scalazVersion % "compile, test", + "com.github.mpilquist" %%% "simulacrum" % "0.7.0" % "compile, test"), licenses += ("Apache 2", url("http://www.apache.org/licenses/LICENSE-2.0")), diff --git a/core/jvm/src/test/scala/matryoshka/example/Example.scala b/core/jvm/src/test/scala/matryoshka/example/Example.scala index 5be47b8b..5514d5a1 100644 --- a/core/jvm/src/test/scala/matryoshka/example/Example.scala +++ b/core/jvm/src/test/scala/matryoshka/example/Example.scala @@ -28,7 +28,6 @@ import org.scalacheck._ import org.specs2.ScalaCheck import org.specs2.mutable._ import scalaz._, Scalaz._ -import scalaz.scalacheck.ScalaCheckBinding._ import scalaz.scalacheck.ScalazProperties._ sealed trait Example[A] diff --git a/core/jvm/src/test/scala/matryoshka/helpers/package.scala b/core/jvm/src/test/scala/matryoshka/helpers/package.scala index da425f89..b1d9c8d8 100644 --- a/core/jvm/src/test/scala/matryoshka/helpers/package.scala +++ b/core/jvm/src/test/scala/matryoshka/helpers/package.scala @@ -16,13 +16,16 @@ package matryoshka -import scala.Predef.implicitly +import scala.{None, Option, Some} +import monocle.law.discipline._ import org.scalacheck._ +import org.specs2.mutable._ +import org.typelevel.discipline.specs2.mutable._ import scalaz._, Scalaz._ -import scalaz.scalacheck.ScalaCheckBinding._ +import scalaz.scalacheck.ScalaCheckBinding.{GenMonad => _, _} -package object helpers { +package object helpers extends Specification with Discipline { implicit def NTArbitrary[F[_], A]( implicit A: Arbitrary[A], F: Arbitrary ~> λ[α => Arbitrary[F[α]]]): Arbitrary[F[A]] = @@ -39,11 +42,10 @@ package object helpers { Arbitrary(Gen.resize(size - 1, corecArbitrary[T, F].arbitrary))).arbitrary.map(_.embed))) implicit def freeArbitrary[F[_]]( - implicit F: Arbitrary ~> λ[α => Arbitrary[F[α]]]): - Arbitrary ~> λ[α => Arbitrary[Free[F, α]]] = - new (Arbitrary ~> λ[α => Arbitrary[Free[F, α]]]) { + implicit F: Arbitrary ~> (Arbitrary ∘ F)#λ): + Arbitrary ~> (Arbitrary ∘ Free[F, ?])#λ = + new (Arbitrary ~> (Arbitrary ∘ Free[F, ?])#λ) { def apply[α](arb: Arbitrary[α]) = - // FIXME: This is only generating leaf nodes Arbitrary(Gen.sized(size => if (size <= 1) arb.map(_.point[Free[F, ?]]).arbitrary @@ -53,16 +55,73 @@ package object helpers { F(freeArbitrary[F](F)(arb)).arbitrary.map(Free.roll)))) } - implicit def freeEqual[F[_]: Functor]( - implicit F: Equal ~> λ[α => Equal[F[α]]]): - Equal ~> λ[α => Equal[Free[F, α]]] = - new (Equal ~> λ[α => Equal[Free[F, α]]]) { - def apply[α](eq: Equal[α]) = - Equal.equal((a, b) => (a.resume, b.resume) match { - case (-\/(f1), -\/(f2)) => - F(freeEqual[F](implicitly, F)(eq)).equal(f1, f2) - case (\/-(a1), \/-(a2)) => eq.equal(a1, a2) + implicit def cofreeArbitrary[F[_]]( + implicit F: Arbitrary ~> (Arbitrary ∘ F)#λ): + Arbitrary ~> (Arbitrary ∘ Cofree[F, ?])#λ = + new (Arbitrary ~> (Arbitrary ∘ Cofree[F, ?])#λ) { + def apply[A](arb: Arbitrary[A]) = + Arbitrary(Gen.sized(size => + if (size <= 0) + Gen.fail[Cofree[F, A]] + else (arb.arbitrary ⊛ F(cofreeArbitrary(F)(arb)).arbitrary)(Cofree(_, _)))) + } + + implicit def optionArbitrary: Arbitrary ~> (Arbitrary ∘ Option)#λ = + new (Arbitrary ~> (Arbitrary ∘ Option)#λ) { + def apply[A](arb: Arbitrary[A]) = + Arbitrary(Gen.frequency( + ( 1, None.point[Gen]), + (75, arb.arbitrary.map(_.some)))) + } + + implicit def optionEqualNT: Equal ~> (Equal ∘ Option)#λ = + new (Equal ~> (Equal ∘ Option)#λ) { + def apply[A](eq: Equal[A]) = + Equal.equal { + case (None, None) => true + case (Some(a), Some(b)) => eq.equal(a, b) case (_, _) => false - }) + } + } + + implicit def optionShowNT: Show ~> (Show ∘ Option)#λ = + new (Show ~> (Show ∘ Option)#λ) { + def apply[A](s: Show[A]) = + Show.show(_.fold(Cord("None"))(Cord("Some(") ++ s.show(_) ++ Cord(")"))) + } + + implicit def eitherArbitrary[A: Arbitrary]: + Arbitrary ~> (Arbitrary ∘ (A \/ ?))#λ = + new (Arbitrary ~> (Arbitrary ∘ (A \/ ?))#λ) { + def apply[B](arb: Arbitrary[B]) = + Arbitrary(Gen.oneOf( + Arbitrary.arbitrary[A].map(-\/(_)), + arb.arbitrary.map(\/-(_)))) + } + + implicit def nonEmptyListArbitrary: + Arbitrary ~> (Arbitrary ∘ NonEmptyList)#λ = + new (Arbitrary ~> (Arbitrary ∘ NonEmptyList)#λ) { + def apply[A](arb: Arbitrary[A]) = + Arbitrary((arb.arbitrary ⊛ Gen.listOf[A](arb.arbitrary))((h, t) => + NonEmptyList.nel(h, t.toIList))) + } + + implicit def nonEmptyListEqual: Equal ~> (Equal ∘ NonEmptyList)#λ = + new (Equal ~> (Equal ∘ NonEmptyList)#λ) { + def apply[A](eq: Equal[A]) = NonEmptyList.nonEmptyListEqual(eq) } + + + def checkAlgebraIsoLaws[F[_], A](iso: AlgebraIso[F, A])( + implicit FA: Arbitrary ~> (Arbitrary ∘ F)#λ, AA: Arbitrary[A], FE: Equal ~> (Equal ∘ F)#λ, AE: Equal[A]) = + checkAll("algebra Iso", IsoTests(iso)) + + def checkAlgebraPrismLaws[F[_], A](prism: AlgebraPrism[F, A])( + implicit FA: Arbitrary ~> (Arbitrary ∘ F)#λ, AA: Arbitrary[A], FE: Equal ~> (Equal ∘ F)#λ, AE: Equal[A]) = + checkAll("algebra Prism", PrismTests(prism)) + + def checkCoalgebraPrismLaws[F[_], A](prism: CoalgebraPrism[F, A])( + implicit FA: Arbitrary ~> (Arbitrary ∘ F)#λ, AA: Arbitrary[A], FE: Equal ~> (Equal ∘ F)#λ, AE: Equal[A]) = + checkAll("coalgebra Prism", PrismTests(prism)) } diff --git a/core/jvm/src/test/scala/matryoshka/instances/fixedpoint/list.scala b/core/jvm/src/test/scala/matryoshka/instances/fixedpoint/list.scala index ebb88c7f..f00ea960 100644 --- a/core/jvm/src/test/scala/matryoshka/instances/fixedpoint/list.scala +++ b/core/jvm/src/test/scala/matryoshka/instances/fixedpoint/list.scala @@ -23,9 +23,10 @@ import scala.Int import org.specs2.ScalaCheck import org.specs2.mutable._ +import org.specs2.scalaz.{ScalazMatchers} import scalaz._, Scalaz._ -class ListSpec extends Specification with ScalaCheck with specs2.scalaz.Matchers { +class ListSpec extends Specification with ScalaCheck with ScalazMatchers { "apply" should { "be equivalent to scala.List.apply" in { List(1, 2, 3, 4).cata(ListF.listIso.get) must diff --git a/core/jvm/src/test/scala/matryoshka/instances/fixedpoint/nat.scala b/core/jvm/src/test/scala/matryoshka/instances/fixedpoint/nat.scala new file mode 100644 index 00000000..c40495c2 --- /dev/null +++ b/core/jvm/src/test/scala/matryoshka/instances/fixedpoint/nat.scala @@ -0,0 +1,51 @@ +/* + * Copyright 2014–2016 SlamData Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package matryoshka.instances.fixedpoint + +import matryoshka._, Recursive.ops._ +import matryoshka.helpers._ + +import scala.Option + +import org.specs2.mutable._ +import org.specs2.scalaz.ScalazMatchers +import scalaz._, Scalaz._ + +class NatSpec extends Specification with ScalazMatchers { + checkCoalgebraPrismLaws(Nat.intPrism) + + "+" should { + "sum values" ! prop { (a: Nat, b: Nat) => + val (ai, bi) = (a.cata(height), b.cata(height)) + (a + b).some must equal((ai + bi).anaM[Mu, Option, Option](Nat.fromInt)) + } + } + + "min" should { + "pick smaller value" ! prop { (a: Nat, b: Nat) => + val (ai, bi) = (a.cata(height), b.cata(height)) + (a min b).some must equal((ai min bi).anaM[Mu, Option, Option](Nat.fromInt)) + } + } + + "max" should { + "pick larger value" ! prop { (a: Nat, b: Nat) => + val (ai, bi) = (a.cata(height), b.cata(height)) + (a max b).some must equal((ai max bi).anaM[Mu, Option, Option](Nat.fromInt)) + } + } +} diff --git a/core/jvm/src/test/scala/matryoshka/instances/fixedpoint/partial.scala b/core/jvm/src/test/scala/matryoshka/instances/fixedpoint/partial.scala index 3c5261a3..82606111 100644 --- a/core/jvm/src/test/scala/matryoshka/instances/fixedpoint/partial.scala +++ b/core/jvm/src/test/scala/matryoshka/instances/fixedpoint/partial.scala @@ -16,37 +16,90 @@ package matryoshka.instances.fixedpoint -import matryoshka._ +import matryoshka._, Recursive.ops._, FunctorT.ops._ import matryoshka.helpers._ import matryoshka.specs2.scalacheck.CheckAll -import scala.Predef.implicitly -import scala.Int +import scala.Predef.{implicitly} +import scala.{Int} import org.scalacheck._ import org.specs2.ScalaCheck import org.specs2.mutable._ +import org.specs2.scalaz.{ScalazMatchers} import scalaz._, Scalaz._ -import scalaz.scalacheck.ScalazProperties._ - -class PartialSpec extends Specification with ScalaCheck with CheckAll { - implicit def eitherArbitrary[A: Arbitrary]: - Arbitrary ~> λ[β => Arbitrary[A \/ β]] = - new (Arbitrary ~> λ[β => Arbitrary[A \/ β]]) { - def apply[β](arb: Arbitrary [β]) = - Arbitrary(Gen.oneOf( - Arbitrary.arbitrary[A].map(-\/(_)), - arb.arbitrary.map(\/-(_)))) - } +import scalaz.scalacheck.{ScalazProperties => Props} +class PartialSpec extends Specification with ScalazMatchers with ScalaCheck with CheckAll { /** For testing cases that should work with truly diverging functions. */ def sometimesNeverGen[A: Arbitrary]: Gen[Partial[A]] = Gen.oneOf(Arbitrary.arbitrary[Partial[A]], Gen.const(Partial.never[A])) "Partial" should { "satisfy relevant laws" in { - checkAll(equal.laws[Partial[Int]](Partial.equal, implicitly)) - checkAll(monad.laws[Partial](Partial.monad, implicitly, implicitly, implicitly, Partial.equal)) + checkAll(Props.equal.laws[Partial[Int]](Partial.equal, implicitly)) + checkAll(Props.monad.laws[Partial](implicitly, implicitly, implicitly, implicitly, Partial.equal)) + } + } + + // https://en.wikipedia.org/wiki/McCarthy_91_function + def mc91(n: Int): Partial[Int] = + if (n > 100) Partial.now(n - 10) + else mc91(n + 11) >>= mc91 + + "never" should { + "always have more steps" in { + Partial.never[Int].runFor(1000) must beRightDisjunction + } + } + + "runFor" should { + "return now immediately" in { + Partial.now(13).runFor(0) must beLeftDisjunction(13) + } + + "return a value when it runs past the end" in { + Partial.later(Partial.now(7)).runFor(300000) must beLeftDisjunction(7) + } + + "return after multiple runs" ! prop { (a: Conat, b: Conat) => + val (ai, bi) = (a.cata(height), b.cata(height)) + bi > 0 ==> { + val first = (a + b).transAna(Partial.delay(27)).runFor(ai) + first must beRightDisjunction + first.flatMap(_.runFor(bi)) must beLeftDisjunction(27) + } + } + + "still pending one short" ! prop { (a: Conat) => + val ai = a.cata(height) + ai > 0 ==> { + val first = a.transAna(Partial.delay(27)).runFor(ai - 1) + first must beRightDisjunction + first.flatMap(_.runFor(1)) must beLeftDisjunction(27) + } + } + + "return exactly at the end" ! prop { (i: Conat) => + i.transAna(Partial.delay(4)).runFor(i.cata(height)) must + beLeftDisjunction(4) + } + } + + "unsafePerformSync" should { + "return now immediately" in { + Partial.now(12).unsafePerformSync must equal(12) + } + + "return a value when it gets to the end" in { + Partial.later(Partial.later(Partial.now(3))).unsafePerformSync must + equal(3) + } + + // TODO: Should work with any Int, but stack overflows on big negatives. + "always terminate with mc91" ! prop { (n: Int) => + n > -3000 ==> + (mc91(n).unsafePerformSync must equal(if (n <= 100) 91 else n - 10)) } } } diff --git a/core/jvm/src/test/scala/matryoshka/instances/fixedpoint/stream.scala b/core/jvm/src/test/scala/matryoshka/instances/fixedpoint/stream.scala index c620e93a..8f7c3eac 100644 --- a/core/jvm/src/test/scala/matryoshka/instances/fixedpoint/stream.scala +++ b/core/jvm/src/test/scala/matryoshka/instances/fixedpoint/stream.scala @@ -22,9 +22,10 @@ import scala.Int import org.specs2.ScalaCheck import org.specs2.mutable._ +import org.specs2.scalaz.{ScalazMatchers} import scalaz._, Scalaz._ -class StreamSpec extends Specification with ScalaCheck with specs2.scalaz.Matchers { +class StreamSpec extends Specification with ScalaCheck with ScalazMatchers { /** Infinite sequence of Fibonacci numbers (at least until they overflow * int32) */ @@ -60,12 +61,12 @@ class StreamSpec extends Specification with ScalaCheck with specs2.scalaz.Matche // FIXME: These two blow up the stack with much larger inputs "have the given value at an arbitrary point" ! prop { (i: Int, d: Int) => - i.ana[Nu, (Int, ?)](constantly).drop(20000).head must equal(i) + i.ana[Nu, (Int, ?)](constantly).drop(30000).head must equal(i) } "have subsequence of the given value" ! prop { (i: Int, t: Int) => - i.ana[Nu, (Int, ?)](constantly).take(900) must - equal(List.fill(900)(i)) + i.ana[Nu, (Int, ?)](constantly).take(450) must + equal(List.fill(450)(i)) } } } diff --git a/core/jvm/src/test/scala/matryoshka/patterns/coenv.scala b/core/jvm/src/test/scala/matryoshka/patterns/coenv.scala index b94f66ba..c12a2e8d 100644 --- a/core/jvm/src/test/scala/matryoshka/patterns/coenv.scala +++ b/core/jvm/src/test/scala/matryoshka/patterns/coenv.scala @@ -16,23 +16,21 @@ package matryoshka.patterns +import matryoshka._ import matryoshka.exp._ import matryoshka.helpers._ +import matryoshka.specs2.scalacheck._ -import scala.Int +import java.lang.{String} +import scala.{Int} -import monocle.law.discipline.IsoTests import org.scalacheck._ import org.specs2.mutable._ -import org.typelevel.discipline.specs2.mutable._ import scalaz._, Scalaz._ -import scalaz.scalacheck.ScalaCheckBinding._ - -class CoEnvSpec extends Specification with Discipline { - implicit def NTArbitrary[F[_], A](implicit A: Arbitrary[A], F: Arbitrary ~> λ[α => Arbitrary[F[α]]]): - Arbitrary[F[A]] = - F(A) +import scalaz.scalacheck.ScalaCheckBinding.{GenMonad => _, _} +import scalaz.scalacheck.ScalazProperties._ +class CoEnvSpec extends Specification with CheckAll { implicit def coEnvArbitrary[E: Arbitrary, F[_]]( implicit F: Arbitrary ~> λ[α => Arbitrary[F[α]]]): Arbitrary ~> λ[α => Arbitrary[CoEnv[E, F, α]]] = @@ -45,5 +43,13 @@ class CoEnvSpec extends Specification with Discipline { F(arb).arbitrary.map(_.right))) ∘ (CoEnv(_)) } - checkAll("CoEnv ⇔ Free", IsoTests(CoEnv.freeIso[Int, Exp])) + "CoEnv should satisfy relevant laws" in { + checkAll(equal.laws[CoEnv[String, Exp, Int]]) + checkAll(traverse.laws[CoEnv[String, Exp, ?]]) + // FIXME: These instances don’t fulfill the laws + // checkAll(monad.laws[CoEnv[String, Option, ?]]) + // checkAll(monad.laws[CoEnv[String, NonEmptyList, ?]]) + } + + checkAlgebraIsoLaws(CoEnv.freeIso[Int, Exp]) } diff --git a/core/jvm/src/test/scala/matryoshka/patterns/diff.scala b/core/jvm/src/test/scala/matryoshka/patterns/diff.scala index 1795371b..3bc1cf4d 100644 --- a/core/jvm/src/test/scala/matryoshka/patterns/diff.scala +++ b/core/jvm/src/test/scala/matryoshka/patterns/diff.scala @@ -28,8 +28,8 @@ import scala.{Int, List, Unit} import org.scalacheck._ import org.specs2.ScalaCheck import org.specs2.mutable._ +import org.specs2.scalaz.{ScalazMatchers} import scalaz._, Scalaz._ -import scalaz.scalacheck.ScalaCheckBinding._ import scalaz.scalacheck.ScalazProperties._ trait DiffArb { @@ -63,7 +63,7 @@ class DiffLaws extends Specification with DiffArb with ScalaCheck with CheckAll } } -class DiffSpec extends Specification with DiffArb with specs2.scalaz.Matchers { +class DiffSpec extends Specification with DiffArb with ScalazMatchers { "diff" should { "find non-recursive differences" in { NonRec[Mu[Example]]("a", 1).embed.paraMerga(NonRec[Mu[Example]]("b", 1).embed)(diff) must diff --git a/core/jvm/src/test/scala/matryoshka/patterns/envt.scala b/core/jvm/src/test/scala/matryoshka/patterns/envt.scala new file mode 100644 index 00000000..50d7f676 --- /dev/null +++ b/core/jvm/src/test/scala/matryoshka/patterns/envt.scala @@ -0,0 +1,51 @@ +/* + * Copyright 2014–2016 SlamData Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package matryoshka.patterns + +import matryoshka._ +import matryoshka.exp._ +import matryoshka.helpers._ +import matryoshka.specs2.scalacheck._ + +import java.lang.{String} +import scala.{Int} + +import org.scalacheck._ +import org.specs2.mutable._ +import scalaz._, Scalaz._ +import scalaz.scalacheck.ScalazProperties._ + +class EnvTSpec extends Specification with CheckAll { + implicit def envTArbitrary[E: Arbitrary, F[_]]( + implicit F: Arbitrary ~> (Arbitrary ∘ F)#λ): + Arbitrary ~> (Arbitrary ∘ EnvT[E, F, ?])#λ = + new (Arbitrary ~> (Arbitrary ∘ EnvT[E, F, ?])#λ) { + def apply[A](arb: Arbitrary[A]) = + Arbitrary( + (Arbitrary.arbitrary[E] ⊛ F(arb).arbitrary)((e, f) => EnvT((e, f)))) + } + + + "EnvT" should { + "satisfy relevant laws" in { + checkAll(equal.laws[EnvT[String, Exp, Int]]) + checkAll(comonad.laws[EnvT[String, NonEmptyList, ?]]) + } + } + + checkAlgebraIsoLaws(EnvT.cofreeIso[Int, Exp]) +} diff --git a/core/jvm/src/test/scala/matryoshka/patterns/listF.scala b/core/jvm/src/test/scala/matryoshka/patterns/listF.scala index a2162388..ccbdae2c 100644 --- a/core/jvm/src/test/scala/matryoshka/patterns/listF.scala +++ b/core/jvm/src/test/scala/matryoshka/patterns/listF.scala @@ -16,11 +16,13 @@ package matryoshka.patterns +import matryoshka._ import matryoshka.helpers._ import matryoshka.specs2.scalacheck.CheckAll import java.lang.String -import scala.Int +import scala.{Int, Seq} +import scala.Predef.{implicitly => imp} import org.scalacheck._ import org.specs2.ScalaCheck @@ -30,16 +32,19 @@ import scalaz.scalacheck.ScalazProperties._ class ListFSpec extends Specification with ScalaCheck with CheckAll { implicit def listFArbitrary[A: Arbitrary]: - Arbitrary ~> λ[β => Arbitrary[ListF[A, β]]] = - new (Arbitrary ~> λ[β => Arbitrary[ListF[A, β]]]) { - def apply[β](arb: Arbitrary[β]) = + Arbitrary ~> (Arbitrary ∘ ListF[A, ?])#λ = + new (Arbitrary ~> (Arbitrary ∘ ListF[A, ?])#λ) { + def apply[B](arb: Arbitrary[B]) = Arbitrary(Gen.oneOf( - NilF[A, β]().point[Gen], - (Arbitrary.arbitrary[A] ⊛ arb.arbitrary)(ConsF[A, β]))) + NilF[A, B]().point[Gen], + (Arbitrary.arbitrary[A] ⊛ arb.arbitrary)(ConsF[A, B]))) } "ListF should satisfy relevant laws" >> { checkAll(equal.laws[ListF[String, Int]]) checkAll(bitraverse.laws[ListF]) } + + checkAlgebraIsoLaws(ListF.listIso[Int])(imp, Arbitrary(Gen.listOf(Arbitrary.arbInt.arbitrary)), imp, imp) + checkAlgebraIsoLaws(ListF.seqIso[Int])(imp, Arbitrary(Gen.listOf(Arbitrary.arbInt.arbitrary).map(_.toSeq)), imp, Equal.equalRef[Seq[Int]]) } diff --git a/core/jvm/src/test/scala/matryoshka/patterns/potentialFailure.scala b/core/jvm/src/test/scala/matryoshka/patterns/potentialFailure.scala index 2474b6e5..f49a1a45 100644 --- a/core/jvm/src/test/scala/matryoshka/patterns/potentialFailure.scala +++ b/core/jvm/src/test/scala/matryoshka/patterns/potentialFailure.scala @@ -45,5 +45,6 @@ class PotentialFailureSpec extends Specification with ScalaCheck with CheckAll { "PotentialFailure should satisfy relevant laws" >> { checkAll(equal.laws[PotentialFailure[Fix, Exp, String, Int]]) checkAll(bitraverse.laws[PotentialFailure[Fix, Exp, ?, ?]]) + checkAll(traverse.laws[PotentialFailure[Fix, Exp, String, ?]]) } } diff --git a/core/jvm/src/test/scala/matryoshka/spec.scala b/core/jvm/src/test/scala/matryoshka/spec.scala index 47e3cb67..5a75b423 100644 --- a/core/jvm/src/test/scala/matryoshka/spec.scala +++ b/core/jvm/src/test/scala/matryoshka/spec.scala @@ -27,13 +27,16 @@ import java.lang.String import scala.{Boolean, Function, Int, None, Option, Predef, Symbol, Unit} import scala.collection.immutable.{List, Map, Nil, ::} +import monocle.law.discipline._ import org.scalacheck._ import org.specs2.ScalaCheck import org.specs2.mutable._ +import org.specs2.scalaz.{ScalazMatchers} +import org.typelevel.discipline.specs2.mutable._ import scalaz.{Apply => _, _}, Scalaz._ import scalaz.scalacheck.ScalazProperties._ -class ExpSpec extends Specification with ScalaCheck with CheckAll { +class ExpSpec extends Specification with CheckAll { // NB: These are just a sanity check that the data structure created for the // tests is lawful. "Exp should satisfy relevant laws" >> { @@ -42,7 +45,7 @@ class ExpSpec extends Specification with ScalaCheck with CheckAll { } } -class Exp2Spec extends Specification with ScalaCheck with CheckAll { +class Exp2Spec extends Specification with CheckAll { // NB: These are just a sanity check that the data structure created for the // tests is lawful. "Exp2 should satisfy relevant laws" >> { @@ -51,7 +54,7 @@ class Exp2Spec extends Specification with ScalaCheck with CheckAll { } } -class MatryoshkaSpecs extends Specification with ScalaCheck with specs2.scalaz.Matchers { +class MatryoshkaSpecs extends Specification with ScalaCheck with ScalazMatchers with Discipline { val example1ƒ: Exp[Option[Int]] => Option[Int] = { case Num(v) => v.some case Mul(left, right) => (left ⊛ right)(_ * _) @@ -129,6 +132,12 @@ class MatryoshkaSpecs extends Specification with ScalaCheck with specs2.scalaz.M case _ => t.map(_.head).embed } + checkAlgebraIsoLaws(recCorecIso[Mu, Exp]) + checkAlgebraIsoLaws(lambekIso[Mu, Exp]) + + // TODO: add checks for the prism versions + checkAll("fold Iso", IsoTests(foldIso[Mu, Exp, Mu[Exp]](lambekIso))) + "Recursive" >> { "isLeaf" >> { "be true for simple literal" in { @@ -716,12 +725,12 @@ class MatryoshkaSpecs extends Specification with ScalaCheck with specs2.scalaz.M "ghylo" >> { "behave like hylo with distCata/distAna" ! prop { (i: Int) => - i.ghylo[Exp, Id, Id, Int](distCata, distAna, eval, extractFactors) must + i.ghylo[Id, Id, Exp, Int](distCata, distAna, eval, extractFactors) must equal(i.hylo(eval, extractFactors)) } "behave like chrono with distHisto/distFutu" ! prop { i: Int => - i.ghylo[Exp, Cofree[Exp, ?], Free[Exp, ?], Fix[Exp]]( + i.ghylo[Cofree[Exp, ?], Free[Exp, ?], Exp, Fix[Exp]]( distHisto, distFutu, partialEval[Fix], extract2and3) must equal(i.chrono(partialEval[Fix], extract2and3)) } @@ -909,8 +918,8 @@ class MatryoshkaSpecs extends Specification with ScalaCheck with specs2.scalaz.M "find and replace two children" in { (holes(mul(num(0), num(1)).unFix) match { case Mul((Fix(Num(0)), f1), (Fix(Num(1)), f2)) => - f1(num(2)) must_=== Mul(num(2), num(1)) - f2(num(2)) must_=== Mul(num(0), num(2)) + f1(num(2)) must equal(Mul(num(2), num(1))) + f2(num(2)) must equal(Mul(num(0), num(2))) case r => failure }): org.specs2.execute.Result } @@ -925,9 +934,9 @@ class MatryoshkaSpecs extends Specification with ScalaCheck with specs2.scalaz.M (holesList(mul(num(0), num(1)).unFix) match { case (t1, f1) :: (t2, f2) :: Nil => t1 must equal(num(0)) - f1(num(2)) must_=== Mul(num(2), num(1)) + f1(num(2)) must equal(Mul(num(2), num(1))) t2 must equal(num(1)) - f2(num(2)) must_=== Mul(num(0), num(2)) + f2(num(2)) must equal(Mul(num(0), num(2))) case _ => failure }): org.specs2.execute.Result } diff --git a/core/jvm/src/test/scala/matryoshka/specs2/scalacheck/CheckAll.scala b/core/jvm/src/test/scala/matryoshka/specs2/scalacheck/CheckAll.scala index 1f90a6dd..80bce377 100644 --- a/core/jvm/src/test/scala/matryoshka/specs2/scalacheck/CheckAll.scala +++ b/core/jvm/src/test/scala/matryoshka/specs2/scalacheck/CheckAll.scala @@ -25,8 +25,8 @@ import org.specs2.mutable.SpecificationLike import org.specs2.scalacheck.{Parameters, ScalaCheckPropertyCheck} import org.specs2.specification.core.Fragments -// TODO[specs2]: specs2 3.7.2 has something similar to this, so we should consider adopting that -// when upgrading to it. +// TODO[specs2]: specs2 3.7.2 has something similar to this, so we should +// consider adopting that when upgrading to it. trait CheckAll extends SpecificationLike with ScalaCheckPropertyCheck { def checkAll(props: Properties)(implicit p: Parameters, f: FreqMap[Set[Any]] => Pretty) = { s"${props.name} must satisfy" >> { diff --git a/core/jvm/src/test/scala/matryoshka/specs2/scalaz/Matchers.scala b/core/jvm/src/test/scala/matryoshka/specs2/scalaz/Matchers.scala deleted file mode 100644 index c945e307..00000000 --- a/core/jvm/src/test/scala/matryoshka/specs2/scalaz/Matchers.scala +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2014–2016 SlamData Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package matryoshka.specs2.scalaz - -import scala.Predef._ - -import scalaz._ - -import org.specs2.matcher._ - -trait Matchers { - // Copied from `scalaz-specs2` because current release does not support scalaz 7.2.x - /** Equality matcher with a [[scalaz.Equal]] typeclass */ - def equal[T : Equal : Show](expected: T): Matcher[T] = new Matcher[T] { - def apply[S <: T](actual: Expectable[S]): MatchResult[S] = { - val actualT = actual.value.asInstanceOf[T] - def test = Equal[T].equal(expected, actualT) - def koMessage = "%s !== %s".format(Show[T].shows(actualT), Show[T].shows(expected)) - def okMessage = "%s === %s".format(Show[T].shows(actualT), Show[T].shows(expected)) - Matcher.result(test, okMessage, koMessage, actual) - } - } -} diff --git a/core/shared/src/main/scala/matryoshka/Corecursive.scala b/core/shared/src/main/scala/matryoshka/Corecursive.scala index 78974819..836e5392 100644 --- a/core/shared/src/main/scala/matryoshka/Corecursive.scala +++ b/core/shared/src/main/scala/matryoshka/Corecursive.scala @@ -43,6 +43,17 @@ import simulacrum.typeclass loop(f(a).point[M]) } + def ganaM[M[_]: Traverse, N[_]: Monad, F[_]: Traverse, A]( + a: A)( + k: DistributiveLaw[M, F], f: A => N[F[M[A]]])( + implicit M: Monad[M]): + N[T[F]] = { + def loop(x: M[F[M[A]]]): N[T[F]] = + k(x).traverse(x => M.lift(f)(x.join).sequence >>= loop) ∘ (embed(_)) + + f(a) ∘ (_.point[M]) >>= loop + } + def elgotAna[M[_], F[_]: Functor, A]( a: A)( k: DistributiveLaw[M, F], ψ: A => M[F[A]])( @@ -53,6 +64,8 @@ import simulacrum.typeclass loop(ψ(a)) } + /** An unfold that can short-circuit certain sections. + */ def apo[F[_]: Functor, A](a: A)(f: A => F[T[F] \/ A]): T[F] = // NB: This is not implemented with [[matryoshka.distApo]] because that // would add a [[matryoshka.Recursive]] constraint. @@ -63,6 +76,11 @@ import simulacrum.typeclass // would add a [[matryoshka.Recursive]] constraint. f(a).fold(Predef.identity, fa => embed(fa ∘ (elgotApo(_)(f)))) + /** An unfold that can handle sections with a secondary unfold. + */ + def gapo[F[_]: Functor, A, B](a: A)(ψ0: B => F[B], ψ: A => F[B \/ A]): T[F] = + embed(ψ(a) ∘ (_.fold(ana(_)(ψ0), gapo(_)(ψ0, ψ)))) + def apoM[F[_]: Traverse, M[_]: Monad, A](a: A)(f: A => M[F[T[F] \/ A]]): M[T[F]] = f(a).flatMap(_.traverse(_.fold(_.point[M], apoM(_)(f)))) ∘ (embed(_)) diff --git a/core/shared/src/main/scala/matryoshka/FunctorT.scala b/core/shared/src/main/scala/matryoshka/FunctorT.scala index 5b9f405e..fd109484 100644 --- a/core/shared/src/main/scala/matryoshka/FunctorT.scala +++ b/core/shared/src/main/scala/matryoshka/FunctorT.scala @@ -38,14 +38,14 @@ import simulacrum.{typeclass} * This is noticable when `T` is `Cofree`. In this function, the result may * have any `head` the algebra desires, whereas in `transCata`, it can only * have the `head` of the argument to `f`. - * + * * Docs for operations, since they seem to break scaladoc if put in the right * place: - * - * * `map` – very roughly like Uniplate’s `descend` - * * `transCataT` – akin to Uniplate’s `transform` - * * `transAnaT` – akin to Uniplate’s `topDownTransform` - * * `transCata` – akin to Fixplate’s `restructure` + * + * - `map` – very roughly like Uniplate’s `descend` + * - `transCataT` – akin to Uniplate’s `transform` + * - `transAnaT` – akin to Uniplate’s `topDownTransform` + * - `transCata` – akin to Fixplate’s `restructure` */ @typeclass trait FunctorT[T[_[_]]] { @op("∘") def map[F[_]: Functor, G[_]: Functor](t: T[F])(f: F[T[F]] => G[T[G]]): @@ -77,9 +77,6 @@ import simulacrum.{typeclass} T[G] = map(t)(f(_).map(_.fold(Predef.identity, transApo(_)(f)))) - def translate[F[_]: Functor, G[_]: Functor](t: T[F])(f: F ~> G): T[G] = - map(t)(f(_).map(translate(_)(f))) - def topDownCata[F[_]: Functor, A](t: T[F], a: A)(f: (A, T[F]) => (A, T[F])): T[F] = { val (a0, tf) = f(a, t) diff --git a/core/shared/src/main/scala/matryoshka/IdOps.scala b/core/shared/src/main/scala/matryoshka/IdOps.scala index 6c396297..28608c83 100644 --- a/core/shared/src/main/scala/matryoshka/IdOps.scala +++ b/core/shared/src/main/scala/matryoshka/IdOps.scala @@ -24,7 +24,7 @@ sealed class IdOps[A](self: A) { def hyloM[M[_]: Monad, F[_]: Traverse, B](f: F[B] => M[B], g: A => M[F[A]]): M[B] = matryoshka.hyloM(self)(f, g) - def ghylo[F[_]: Functor, W[_]: Comonad, M[_]: Monad, B]( + def ghylo[W[_]: Comonad, M[_]: Monad, F[_]: Functor, B]( w: DistributiveLaw[F, W], m: DistributiveLaw[M, F], f: F[W[B]] => B, @@ -32,6 +32,23 @@ sealed class IdOps[A](self: A) { B = matryoshka.ghylo(self)(w, m, f, g) + def ghyloM[W[_]: Comonad: Traverse, M[_]: Monad: Traverse, N[_]: Monad, F[_]: Traverse, B]( + w: DistributiveLaw[F, W], + m: DistributiveLaw[M, F], + f: F[W[B]] => N[B], + g: A => N[F[M[A]]]): + N[B] = + matryoshka.ghyloM(self)(w, m, f, g) + + def dyna[F[_]: Functor, B](φ: F[Cofree[F, B]] => B, ψ: A => F[A]): B = + matryoshka.dyna(self)(φ, ψ) + + def codyna[F[_]: Functor, B](φ: F[B] => B, ψ: A => F[Free[F, A]]): B = + matryoshka.codyna(self)(φ, ψ) + + def codynaM[M[_]: Monad, F[_]: Traverse, B](φ: F[B] => M[B], ψ: A => M[F[Free[F, A]]]): M[B] = + matryoshka.codynaM(self)(φ, ψ) + def chrono[F[_]: Functor, B]( g: F[Cofree[F, B]] => B, f: A => F[Free[F, A]]): B = @@ -73,6 +90,11 @@ sealed class IdOps[A](self: A) { implicit T: Corecursive[T]): T[F] = T.elgotAna(self)(k, f) + def ganaM[T[_[_]], M[_]: Monad: Traverse, N[_]: Monad, F[_]: Traverse]( + k: DistributiveLaw[M, F], f: A => N[F[M[A]]])( + implicit T: Corecursive[T]): + N[T[F]] = + T.ganaM(self)(k, f) def apo[T[_[_]], F[_]: Functor](f: A => F[T[F] \/ A])(implicit T: Corecursive[T]): T[F] = T.apo(self)(f) def elgotApo[T[_[_]], F[_]: Functor](f: A => T[F] \/ F[A])(implicit T: Corecursive[T]): T[F] = diff --git a/core/shared/src/main/scala/matryoshka/Recursive.scala b/core/shared/src/main/scala/matryoshka/Recursive.scala index 45e6ba5e..3598b07b 100644 --- a/core/shared/src/main/scala/matryoshka/Recursive.scala +++ b/core/shared/src/main/scala/matryoshka/Recursive.scala @@ -46,6 +46,16 @@ import simulacrum.typeclass g(loop(t).copoint) } + def gcataM[W[_]: Comonad: Traverse, M[_]: Monad, F[_]: Traverse, A]( + t: T[F])( + k: DistributiveLaw[F, W], g: F[W[A]] => M[A]): + M[A] = { + def loop(t: T[F]): M[W[F[W[A]]]] = + project(t).traverse(loop(_) >>= (_.traverse(g) ∘ (_.cojoin))) ∘ (k(_)) + + loop(t) ∘ (_.copoint) >>= g + } + /** A catamorphism generalized with a comonad outside the functor. */ def elgotCata[W[_]: Comonad, F[_]: Functor, A]( t: T[F])( @@ -90,6 +100,12 @@ import simulacrum.typeclass A = gcata[EnvT[B, W, ?], F, A](t)(distZygoT(f, w), g) + def gElgotZygo[F[_]: Functor, W[_]: Comonad, A, B]( + t: T[F])( + f: F[B] => B, w: DistributiveLaw[F, W], g: ElgotAlgebra[EnvT[B, W, ?], F, A]): + A = + elgotCata[EnvT[B, W, ?], F, A](t)(distZygoT(f, w), g) + /** Mutually-recursive fold. */ def mutu[F[_]: Functor, A, B](t: T[F])(f: F[(A, B)] => B, g: F[(B, A)] => A): A = diff --git a/core/shared/src/main/scala/matryoshka/cofree.scala b/core/shared/src/main/scala/matryoshka/cofree.scala index 1e448869..50ed5454 100644 --- a/core/shared/src/main/scala/matryoshka/cofree.scala +++ b/core/shared/src/main/scala/matryoshka/cofree.scala @@ -37,7 +37,14 @@ trait CofreeInstances { f(t.tail).map(Cofree(t.head, _)) } - implicit def cofreeShow[F[_], A: Show](implicit F: (Show ~> λ[α => Show[F[α]]])): + implicit def cofreeEqual[F[_]](implicit F: Equal ~> (Equal ∘ F)#λ): + Equal ~> (Equal ∘ Cofree[F, ?])#λ = + new (Equal ~> (Equal ∘ Cofree[F, ?])#λ) { + def apply[A](eq: Equal[A]) = Equal.equal((a, b) => + eq.equal(a.head, b.head) && F(cofreeEqual(F)(eq)).equal(a.tail, b.tail)) + } + + implicit def cofreeShow[F[_], A: Show](implicit F: Show ~> λ[α => Show[F[α]]]): Show[Cofree[F, A]] = Show.shows(cof => "(" + cof.head.show + ", " + cof.tail.shows + ")") } diff --git a/core/shared/src/main/scala/matryoshka/free.scala b/core/shared/src/main/scala/matryoshka/free.scala index 127a7313..c328630b 100644 --- a/core/shared/src/main/scala/matryoshka/free.scala +++ b/core/shared/src/main/scala/matryoshka/free.scala @@ -31,6 +31,19 @@ trait FreeInstances { _.point[Free[G, ?]].point[M], f(_).map(Free.liftF(_).join)) } + + implicit def freeEqual[F[_]: Functor]( + implicit F: Equal ~> λ[α => Equal[F[α]]]): + Equal ~> λ[α => Equal[Free[F, α]]] = + new (Equal ~> λ[α => Equal[Free[F, α]]]) { + def apply[α](eq: Equal[α]) = + Equal.equal((a, b) => (a.resume, b.resume) match { + case (-\/(f1), -\/(f2)) => + F(freeEqual[F].apply(eq)).equal(f1, f2) + case (\/-(a1), \/-(a2)) => eq.equal(a1, a2) + case (_, _) => false + }) + } } object free extends FreeInstances diff --git a/core/shared/src/main/scala/matryoshka/instances/fixedpoint/package.scala b/core/shared/src/main/scala/matryoshka/instances/fixedpoint/package.scala index 8039aa56..7511b4b4 100644 --- a/core/shared/src/main/scala/matryoshka/instances/fixedpoint/package.scala +++ b/core/shared/src/main/scala/matryoshka/instances/fixedpoint/package.scala @@ -19,7 +19,8 @@ package matryoshka.instances import matryoshka._, Recursive.ops._ import matryoshka.patterns._ -import scala.{Boolean, Int, Option} +import scala.{Boolean, Int, None, Option, Some} +import scala.annotation.{tailrec} import scalaz._, Scalaz._ @@ -27,16 +28,60 @@ import scalaz._, Scalaz._ * implemented explicitly as fixed-points. */ package object fixedpoint { - type Free[F[_], A] = Mu[CoEnv[A, F, ?]] - type Cofree[F[_], A] = Mu[EnvT[A, F, ?]] - type List[A] = Mu[ListF[A, ?]] + type Nat = Mu[Option] + object Nat { + def fromInt: CoalgebraM[Option, Option, Int] = + x => if (x < 0) None else Some(if (x > 0) (x - 1).some else None) + + def intPrism = CoalgebraPrism[Option, Int](fromInt)(height) + } + + implicit class NatOps[T[_[_]]: Recursive: Corecursive](self: T[Option]) { + def +(other: T[Option]) = self.cata[T[Option]] { + case None => other + case o => o.embed + } + def min(other: T[Option]) = + (self, other).ana(_.bimap(_.project, _.project) match { + case (None, _) => None + case (_, None) => None + case (Some(a), Some(b)) => Some((a, b)) + }) + + def max(other: T[Option]) = + (self, other).apo(_.bimap(_.project, _.project) match { + case (None, b) => b ∘ (_.left) + case (a, None) => a ∘ (_.left) + case (Some(a), Some(b)) => Some((a, b).right) + }) +} + + type Conat = Nu[Option] + object Conat { + val inf: Conat = ().ana[Nu, Option](_.some) + } + + type Free[F[_], A] = Mu[CoEnv[A, F, ?]] + type Cofree[F[_], A] = Mu[EnvT[A, F, ?]] + type List[A] = Mu[ListF[A, ?]] object List { def apply[A](elems: A*) = elems.ana[Mu, ListF[A, ?]](ListF.seqIso[A].reverseGet) + def fillƒ[A](elem: => A): Option ~> ListF[A, ?] = + new (Option ~> ListF[A, ?]) { + def apply[β](opt: Option[β]) = opt match { + case None => NilF() + case Some(b) => ConsF(elem, b) + } + } + def fill[A](n: Int)(elem: => A): List[A] = - n.ana[Mu, ListF[A, ?]](r => if (r > 0) ConsF(elem, r - 1) else NilF()) + n.hyloM( + transformToAlgebra[Mu, Id, Option, Option, ListF[A, ?]](fillƒ(elem).apply(_).point[Option]), + Nat.fromInt) + .getOrElse(NilF[A, List[A]]().embed) } implicit class ListOps[A](self: Mu[ListF[A, ?]]) { @@ -59,7 +104,7 @@ package object fixedpoint { implicit class StreamOps[A](self: Nu[(A, ?)]) { def head: A = self.project._1 def tail: Nu[(A, ?)] = self.project._2 - def drop(n: Int): Nu[(A, ?)] = + @tailrec final def drop(n: Int): Nu[(A, ?)] = if (n > 0) tail.drop(n - 1) else self def take(n: Int): Mu[ListF[A, ?]] = (n, self).ana[Mu, ListF[A, ?]] { @@ -76,16 +121,14 @@ package object fixedpoint { def now[A](a: A): Partial[A] = a.left[Nu[A \/ ?]].embed def later[A](partial: Partial[A]): Partial[A] = partial.right[A].embed + def delay[A](a: A): Option ~> (A \/ ?) = new (Option ~> (A \/ ?)) { + def apply[B](b: Option[B]) = b \/> a + } + /** Canonical function that diverges. */ def never[A]: Partial[A] = ().ana[Nu, A \/ ?](_.right) - implicit def monad: Monad[Partial] = new Monad[Partial] { - def point[A](a: => A) = now(a) - def bind[A, B](fa: Partial[A])(f: A => Partial[B]) = - fa.project.fold(f, l => later(bind(l)(f))) - } - /** This instance is not implicit, because it potentially runs forever. */ def equal[A: Equal]: Equal[Partial[A]] = @@ -97,6 +140,12 @@ package object fixedpoint { pf.lift ⋙ fromOption } + implicit val partialMonad: Monad[Partial] = new Monad[Partial] { + def point[A](a: => A) = Partial.now(a) + def bind[A, B](fa: Partial[A])(f: A => Partial[B]) = + fa.project.fold(f, l => Partial.later(bind(l)(f))) + } + implicit class PartialOps[A](self: Nu[A \/ ?]) { import Partial._ @@ -109,7 +158,10 @@ package object fixedpoint { /** Run to completion (if it completes). */ - def unsafePerformSync: A = self.project.fold(x => x, _.unsafePerformSync) + @tailrec final def unsafePerformSync: A = self.project match { + case -\/(a) => a + case \/-(p) => p.unsafePerformSync + } /** If two `Partial`s eventually have the same value, then they are * equivalent. diff --git a/core/shared/src/main/scala/matryoshka/package.scala b/core/shared/src/main/scala/matryoshka/package.scala index 597575b4..d9b82236 100644 --- a/core/shared/src/main/scala/matryoshka/package.scala +++ b/core/shared/src/main/scala/matryoshka/package.scala @@ -23,7 +23,17 @@ import scala.collection.immutable.{List, ::} import monocle._ import scalaz._, Scalaz._ -/** Generalized folds, unfolds, and refolds. */ +/** Generalized folds, unfolds, and refolds. + * + * @groupname algebras Algebras & Coalgebras + * @groupdesc algebras Generic algebras that operate over most functors. + * @groupname algtrans Algebra Transformations + * @groupdesc algtrans Operations that modify algebras in various ways to make them easier to combine with others. + * @groupname refolds Refolds + * @groupdesc refolds Traversals that apply an unfold and a fold in a single pass. + * @groupname dist Distributive Laws + * @groupdesc dist Natural transformations required for generalized folds and unfolds. + */ package object matryoshka extends CofreeInstances with FreeInstances { def lambek[T[_[_]]: Corecursive: Recursive, F[_]: Functor](tf: T[F]): @@ -34,26 +44,87 @@ package object matryoshka extends CofreeInstances with FreeInstances { T[F] = ft.ana(_ ∘ (_.project)) + /** @group algebras */ type GAlgebraM[W[_], M[_], F[_], A] = F[W[A]] => M[A] + /** @group algebras */ type GAlgebra[W[_], F[_], A] = GAlgebraM[W, Id, F, A] // F[W[A]] => A + /** @group algebras */ type AlgebraM[M[_], F[_], A] = GAlgebraM[Id, M, F, A] // F[A] => M[A] + /** @group algebras */ type Algebra[F[_], A] = GAlgebra[Id, F, A] // F[A] => A + /** @group algebras */ type ElgotAlgebraM[W[_], M[_], F[_], A] = W[F[A]] => M[A] + /** @group algebras */ type ElgotAlgebra[W[_], F[_], A] = ElgotAlgebraM[W, Id, F, A] // W[F[A]] => A + /** @group algebras */ type GCoalgebraM[N[_], M[_], F[_], A] = A => M[F[N[A]]] + /** @group algebras */ type GCoalgebra[N[_], F[_], A] = GCoalgebraM[N, Id, F, A] // A => N[F[A]] + /** @group algebras */ type CoalgebraM[M[_], F[_], A] = GCoalgebraM[Id, M, F, A] // A => F[M[A]] + /** @group algebras */ type Coalgebra[F[_], A] = GCoalgebra[Id, F, A] // A => F[A] + /** @group algebras */ type ElgotCoalgebraM[E[_], M[_], F[_], A] = A => M[E[F[A]]] + /** @group algebras */ type ElgotCoalgebra[E[_], F[_], A] = ElgotCoalgebraM[E, Id, F, A] // A => E[F[A]] + /** @group algebras */ + type GAlgebraicTransformM[T[_[_]], W[_], M[_], F[_], G[_]] = F[W[T[G]]] => M[G[T[G]]] + /** @group algebras */ + type AlgebraicTransformM[T[_[_]], M[_], F[_], G[_]] = GAlgebraicTransformM[T, Id, M, F, G] + /** @group algebras */ + type GAlgebraicTransform[T[_[_]], W[_], F[_], G[_]] = GAlgebraicTransformM[T, W, Id, F, G] + /** @group algebras */ + type AlgebraicTransform[T[_[_]], F[_], G[_]] = GAlgebraicTransformM[T, Id, Id, F, G] + + /** @group algebras */ + type GCoalgebraicTransformM[T[_[_]], M[_], N[_], F[_], G[_]] = F[T[F]] => N[G[M[T[F]]]] + /** @group algebras */ + type CoalgebraicTransformM[T[_[_]], N[_], F[_], G[_]] = GCoalgebraicTransformM[T, Id, N, F, G] + /** @group algebras */ + type GCoalgebraicTransform[T[_[_]], M[_], F[_], G[_]] = GCoalgebraicTransformM[T, M, Id, F, G] + /** @group algebras */ + type CoalgebraicTransform[T[_[_]], F[_], G[_]] = GCoalgebraicTransformM[T, Id, Id, F, G] + + /** + * + * @group algtrans + */ + def transformToAlgebra[T[_[_]]: Corecursive, W[_], M[_]: Functor, F[_], G[_]: Functor]( + self: GAlgebraicTransformM[T, W, M, F, G]): + GAlgebraM[W, M, F, T[G]] = + self(_) ∘ (_.embed) + /** An algebra and its dual form an isomorphism. */ - type AlgebraIso[F[_], A] = Iso[F[A], A] + type GAlgebraIso[W[_], M[_], F[_], A] = PIso[F[W[A]], F[M[A]], A, A] + object GAlgebraIso { + def apply[W[_], M[_], F[_], A](φ: F[W[A]] => A)(ψ: A => F[M[A]]): + GAlgebraIso[W, M, F, A] = + PIso(φ)(ψ) + } + + type AlgebraIso[F[_], A] = GAlgebraIso[Id, Id, F, A] object AlgebraIso { - def apply[F[_], A](get: F[A] => A)(reverseGet: A => F[A]): Iso[F[A], A] = - Iso(get)(reverseGet) + def apply[F[_], A](φ: F[A] => A)(ψ: A => F[A]): + AlgebraIso[F, A] = + Iso(φ)(ψ) + } + + type AlgebraPrism[F[_], A] = Prism[F[A], A] + object AlgebraPrism { + def apply[F[_], A](φ: F[A] => Option[A])(ψ: A => F[A]): + AlgebraPrism[F, A] = + Prism(φ)(ψ) + } + + type CoalgebraPrism[F[_], A] = Prism[A, F[A]] + object CoalgebraPrism { + def apply[F[_], A](ψ: A => Option[F[A]])(φ: F[A] => A): + CoalgebraPrism[F, A] = + Prism(ψ)(φ) } def recCorecIso[T[_[_]]: Recursive: Corecursive, F[_]: Functor] = @@ -67,8 +138,21 @@ package object matryoshka extends CofreeInstances with FreeInstances { def foldIso[T[_[_]]: Corecursive: Recursive, F[_]: Functor, A](alg: AlgebraIso[F, A]) = Iso[T[F], A](_.cata(alg.get))(_.ana(alg.reverseGet)) - /** A NaturalTransformation that sequences two types */ - type DistributiveLaw[F[_], G[_]] = λ[α => F[G[α]]] ~> λ[α => G[F[α]]] + /** There is a fold prism for any AlgebraPrism. + */ + def foldPrism[T[_[_]]: Corecursive: Recursive, F[_]: Traverse, A](alg: AlgebraPrism[F, A]) = + Prism[T[F], A](Recursive[T].cataM(_)(alg.getOption))(_.ana(alg.reverseGet)) + + /** There is an unfold prism for any CoalgebraPrism. + */ + def unfoldPrism[T[_[_]]: Corecursive: Recursive, F[_]: Traverse, A](coalg: CoalgebraPrism[F, A]) = + Prism[A, T[F]](_.anaM(coalg.getOption))(_.cata(coalg.reverseGet)) + + /** A NaturalTransformation that sequences two types + * + * @group dist + */ + type DistributiveLaw[F[_], G[_]] = (F ∘ G)#λ ~> (G ∘ F)#λ /** This folds a Free that you may think of as “already partially-folded”. * It’s also the fold of a decomposed `elgot`. @@ -116,17 +200,45 @@ package object matryoshka extends CofreeInstances with FreeInstances { /** Composition of an anamorphism and a catamorphism that avoids building the * intermediate recursive data structure. + * + * @group refolds */ def hylo[F[_]: Functor, A, B](a: A)(f: F[B] => B, g: A => F[A]): B = f(g(a) ∘ (hylo(_)(f, g))) - /** A Kleisli hylomorphism. */ + /** A Kleisli hylomorphism. + * + * @group refolds + */ def hyloM[M[_]: Monad, F[_]: Traverse, A, B](a: A)(f: F[B] => M[B], g: A => M[F[A]]): M[B] = - g(a) >>= (_.traverse(hyloM(_)(f, g)) >>= (f)) + g(a) >>= (_.traverse(hyloM(_)(f, g)) >>= f) + + /** `histo ⋘ ana` + * + * @group refolds + */ + def dyna[F[_]: Functor, A, B](a: A)(φ: F[Cofree[F, B]] => B, ψ: A => F[A]): B = + ghylo[Cofree[F, ?], Id, F, A, B](a)(distHisto, distAna, φ, ψ) + + /** `cata ⋘ futu` + * + * @group refolds + */ + def codyna[F[_]: Functor, A, B](a: A)(φ: F[B] => B, ψ: A => F[Free[F, A]]): B = + ghylo[Id, Free[F, ?], F, A, B](a)(distCata, distFutu, φ, ψ) + + /** `cataM ⋘ futuM` + * + * @group refolds + */ + def codynaM[M[_]: Monad, F[_]: Traverse, A, B](a: A)(φ: F[B] => M[B], ψ: A => M[F[Free[F, A]]]): M[B] = + ghyloM[Id, Free[F, ?], M, F, A, B](a)(distCata, distFutu, φ, ψ) /** A generalized version of a hylomorphism that composes any coalgebra and - * algebra. + * algebra. (`gcata ⋘ gana`) + * + * @group refolds */ def ghylo[W[_]: Comonad, M[_], F[_]: Functor, A, B]( a: A)( @@ -140,8 +252,30 @@ package object matryoshka extends CofreeInstances with FreeInstances { h(a.point[M]).copoint } + /** A Kleisli `ghylo` (`gcataM ⋘ ganaM`) + * + * @group refolds + */ + def ghyloM[W[_]: Comonad: Traverse, M[_]: Traverse, N[_]: Monad, F[_]: Traverse, A, B]( + a: A)( + w: DistributiveLaw[F, W], + m: DistributiveLaw[M, F], + f: F[W[B]] => N[B], + g: A => N[F[M[A]]])( + implicit M: Monad[M]): + N[B] = { + def h(x: M[A]): N[W[B]] = + (M.lift(g)(x).sequence >>= + (m(_: M[F[M[A]]]).traverse(y => h(y.join) ∘ (_.cojoin)))) ∘ + (w(_)) >>= + (_.traverse(f)) + h(a.point[M]) ∘ (_.copoint) + } + /** Similar to a hylomorphism, this composes a futumorphism and a * histomorphism. + * + * @group refolds */ def chrono[F[_]: Functor, A, B]( a: A)( @@ -149,12 +283,20 @@ package object matryoshka extends CofreeInstances with FreeInstances { B = ghylo[Cofree[F, ?], Free[F, ?], F, A, B](a)(distHisto, distFutu, g, f) + /** `cata ⋘ elgotGApo` + * + * @group refolds + */ def elgot[F[_]: Functor, A, B](a: A)(φ: F[B] => B, ψ: A => B \/ F[A]): B = { def h: A => B = (((x: B) => x) ||| ((x: F[A]) => φ(x ∘ h))) ⋘ ψ h(a) } + /** `elgotZygo ⋘ ana` + * + * @group refolds + */ def coelgot[F[_]: Functor, A, B](a: A)(φ: ((A, F[B])) => B, ψ: A => F[A]): B = { def h: A => B = @@ -162,6 +304,10 @@ package object matryoshka extends CofreeInstances with FreeInstances { h(a) } + /** `elgotZygoM ⋘ anaM` + * + * @group refolds + */ def coelgotM[M[_]] = new CoelgotMPartiallyApplied[M] final class CoelgotMPartiallyApplied[M[_]] { def apply[F[_]: Traverse, A, B](a: A)(φ: ((A, F[B])) => M[B], ψ: A => M[F[A]])(implicit M: Monad[M]): @@ -171,23 +317,43 @@ package object matryoshka extends CofreeInstances with FreeInstances { } } + /** + * + * @group dist + */ def distPara[T[_[_]]: Corecursive, F[_]: Functor]: DistributiveLaw[F, (T[F], ?)] = distZygo(_.embed) + /** + * + * @group dist + */ def distParaT[T[_[_]], F[_]: Functor, W[_]: Comonad]( t: DistributiveLaw[F, W])( implicit T: Corecursive[T]): DistributiveLaw[F, EnvT[T[F], W, ?]] = distZygoT(_.embed, t) + /** + * + * @group dist + */ def distCata[F[_]]: DistributiveLaw[F, Id] = NaturalTransformation.refl + /** + * + * @group dist + */ def distZygo[F[_]: Functor, B](g: F[B] => B) = new DistributiveLaw[F, (B, ?)] { def apply[α](m: F[(B, α)]) = (g(m ∘ (_._1)), m ∘ (_._2)) } + /** + * + * @group dist + */ def distZygoT[F[_], W[_]: Comonad, B]( g: F[B] => B, k: DistributiveLaw[F, W])( implicit F: Functor[F]) = @@ -198,12 +364,20 @@ package object matryoshka extends CofreeInstances with FreeInstances { k(F.lift[EnvT[B, W, α], W[α]](_.lower)(fe)))) } + /** + * + * @group dist + */ def distHisto[F[_]: Functor] = new DistributiveLaw[F, Cofree[F, ?]] { def apply[α](m: F[Cofree[F, α]]) = distGHisto[F, F](NaturalTransformation.refl[λ[α => F[F[α]]]]).apply(m) } + /** + * + * @group dist + */ def distGHisto[F[_], H[_]]( k: DistributiveLaw[F, H])( implicit F: Functor[F], H: Functor[H]) = @@ -214,23 +388,43 @@ package object matryoshka extends CofreeInstances with FreeInstances { k(F.lift[Cofree[H, α], H[Cofree[H, α]]](_.tail)(as)))) } + /** + * + * @group dist + */ def distAna[F[_]]: DistributiveLaw[Id, F] = NaturalTransformation.refl + /** + * + * @group dist + */ def distApo[T[_[_]]: Recursive, F[_]: Functor]: DistributiveLaw[T[F] \/ ?, F] = distGApo(_.project) + /** + * + * @group dist + */ def distGApo[F[_]: Functor, B](g: B => F[B]) = new DistributiveLaw[B \/ ?, F] { def apply[α](m: B \/ F[α]) = m.fold(g(_) ∘ (_.left), _ ∘ (_.right)) } + /** + * + * @group dist + */ def distFutu[F[_]: Functor] = new DistributiveLaw[Free[F, ?], F] { def apply[α](m: Free[F, F[α]]) = distGFutu[F, F](NaturalTransformation.refl[λ[α => F[F[α]]]]).apply(m) } + /** + * + * @group dist + */ def distGFutu[H[_], F[_]]( k: DistributiveLaw[H, F])( implicit H: Functor[H], F: Functor[F]): DistributiveLaw[Free[H, ?], F] = @@ -271,30 +465,54 @@ package object matryoshka extends CofreeInstances with FreeInstances { else fa.toList.drop(index).headOption /** Turns any F-algebra, into an identical one that attributes the tree with - * the results for each node. */ + * the results for each node. + * + * @group algtrans + */ def attributeAlgebraM[F[_]: Functor, M[_]: Functor, A](f: F[A] => M[A]): F[Cofree[F, A]] => M[Cofree[F, A]] = fa => f(fa ∘ (_.head)) ∘ (Cofree(_, fa)) + /** + * + * @group algtrans + */ def attributeAlgebra[F[_]: Functor, A](f: F[A] => A) = attributeAlgebraM[F, Id, A](f) + /** + * + * @group algtrans + */ def attributeCoalgebra[F[_], B](ψ: Coalgebra[F, B]): Coalgebra[EnvT[B, F, ?], B] = b => EnvT[B, F, B]((b, ψ(b))) + /** + * + * @group algtrans + */ def attrK[F[_]: Functor, A](k: A) = attributeAlgebra[F, A](Function.const(k)) + /** + * + * @group algtrans + */ def attrSelf[T[_[_]]: Corecursive, F[_]: Functor] = attributeAlgebra[F, T[F]](_.embed) /** NB: Since Cofree carries the functor, the resulting algebra is a cata, not - * a para. */ + * a para. + * + * @group algtrans + */ def attributePara[T[_[_]]: Corecursive, F[_]: Functor, A](f: F[(T[F], A)] => A): F[Cofree[F, A]] => Cofree[F, A] = fa => Cofree(f(fa ∘ (x => (Recursive[Cofree[?[_], A]].convertTo[F, T](x), x.head))), fa) /** A function to be called like `attributeElgotM[M](myElgotAlgebraM)`. + * + * @group algtrans */ object attributeElgotM { def apply[W[_], M[_]] = new Aux[W, M] @@ -306,56 +524,86 @@ package object matryoshka extends CofreeInstances with FreeInstances { } } + /** + * + * @group algtrans + */ implicit def GAlgebraZip[W[_]: Functor, F[_]: Functor]: Zip[GAlgebra[W, F, ?]] = new Zip[GAlgebra[W, F, ?]] { def zip[A, B](a: ⇒ GAlgebra[W, F, A], b: ⇒ GAlgebra[W, F, B]) = node => (a(node ∘ (_ ∘ (_._1))), b(node ∘ (_ ∘ (_._2)))) } + /** + * + * @group algtrans + */ implicit def AlgebraZip[F[_]: Functor] = GAlgebraZip[Id, F] + /** + * + * @group algtrans + */ implicit def ElgotAlgebraMZip[W[_]: Functor, M[_]: Applicative, F[_]: Functor]: Zip[ElgotAlgebraM[W, M, F, ?]] = new Zip[ElgotAlgebraM[W, M, F, ?]] { def zip[A, B](a: ⇒ ElgotAlgebraM[W, M, F, A], b: ⇒ ElgotAlgebraM[W, M, F, B]) = w => Bitraverse[(?, ?)].bisequence((a(w ∘ (_ ∘ (_._1))), b(w ∘ (_ ∘ (_._2))))) } + /** + * + * @group algtrans + */ implicit def ElgotAlgebraZip[W[_]: Functor, F[_]: Functor] = ElgotAlgebraMZip[W, Id, F] // (ann, node) => node.unfzip.bimap(f(ann, _), g(ann, _)) /** Repeatedly applies the function to the result as long as it returns Some. * Finally returns the last non-None value (which may be the initial input). + * + * @group algtrans */ def repeatedly[A](f: A => Option[A]): A => A = expr => f(expr).fold(expr)(repeatedly(f)) /** Converts a failable fold into a non-failable, by simply returning the * argument upon failure. + * + * @group algtrans */ def orOriginal[A](f: A => Option[A]): A => A = expr => f(expr).getOrElse(expr) /** Converts a failable fold into a non-failable, by returning the default - * upon failure. + * upon failure. + * + * @group algtrans */ def orDefault[A, B](default: B)(f: A => Option[B]): A => B = expr => f(expr).getOrElse(default) /** Count the instinces of `form` in the structure. + * + * @group algebras */ def count[T[_[_]]: Recursive, F[_]: Functor: Foldable](form: T[F]): F[(T[F], Int)] => Int = e => e.foldRight(if (e ∘ (_._1) == form.project) 1 else 0)(_._2 + _) /** The number of nodes in this structure. + * + * @group algebras */ def size[F[_]: Foldable]: F[Int] => Int = _.foldRight(1)(_ + _) /** The largest number of hops from a node to a leaf. + * + * @group algebras */ - def height[F[_]: Foldable]: F[Int] => Int = _.foldRight(0)(_ max _) + def height[F[_]: Foldable]: F[Int] => Int = _.foldRight(-1)(_ max _) + 1 /** Combines a tuple of zippable functors. + * + * @group algebras */ def zipTuple[T[_[_]]: Recursive, F[_]: Functor: Zip]: Coalgebra[F, (T[F], T[F])] = @@ -363,18 +611,24 @@ package object matryoshka extends CofreeInstances with FreeInstances { /** Aligns “These” into a single structure, short-circuting when we hit a * “This” or “That”. + * + * @group algebras */ def alignThese[T[_[_]]: Recursive, F[_]: Align]: ElgotCoalgebra[T[F] \/ ?, F, T[F] \&/ T[F]] = _.fold(_.left, _.left, (a, b) => a.project.align(b.project).right) /** Merges a tuple of functors, if possible. + * + * @group algebras */ def mergeTuple[T[_[_]]: Recursive, F[_]: Functor: Merge]: CoalgebraM[Option, F, (T[F], T[F])] = p => Merge[F].merge[T[F], T[F]](p._1.project, p._2.project) /** Generates an infinite sequence from two seed values. + * + * @group algebras */ def binarySequence[A](relation: (A, A) => A): Coalgebra[(A, ?), (A, A)] = { case (a1, a2) => @@ -385,23 +639,27 @@ package object matryoshka extends CofreeInstances with FreeInstances { /** Converts a fixed-point structure into a generic Tree. * One use of this is using `.cata(toTree).drawTree` rather than `.show` to * get a pretty-printed tree. + * + * @group algebras */ def toTree[F[_]: Functor: Foldable]: Algebra[F, Tree[F[Unit]]] = x => Tree.Node(x.void, x.toStream) + sealed abstract class ∘[F[_], G[_]] { type λ[A] = F[G[A]] } + /** To avoid diverging implicits with fixed-point types, we need to defer the * lookup. We do this with a `NaturalTransformation` (although there * are more type class-y solutions available now). This implicit allows those * implicits to be looked up when searching for a traditionally-defined * instance. */ - implicit def NTEqual[F[_], A](implicit A: Equal[A], F: Equal ~> λ[α => Equal[F[α]]]): + implicit def NTEqual[F[_], A](implicit A: Equal[A], F: Equal ~> (Equal ∘ F)#λ): Equal[F[A]] = F(A) /** See `NTEqual`. */ - implicit def NTShow[F[_], A](implicit A: Show[A], F: Show ~> λ[α => Show[F[α]]]): + implicit def NTShow[F[_], A](implicit A: Show[A], F: Show ~> (Show ∘ F)#λ): Show[F[A]] = F(A) diff --git a/core/shared/src/main/scala/matryoshka/patterns/CoEnv.scala b/core/shared/src/main/scala/matryoshka/patterns/CoEnv.scala index 983ea064..78e42c25 100644 --- a/core/shared/src/main/scala/matryoshka/patterns/CoEnv.scala +++ b/core/shared/src/main/scala/matryoshka/patterns/CoEnv.scala @@ -56,19 +56,19 @@ sealed abstract class CoEnvInstances extends CoEnvInstances0 { } // TODO: write a test to ensure the two monad instances are identical - implicit def monadCo[F[_]: Applicative: Comonad, A]: Monad[CoEnv[A, F, ?]] = - new Monad[CoEnv[A, F, ?]] { - def bind[B, C](fa: CoEnv[A, F, B])(f: (B) ⇒ CoEnv[A, F, C]) = - fa.run.fold(a => CoEnv(a.left), fb => f(fb.copoint)) - def point[B](x: => B) = CoEnv(x.point[F].right) - } + // implicit def monadCo[F[_]: Applicative: Comonad, A]: Monad[CoEnv[A, F, ?]] = + // new Monad[CoEnv[A, F, ?]] { + // def bind[B, C](fa: CoEnv[A, F, B])(f: (B) ⇒ CoEnv[A, F, C]) = + // CoEnv(fa.run >>= (fb => f(fb.copoint).run)) + // def point[B](x: => B) = CoEnv(x.point[F].right) + // } } sealed abstract class CoEnvInstances0 { - implicit def monad[F[_]: Monad: Traverse, A]: Monad[CoEnv[A, F, ?]] = - new Monad[CoEnv[A, F, ?]] { - def bind[B, C](fa: CoEnv[A, F, B])(f: (B) ⇒ CoEnv[A, F, C]) = - CoEnv(fa.run.fold(_.left, _.traverse[CoEnv[A, F, ?], C](f).run.map(_.join))) - def point[B](x: => B) = CoEnv(x.point[F].right) - } + // implicit def monad[F[_]: Monad: Traverse, A]: Monad[CoEnv[A, F, ?]] = + // new Monad[CoEnv[A, F, ?]] { + // def bind[B, C](fa: CoEnv[A, F, B])(f: (B) ⇒ CoEnv[A, F, C]) = + // CoEnv(fa.run >>= (_.traverse[CoEnv[A, F, ?], C](f).run.map(_.join))) + // def point[B](x: => B) = CoEnv(x.point[F].right) + // } } diff --git a/core/shared/src/main/scala/matryoshka/patterns/EnvT.scala b/core/shared/src/main/scala/matryoshka/patterns/EnvT.scala index 809b9ded..392ed4e4 100644 --- a/core/shared/src/main/scala/matryoshka/patterns/EnvT.scala +++ b/core/shared/src/main/scala/matryoshka/patterns/EnvT.scala @@ -53,6 +53,13 @@ sealed abstract class EnvTInstances extends EnvTInstances0 { implicit def envTComonad[E, W[_]](implicit W0: Comonad[W]): Comonad[EnvT[E, W, ?]] = new EnvTComonad[E, W] { implicit def W: Comonad[W] = W0 } + + implicit def equal[E: Equal, W[_]](implicit W: Equal ~> (Equal ∘ W)#λ): + Equal ~> (Equal ∘ EnvT[E, W, ?])#λ = + new (Equal ~> (Equal ∘ EnvT[E, W, ?])#λ) { + def apply[A](eq: Equal[A]) = + Equal.equal((a, b) => a.ask ≟ b.ask && W(eq).equal(a.lower, b.lower)) + } } trait EnvTFunctions { diff --git a/core/shared/src/main/scala/matryoshka/patterns/ListF.scala b/core/shared/src/main/scala/matryoshka/patterns/ListF.scala index c1339bed..27403b62 100644 --- a/core/shared/src/main/scala/matryoshka/patterns/ListF.scala +++ b/core/shared/src/main/scala/matryoshka/patterns/ListF.scala @@ -52,7 +52,7 @@ object ListF { case NilF() => Seq.empty } (s => s.headOption.fold[ListF[A, Seq[A]]](NilF())(ConsF(_, s.tail))) - implicit def equal[A: Equal, B]: Equal ~> λ[β => Equal[ListF[A, β]]] = + implicit def equal[A: Equal]: Equal ~> λ[β => Equal[ListF[A, β]]] = new (Equal ~> λ[β => Equal[ListF[A, β]]]) { def apply[β](eq: Equal[β]) = Equal.equal((a, b) => (a, b) match { case (ConsF(h1, t1), ConsF(h2, t2)) => h1 ≟ h2 && eq.equal(t1, t2) @@ -61,7 +61,7 @@ object ListF { }) } - implicit def show[A: Show, B]: Show ~> λ[β => Show[ListF[A, β]]] = + implicit def show[A: Show]: Show ~> λ[β => Show[ListF[A, β]]] = new (Show ~> λ[β => Show[ListF[A, β]]]) { def apply[β](show: Show[β]) = Show.show { case ConsF(h, t) => h.show ++ Cord("::") ++ show.show(t) @@ -76,7 +76,7 @@ object ListF { implicit G: Applicative[G]) = fab match { case NilF() => G.point(NilF()) - case ConsF(a, b) => (f(a) ⊛ g(b))(ConsF(_, _)) + case ConsF(a, b) => (f(a) ⊛ g(b))(ConsF(_, _)) } } diff --git a/project/plugins.sbt b/project/plugins.sbt index dbc84849..d9f4ebe7 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,10 +1,12 @@ resolvers += "Jenkins-CI" at "http://repo.jenkins-ci.org/repo" libraryDependencies += "org.kohsuke" % "github-api" % "1.59" -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.9") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.3.3") +addSbtPlugin("de.heikoseeberger" % "sbt-header" % "1.5.0") addSbtPlugin("org.scala-sbt.plugins" % "sbt-onejar" % "0.8") +addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.9") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.3.3") +addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "1.0.0") +addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.3.3") addSbtPlugin("org.brianmckenna" % "sbt-wartremover" % "0.14") -addSbtPlugin("de.heikoseeberger" % "sbt-header" % "1.5.0") -addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0")