Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add some new refolds. #27

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG/breaking.md
Original file line number Diff line number Diff line change
@@ -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
62 changes: 59 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
27 changes: 14 additions & 13 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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")),

Expand Down
1 change: 0 additions & 1 deletion core/jvm/src/test/scala/matryoshka/example/Example.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
93 changes: 76 additions & 17 deletions core/jvm/src/test/scala/matryoshka/helpers/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]] =
Expand All @@ -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
Expand All @@ -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))
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 51 additions & 0 deletions core/jvm/src/test/scala/matryoshka/instances/fixedpoint/nat.scala
Original file line number Diff line number Diff line change
@@ -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))
}
}
}
Loading