diff --git a/CHANGELOG/minor.md b/CHANGELOG/minor.md new file mode 100644 index 00000000..8cee7a02 --- /dev/null +++ b/CHANGELOG/minor.md @@ -0,0 +1 @@ +- move `attributeCoelgotM` to `attributeElgotAlgebraM`, updating it to the generalized form. diff --git a/core/src/main/scala/matryoshka/AlgebraOps.scala b/core/src/main/scala/matryoshka/AlgebraOps.scala deleted file mode 100644 index 32b9754c..00000000 --- a/core/src/main/scala/matryoshka/AlgebraOps.scala +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2014 - 2015 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 - -import scalaz._ -import scalaz.syntax.comonad._ - -sealed class AlgebraOps[F[_], A](self: Algebra[F, A]) { - def generalize[W[_]: Comonad](implicit F: Functor[F]): GAlgebra[W, F, A] = - node => self(node ∘ (_.copoint)) - - def generalizeElgot[W[_]: Comonad]: ElgotAlgebra[W, F, A] = - w => self(w.copoint) -} diff --git a/core/src/main/scala/matryoshka/CoalgebraOps.scala b/core/src/main/scala/matryoshka/CoalgebraOps.scala deleted file mode 100644 index 2b98528d..00000000 --- a/core/src/main/scala/matryoshka/CoalgebraOps.scala +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2014 - 2015 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 - -import scalaz._ -import scalaz.syntax.monad._ - -sealed class CoalgebraOps[F[_], A](self: Coalgebra[F, A]) { - def generalize[M[_]: Monad](implicit F: Functor[F]): GCoalgebra[M, F, A] = - self(_).map(_.map(_.point[M])) - - def generalizeM[M[_]: Monad]: CoalgebraM[M, F, A] = self(_).point[M] - - def generalizeElgot[M[_]: Monad]: CoalgebraM[M, F, A] = self.generalizeM -} diff --git a/core/src/main/scala/matryoshka/CofreeOps.scala b/core/src/main/scala/matryoshka/CofreeOps.scala deleted file mode 100644 index e56db101..00000000 --- a/core/src/main/scala/matryoshka/CofreeOps.scala +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2014 - 2015 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 - -import scalaz._ - -sealed class CofreeOps[F[_], A](self: Cofree[F, A]) { - def elgotCata[B](φ: ((A, F[B])) => B)(implicit F: Functor[F]): B = - matryoshka.elgotCata(self)(φ) - - def elgotCataM[M[_]: Monad, B]( - φ: ((A, F[B])) => M[B])( - implicit F: Traverse[F]): - M[B] = - matryoshka.elgotCataM(self)(φ) - - def cofCataM[M[_]: Monad, B](f: (A, F[B]) => M[B])(implicit F: Traverse[F]): - M[B] = - matryoshka.cofCataM(self)(f) -} diff --git a/core/src/main/scala/matryoshka/FreeOps.scala b/core/src/main/scala/matryoshka/FreeOps.scala deleted file mode 100644 index ed27319a..00000000 --- a/core/src/main/scala/matryoshka/FreeOps.scala +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2014 - 2015 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 - -import scalaz._ - -sealed class FreeOps[F[_], A](self: Free[F, A]) { - def interpretCata(φ: F[A] => A)(implicit F: Functor[F]): A = - matryoshka.interpretCata(self)(φ) -} diff --git a/core/src/main/scala/matryoshka/IdOps.scala b/core/src/main/scala/matryoshka/IdOps.scala index 87b3f18d..de44ad8b 100644 --- a/core/src/main/scala/matryoshka/IdOps.scala +++ b/core/src/main/scala/matryoshka/IdOps.scala @@ -19,21 +19,21 @@ package matryoshka import scalaz._ sealed class IdOps[A](self: A) { - def hylo[F[_]: Functor, B](f: F[B] => B, g: A => F[A]): B = + def hylo[F[_]: Functor, B](f: Algebra[F, B], g: Coalgebra[F, A]): B = matryoshka.hylo(self)(f, g) - def hyloM[M[_]: Monad, F[_]: Traverse, B](f: F[B] => M[B], g: A => M[F[A]]): + def hyloM[M[_]: Monad, F[_]: Traverse, B](f: AlgebraM[M, F, B], g: CoalgebraM[M, F, A]): M[B] = matryoshka.hyloM(self)(f, g) def ghylo[F[_]: Functor, W[_]: Comonad, M[_]: Monad, B]( w: DistributiveLaw[F, W], m: DistributiveLaw[M, F], - f: F[W[B]] => B, - g: A => F[M[A]]): + f: GAlgebra[W, F, B], + g: GCoalgebra[M, F, A]): B = matryoshka.ghylo(self)(w, m, f, g) def chrono[F[_]: Functor, B]( - g: F[Cofree[F, B]] => B, f: A => F[Free[F, A]]): + g: GAlgebra[Cofree[F, ?], F, B], f: GCoalgebra[Free[F, ?], F, A]): B = matryoshka.chrono(self)(g, f) @@ -47,7 +47,7 @@ sealed class IdOps[A](self: A) { def elgotAna[F[_]: Functor, B](ψ: A => B \/ F[A]): Free[F, B] = matryoshka.elgotAna(self)(ψ) - def elgot[F[_]: Functor, B](φ: F[B] => B, ψ: A => B \/ F[A]): B = + def elgot[F[_]: Functor, B](φ: Algebra[F, B], ψ: ElgotCoalgebra[B \/ ?, F, A]): B = matryoshka.elgot(self)(φ, ψ) def coelgot[F[_]: Functor, B](φ: ((A, F[B])) => B, ψ: A => F[A]): B = diff --git a/core/src/main/scala/matryoshka/Proj.scala b/core/src/main/scala/matryoshka/Proj.scala index ba1568ca..fc3830da 100644 --- a/core/src/main/scala/matryoshka/Proj.scala +++ b/core/src/main/scala/matryoshka/Proj.scala @@ -24,7 +24,7 @@ import scalaz._ /** An extractor to make it easier to pattern-match on arbitrary Recursive * structures. * - * NB: This extractor is irrufutable and doesn’t break exhaustiveness checking. + * NB: This extractor is irrefutable and doesn’t break exhaustiveness checking. */ object Proj { def unapply[T[_[_]]: Recursive, F[_]: Functor](obj: T[F]): Some[F[T[F]]] = diff --git a/core/src/main/scala/matryoshka/TraverseT.scala b/core/src/main/scala/matryoshka/TraverseT.scala index fa8ebda0..c8ccaa46 100644 --- a/core/src/main/scala/matryoshka/TraverseT.scala +++ b/core/src/main/scala/matryoshka/TraverseT.scala @@ -40,6 +40,10 @@ import simulacrum.typeclass def transAnaM[M[_]: Monad, F[_]: Functor, G[_]: Traverse](t: T[F])(f: F[T[F]] => M[G[T[F]]]): M[T[G]] = traverse(t)(f(_).flatMap(_.traverse(transAnaM(_)(f)))) + def transApoM[M[_]: Monad, F[_]: Functor, G[_]: Traverse](t: T[F])(f: F[T[F]] => M[G[T[G] \/ T[F]]]): + M[T[G]] = + traverse(t)(f(_).flatMap(_.traverse(_.fold(_.point[M], transApoM(_)(f))))) + def topDownCataM[F[_]: Traverse, M[_]: Monad, A]( t: T[F], a: A)( f: (A, T[F]) => M[(A, T[F])]): diff --git a/core/src/main/scala/matryoshka/algebra.scala b/core/src/main/scala/matryoshka/algebra.scala new file mode 100644 index 00000000..0b086fb6 --- /dev/null +++ b/core/src/main/scala/matryoshka/algebra.scala @@ -0,0 +1,154 @@ +/* + * Copyright 2014 - 2015 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 + +import scala.Function1 + +import scalaz._, Leibniz._, Scalaz._ + +/** The most general algebra, using both generalized and Elgot comonads as + * well as a monad. + */ +final class GElgotAlgebraM[E[_], G[_], M[_], F[_], A](f: E[F[G[A]]] => M[A]) + extends Function1[E[F[G[A]]], M[A]] { + def apply(v1: E[F[G[A]]]) = f(v1) + + def attribute(implicit E: Comonad[E], G: Comonad[G], M: Functor[M], F: Functor[F]) = + matryoshka.attribute[E, G, M, F, A](this) + + def generalizeElgot[EE[_]: Comonad]( + implicit ev: GElgotAlgebraM[E, G, M, F, A] === GAlgebraM[G, M, F, A]): + GElgotAlgebraM[EE, G, M, F, A] = + matryoshka.generalizeW[EE, F[G[A]], M[A]](ev(this).apply) + + def generalize[GG[_]: Comonad](implicit EF: Functor[λ[α => E[F[α]]]], ev: GElgotAlgebraM[E, G, M, F, A] === ElgotAlgebraM[E, M, F, A]): + GElgotAlgebraM[E, GG, M, F, A] = + matryoshka.generalizeAlgebra[λ[α => E[F[α]]], GG, Id, M, F, A](ev(this).apply) + + def generalizeM[MM[_]: Applicative]( + implicit ev: GElgotAlgebraM[E, G, M, F, A] === GElgotAlgebra[E, G, F, A]): + GElgotAlgebraM[E, G, MM, F, A] = + matryoshka.generalizeM[MM, E[F[G[A]]], A](ev(this).apply) + + def zip[B]( + b: ⇒ GElgotAlgebraM[E, G, M, F, B])( + implicit E: Functor[E], G: Functor[G], M: Applicative[M], F: Functor[F]): + GElgotAlgebraM[E, G, M, F, (A, B)]= + node => (this.f(node ∘ (_ ∘ (_ ∘ (_._1)))) ⊛ b(node ∘ (_ ∘ (_ ∘ (_._2)))))((_, _)) +} +object GElgotAlgebraM { + implicit def gElgotAlgebraMZip[E[_]: Functor, G[_]: Functor, M[_]: Applicative, F[_]: Functor]: + Zip[GElgotAlgebraM[E, G, M, F, ?]] = + new Zip[GElgotAlgebraM[E, G, M, F, ?]] { + def zip[A, B]( + a: ⇒ GElgotAlgebraM[E, G, M, F, A], + b: ⇒ GElgotAlgebraM[E, G, M, F, B]) = + a.zip(b) + } +} + +sealed class GElgotCoalgebraM[E[_], G[_], M[_], F[_], A](f: A => M[E[F[G[A]]]]) + extends Function1[A, M[E[F[G[A]]]]] { + def apply(v1: A) = f(v1) + + def generalizeM[MM[_]: Applicative](implicit ev: GElgotCoalgebraM[E, G, M, F, A] === GElgotCoalgebra[E, G, F, A]): + GElgotCoalgebraM[E, G, MM, F, A] = + matryoshka.generalizeM[MM, A, E[F[G[A]]]](ev(this).apply) + + def generalizeElgot[EE[_]: Applicative]( + implicit M: Functor[M], + ev: GElgotCoalgebraM[E, G, M, F, A] === GCoalgebraM[G, M, F, A]): + GElgotCoalgebraM[EE, G, M, F, A] = + matryoshka.generalizeCoalgebra[M, EE, λ[α => F[G[α]]], A](ev(this).apply) + + def generalize[GG[_]: Applicative](implicit MEF: Functor[λ[α => M[E[F[α]]]]], ev: GElgotCoalgebraM[E, G, M, F, A] === ElgotCoalgebraM[E, M, F, A]): + GElgotCoalgebraM[E, GG, M, F, A] = + matryoshka.generalizeCoalgebra[λ[α => M[E[F[α]]]], GG, Id, A](ev(this).apply) +} + +// NB: This class is needed to avoid the `Id[Id[_]]` “cyclic alias” issue. +final class GCoalgebra[G[_], F[_], A](f: A => F[G[A]]) + extends GElgotCoalgebraM[Id, G, Id, F, A](f) + +sealed trait ZeroIdInstances { + implicit def toGElgotAlgebraM[E[_], G[_], M[_], F[_], A](f: E[F[G[A]]] => M[A]): GElgotAlgebraM[E, G, M, F, A] = + new GElgotAlgebraM[E, G, M, F, A](f) + + implicit def toGElgotCoalgebraM[E[_], G[_], M[_], F[_], A](f: A => M[E[F[G[A]]]]): GElgotCoalgebraM[E, G, M, F, A] = + new GElgotCoalgebraM[E, G, M, F, A](f) +} + +sealed trait OneIdInstances extends ZeroIdInstances { +} + +sealed trait TwoIdInstances extends OneIdInstances { + // FIXME: somehow this causes an ambiguous implicit with a lower priority + // implicit. + // implicit def toElgotAlgebra[E[_], F[_], A](f: E[F[A]] => A): + // ElgotAlgebra[E, F, A] = + // new GElgotAlgebraM[E, Id, Id, F, A](f) + // Poor man’s unapply trick + implicit def toElgotAlgebraU[E[_[_], _], F[_], A, X[_]](f: E[X, F[A]] => A): + ElgotAlgebra[E[X, ?], F, A] = + new GElgotAlgebraM[E[X, ?], Id, Id, F, A](f) + + implicit def toElgotCoalgebra[E[_], F[_], A](f: A => E[F[A]]): + ElgotCoalgebra[E, F, A] = + new GElgotCoalgebraM[E, Id, Id, F, A](f) + // Poor man’s unapply trick + implicit def toElgotCoalgebraU[E[_[_], _], F[_], A, X[_]](f: A => E[X, F[A]]): + ElgotCoalgebra[E[X, ?], F, A] = + new GElgotCoalgebraM[E[X, ?], Id, Id, F, A](f) + + implicit def toGAlgebra[G[_], F[_], A](f: F[G[A]] => A): GAlgebra[G, F, A] = + new GElgotAlgebraM[Id, G, Id, F, A](f) + // Poor man’s unapply trick + implicit def toGAlgebraU[G[_[_], _], F[_], A, X[_]](f: F[G[X, A]] => A): + GAlgebra[G[X, ?], F, A] = + new GElgotAlgebraM[Id, G[X, ?], Id, F, A](f) + + implicit def toGCoalgebra[G[_], F[_], A](f: A => F[G[A]]): + GCoalgebra[G, F, A] = + new GCoalgebra[G, F, A](f) + // Poor man’s unapply trick + implicit def toGCoalgebraU[G[_[_], _], F[_], A, X[_]](f: A => F[G[X, A]]): + GCoalgebra[G[X, ?], F, A] = + new GCoalgebra[G[X, ?], F, A](f) + + implicit def toAlgebraM[M[_], F[_], A](f: F[A] => M[A]): AlgebraM[M, F, A] = + new GElgotAlgebraM[Id, Id, M, F, A](f) + // Poor man’s unapply trick + implicit def toAlgebraMU[M[_[_], _], F[_], A, X[_]](f: F[A] => M[X, A]): + AlgebraM[M[X, ?], F, A] = + new GElgotAlgebraM[Id, Id, M[X, ?], F, A](f) + + implicit def toCoalgebraM[M[_], F[_], A](f: A => M[F[A]]): + CoalgebraM[M, F, A] = + new GElgotCoalgebraM[Id, Id, M, F, A](f) + // Poor man’s unapply trick + implicit def toCoalgebraMU[M[_[_], _], F[_], A, X[_]](f: A => M[X, F[A]]): + CoalgebraM[M[X, ?], F, A] = + new GElgotCoalgebraM[Id, Id, M[X, ?], F, A](f) +} + +trait ThreeIdInstances extends TwoIdInstances { + implicit def toAlgebra[F[_], A](f: F[A] => A): Algebra[F, A] = + new GElgotAlgebraM[Id, Id, Id, F, A](f) + + implicit def toCoalgebra[F[_], A](f: A => F[A]): Coalgebra[F, A] = + new GCoalgebra[Id, F, A](f) +} diff --git a/core/src/main/scala/matryoshka/package.scala b/core/src/main/scala/matryoshka/package.scala index 59f5c3d2..894c663c 100644 --- a/core/src/main/scala/matryoshka/package.scala +++ b/core/src/main/scala/matryoshka/package.scala @@ -22,8 +22,26 @@ import scala.collection.immutable.{List, ::} import scalaz._, Scalaz._ import simulacrum.typeclass -/** Generalized folds, unfolds, and refolds. */ -package object matryoshka extends CofreeInstances with FreeInstances { +/** Generalized folds, unfolds, and refolds. + * + * @groupname Algebra [Co]Algebras + * @groupname Fold Folds + * @groupdesc Fold + * These are folds that don’t fit under [[matryoshka.Recursive]] + * because they fold from a specific fixed point operator. + * @groupname Unfold Unfolds + * @groupdesc Unfold + * These are unfolds that don’t fit under [[matryoshka.Corecursive]] + * because they unfold to a specific fixed point operator. + * @groupname Refold Refolds + * @groupdesc Refold + * These operations apply a function both on the way to the leaves + * of the structure as well as on the way back, so they avoid ever + * constructing an intermediate fixed point structure. + * @groupname Dist DistributiveLaws + */ +package object matryoshka + extends ThreeIdInstances with CofreeInstances with FreeInstances { def lambek[T[_[_]]: Corecursive: Recursive, F[_]: Functor](tf: T[F]): F[T[F]] = @@ -33,34 +51,53 @@ package object matryoshka extends CofreeInstances with FreeInstances { T[F] = ft.ana(_ ∘ (_.project)) - type GAlgebraM[W[_], M[_], F[_], A] = F[W[A]] => M[A] + type GElgotAlgebra[E[_], G[_], F[_], A] = GElgotAlgebraM[E, G, Id, F, A] // E[F[G[A]]] => A + type GAlgebraM[G[_], M[_], F[_], A] = GElgotAlgebraM[Id, G, M, F, A] // F[W[A]] => M[A] type GAlgebra[W[_], F[_], A] = GAlgebraM[W, Id, F, A] // F[W[A]] => A - type AlgebraM[M[_], F[_], A] = GAlgebraM[Id, M, F, A] // F[A] => M[A] - type Algebra[F[_], A] = GAlgebra[Id, F, A] // F[A] => A - type ElgotAlgebraM[W[_], M[_], F[_], A] = W[F[A]] => M[A] + type AlgebraM[M[_], F[_], A] = GElgotAlgebraM[Id, Id, M, F, A] // F[A] => M[A] + type Algebra[F[_], A] = GAlgebra[Id, F, A] // F[A] => A + type ElgotAlgebraM[E[_], M[_], F[_], A] = GElgotAlgebraM[E, Id, M, F, A] // W[F[A]] => M[A] type ElgotAlgebra[W[_], F[_], A] = ElgotAlgebraM[W, Id, F, A] // W[F[A]] => A - type GCoalgebraM[N[_], M[_], F[_], A] = A => M[F[N[A]]] - type GCoalgebra[N[_], F[_], A] = GCoalgebraM[N, Id, F, A] // A => N[F[A]] - type CoalgebraM[M[_], F[_], A] = GCoalgebraM[Id, M, F, A] // A => F[M[A]] - type Coalgebra[F[_], A] = GCoalgebra[Id, F, A] // A => F[A] - + type GElgotCoalgebra[E[_], G[_], F[_], A] = GElgotCoalgebraM[E, G, Id, F, A] + /** A coalgebra with two monads, one handled by the fold and the other in the + * result. `N` is the fold’s monad, whereas `M` is the Kleisli. + */ + type GCoalgebraM[G[_], M[_], F[_], A] = GElgotCoalgebraM[Id, G, M, F, A] // A => M[F[G[A]]] + type CoalgebraM[M[_], F[_], A] = GCoalgebraM[Id, M, F, A] // A => M[F[A]] + type Coalgebra[F[_], A] = GCoalgebra[Id, F, A] // A => F[A] + type ElgotCoalgebraM[E[_], M[_], F[_], A] = GElgotCoalgebraM[E, Id, M, F, A] // A => M[E[F[A]]] + /** Identical to [[matryoshka.CoalgebraM]] in structure, this signifies a + * different intent – the dual of [[matryoshka.ElgotAlgebra]]. + */ + type ElgotCoalgebra[N[_], F[_], A] = ElgotCoalgebraM[N, Id, F, A] // A => N[F[A]] - /** A NaturalTransformation that sequences two types */ + /** 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`. + * + * @group Fold */ def interpretCata[F[_]: Functor, A](t: Free[F, A])(φ: F[A] => A): A = t.fold(x => x, f => φ(f ∘ (interpretCata(_)(φ)))) /** The fold of a decomposed `coelgot`, since the Cofree already has the * attribute for each node. + * + * @group Fold */ def elgotCata[F[_]: Functor, A, B](t: Cofree[F, A])(φ: ((A, F[B])) => B): B = φ((t.head, t.tail ∘ (elgotCata(_)(φ)))) + /** A Kleisli version of [[matryoshka.elgotCata]]. + * + * @group Fold + */ def elgotCataM[F[_]: Traverse, M[_]: Monad, A, B]( t: Cofree[F, A])( φ: ((A, F[B])) => M[B]): @@ -70,70 +107,110 @@ package object matryoshka extends CofreeInstances with FreeInstances { /** A version of [[matryoshka.Corecursive.ana]] that annotates each node with * the `A` it was expanded from. This is also the unfold from a decomposed * `coelgot`. + * + * @group Unfold */ def attributeAna[F[_]: Functor, A](a: A)(ψ: A => F[A]): Cofree[F, A] = Cofree(a, ψ(a) ∘ (attributeAna(_)(ψ))) - /** A Kleisli [[matryoshka.attributeAna]]. */ - def attributeAnaM[M[_]: Monad, F[_]: Traverse, A](a: A)(ψ: A => M[F[A]]): M[Cofree[F, A]] = + /** A Kleisli [[matryoshka.attributeAna]]. + * + * @group Unfold + */ + def attributeAnaM[M[_]: Monad, F[_]: Traverse, A](a: A)(ψ: A => M[F[A]]): + M[Cofree[F, A]] = ψ(a).flatMap(_.traverse(attributeAnaM(_)(ψ))) ∘ (Cofree(a, _)) - /** The unfold from a decomposed `elgot`. */ + /** The unfold from a decomposed `elgot`. + * + * @group Unfold + */ def elgotAna[F[_]: Functor, A, B](a: A)(ψ: A => B \/ F[A]): Free[F, B] = ψ(a).fold( _.point[Free[F, ?]], fb => Free.liftF(fb ∘ (elgotAna(_)(ψ))).join) - def cofCataM[S[_]: Traverse, M[_]: Monad, A, B](t: Cofree[S, A])(f: (A, S[B]) => M[B]): M[B] = + /** The “second” fold from `elgotZygo`, with the “helper value” coming from + * `Cofree` rather than from another algebra. + * + * @group Fold + */ + def cofCataM[S[_]: Traverse, M[_]: Monad, A, B](t: Cofree[S, A])(f: (A, S[B]) => M[B]): + M[B] = t.tail.traverse(cofCataM(_)(f)) >>= (f(t.head, _)) + /** This is the “second” fold from `paraElgotZygoM`, with the comonad + * structures coming from `Cofree` rather than from another algebra. + * + * @group Fold + */ def cofParaM[M[_]] = new CofParaMPartiallyApplied[M] final class CofParaMPartiallyApplied[M[_]] { self => - def apply[T[_[_]]: Corecursive, S[_]: Traverse, A, B](t: Cofree[S, A])(f: (A, S[(T[S], B)]) => M[B])(implicit M: Monad[M]): M[B] = - t.tail.traverseU(cs => self(cs)(f) ∘ ((Recursive[Cofree[?[_], A]].convertTo[S, T](cs), _))) >>= (f(t.head, _)) + def apply[T[_[_]]: Corecursive, S[_]: Traverse, A, B](t: Cofree[S, A])(f: GElgotAlgebraM[(A, ?), (T[S], ?), M, S, B])(implicit M: Monad[M]): M[B] = + t.tail.traverseU(cs => self(cs)(f) ∘ ((Recursive[Cofree[?[_], A]].convertTo[S, T](cs), _))) >>= (x => f((t.head, x))) } - /** Composition of an anamorphism and a catamorphism that avoids building the - * intermediate recursive data structure. + /** A hylomorphism is a composition of [[matryoshka.Corecursive.ana]] and + * [[matryoshka.Recursive.cata]] that avoids building the intermediate + * recursive data structure. + * + * @group Refold */ - def hylo[F[_]: Functor, A, B](a: A)(f: F[B] => B, g: A => F[A]): B = - f(g(a) ∘ (hylo(_)(f, g))) + def hylo[F[_]: Functor, A, B](a: A)(f: Algebra[F, B], g: Coalgebra[F, A]): B = + f(g(a).join.copoint ∘ (hylo[F, A, B](_)(f, g))) - /** A Kleisli hylomorphism. */ - def hyloM[M[_]: Monad, F[_]: Traverse, A, B](a: A)(f: F[B] => M[B], g: A => M[F[A]]): + /** A Kleisli [[matryoshka.hylo]]. + * + * @group Refold + */ + def hyloM[M[_]: Monad, F[_]: Traverse, A, B](a: A)(f: AlgebraM[M, F, B], g: CoalgebraM[M, F, A]): M[B] = - g(a) >>= (_.traverse(hyloM(_)(f, g)) >>= (f)) + g(a) >>= (_.join.copoint.traverse(hyloM[M, F, A, B](_)(f, g)) >>= f) - /** A generalized version of a hylomorphism that composes any coalgebra and - * algebra. + /** A generalized version of [[matryoshka.hylo]] that composes any coalgebra + * and algebra. + * + * @group Refold */ def ghylo[F[_]: Functor, W[_]: Comonad, M[_], A, B]( a: A)( w: DistributiveLaw[F, W], m: DistributiveLaw[M, F], - f: F[W[B]] => B, - g: A => F[M[A]])( + f: GAlgebra[W, F, B], + g: GCoalgebra[M, F, A])( implicit M: Monad[M]): B = { def h(x: M[A]): W[B] = w(m(M.lift(g)(x)) ∘ (y => h(y.join).cojoin)) ∘ f h(a.point[M]).copoint } - /** Similar to a hylomorphism, this composes a futumorphism and a - * histomorphism. + /** Similar to [[matryoshka.hylo]], this composes + * [[matryoshka.Corecursive.futu]] and [[matryoshka.Recursive.histo]]. + * + * @group Refold */ def chrono[F[_]: Functor, A, B]( a: A)( - g: F[Cofree[F, B]] => B, f: A => F[Free[F, A]]): + g: GAlgebra[Cofree[F, ?], F, B], f: GCoalgebra[Free[F, ?], F, A]): B = ghylo[F, Cofree[F, ?], Free[F, ?], A, B](a)(distHisto, distFutu, g, f) - def elgot[F[_]: Functor, A, B](a: A)(φ: F[B] => B, ψ: A => B \/ F[A]): B = { + /** Similar to [[matryoshka.hylo]], `elgot` allows the unfold to short-circuit + * by returning the final value for a branch directly. + * + * @group Refold + */ + def elgot[F[_]: Functor, A, B](a: A)(φ: Algebra[F, B], ψ: ElgotCoalgebra[B \/ ?, F, A]): B = { def h: A => B = (((x: B) => x) ||| ((x: F[A]) => φ(x ∘ h))) ⋘ ψ h(a) } + /** The dual of [[matryoshka.elgot]], `coelgot` has access to the pre-unfolded + * value for each node. + * + * @group Refold + */ def coelgot[F[_]: Functor, A, B](a: A)(φ: ((A, F[B])) => B, ψ: A => F[A]): B = { def h: A => B = @@ -141,6 +218,10 @@ package object matryoshka extends CofreeInstances with FreeInstances { h(a) } + /** A Kleisli version of [[matryoshka.coelgot]]. + * + * @group Refold + */ 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]): @@ -150,23 +231,28 @@ package object matryoshka extends CofreeInstances with FreeInstances { } } + /** @group Dist */ def distPara[T[_[_]], F[_]: Functor](implicit T: Corecursive[T]): 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]) = @@ -177,12 +263,14 @@ 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]) = @@ -193,14 +281,17 @@ 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 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] = @@ -240,18 +331,11 @@ package object matryoshka extends CofreeInstances with FreeInstances { if (index < 0) None else fa.toList.drop(index).headOption - /** Turns any F-algebra, into an identical one that attributes the tree with - * the results for each node. */ - def attributeM[F[_]: Functor, M[_]: Functor, A](f: F[A] => M[A]): - F[Cofree[F, A]] => M[Cofree[F, A]] = - fa => f(fa ∘ (_.head)) ∘ (Cofree(_, fa)) - - def attribute[F[_]: Functor, A](f: F[A] => A) = attributeM[F, Id, A](f) - - def attrK[F[_]: Functor, A](k: A) = attribute[F, A](Function.const(k)) + def attrK[F[_]: Functor, A](k: A) = + attribute(new GElgotAlgebraM[Id, Id, Id, F, A](Function.const(k))) def attrSelf[T[_[_]]: Corecursive, F[_]: Functor] = - attribute[F, T[F]](_.embed) + attribute(new GElgotAlgebraM[Id, Id, Id, F, T[F]](_.embed)) /** NB: Since Cofree carries the functor, the resulting algebra is a cata, not * a para. */ @@ -259,30 +343,21 @@ package object matryoshka extends CofreeInstances with FreeInstances { F[Cofree[F, A]] => Cofree[F, A] = fa => Cofree(f(fa ∘ (x => (Recursive[Cofree[?[_], A]].convertTo[F, T](x), x.head))), fa) - def attributeCoelgotM[M[_]] = new AttributeCoelgotMPartiallyApplied[M] - final class AttributeCoelgotMPartiallyApplied[M[_]] { - def apply[F[_]: Functor, A, B](f: (A, F[B]) => M[B])(implicit M: Functor[M]): - (A, F[Cofree[F, B]]) => M[Cofree[F, B]] = - (a, node) => f(a, node ∘ (_.head)) ∘ (Cofree(_, node)) - } - - 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)))) - } - implicit def AlgebraZip[F[_]: Functor] = GAlgebraZip[Id, F] - - 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))))) - } - implicit def ElgotAlgebraZip[W[_]: Functor, F[_]: Functor] = - ElgotAlgebraMZip[W, Id, F] - // (ann, node) => node.unfzip.bimap(f(ann, _), g(ann, _)) + /** Turns any F-algebra, into an identical one that attributes the tree with + * the results for each node. */ + def attribute[E[_]: Comonad, G[_]: Comonad, M[_]: Functor, F[_]: Functor, A]( + f: GElgotAlgebraM[E, G, M, F, A]): + GElgotAlgebraM[E, G, M, F, Cofree[F, A]] = + node => f(node ∘ (_ ∘ (_ ∘ (_.head)))) ∘ (Cofree(_, node.copoint ∘ (_.copoint))) + + def generalizeM[M[_]: Applicative, A, B](f: A => B): A => M[B] = f(_).point[M] + def generalizeW[W[_]: Comonad, A, B](f: A => B): W[A] => B = w => f(w.copoint) + def generalizeAlgebra[O[_]: Functor, X[_]: Comonad, I[_], M[_], F[_], A](f: O[I[A]] => M[A]): + O[X[I[A]]] => M[A] = + node => f(node ∘ (_.copoint)) + def generalizeCoalgebra[O[_]: Functor, X[_]: Applicative, I[_], A](f: A => O[I[A]]): + A => O[X[I[A]]] = + f(_).map(_.point[X]) /** 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). @@ -290,20 +365,24 @@ package object matryoshka extends CofreeInstances with FreeInstances { 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. - */ - 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. */ def orDefault[A, B](default: B)(f: A => Option[B]): A => B = - expr => f(expr).getOrElse(default) + f(_).getOrElse(default) + + /** Converts a failable fold into a non-failable, by simply returning the + * argument upon failure. + */ + def orOriginal[A](f: A => Option[A]): A => A = + expr => orDefault(expr)(f)(expr) /** Count the instinces of `form` in the structure. + * + * @group Algebra */ - def count[T[_[_]]: Recursive, F[_]: Functor: Foldable](form: T[F]): F[(T[F], Int)] => Int = + 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 + _) implicit def ToIdOps[A](a: A): IdOps[A] = new IdOps[A](a) @@ -312,15 +391,25 @@ package object matryoshka extends CofreeInstances with FreeInstances { CorecursiveOps[T, F] = new CorecursiveOps[T, F](f) - implicit def ToAlgebraOps[F[_], A](a: Algebra[F, A]): AlgebraOps[F, A] = - new AlgebraOps[F, A](a) + implicit final class CofreeOps[F[_], A](val self: Cofree[F, A]) + extends scala.AnyVal { + def elgotCata[B](φ: ((A, F[B])) => B)(implicit F: Functor[F]): B = + matryoshka.elgotCata(self)(φ) - implicit def ToCoalgebraOps[F[_], A](a: Coalgebra[F, A]): CoalgebraOps[F, A] = - new CoalgebraOps[F, A](a) + def elgotCataM[M[_]: Monad, B]( + φ: ((A, F[B])) => M[B])( + implicit F: Traverse[F]): + M[B] = + matryoshka.elgotCataM(self)(φ) - implicit def ToCofreeOps[F[_], A](a: Cofree[F, A]): CofreeOps[F, A] = - new CofreeOps[F, A](a) + def cofCataM[M[_]: Monad, B](f: (A, F[B]) => M[B])(implicit F: Traverse[F]): + M[B] = + matryoshka.cofCataM(self)(f) + } - implicit def ToFreeOps[F[_], A](a: Free[F, A]): FreeOps[F, A] = - new FreeOps[F, A](a) + implicit final class FreeOps[F[_], A](val self: Free[F, A]) + extends scala.AnyVal { + def interpretCata(φ: F[A] => A)(implicit F: Functor[F]): A = + matryoshka.interpretCata(self)(φ) + } } diff --git a/core/src/test/scala/matryoshka/spec.scala b/core/src/test/scala/matryoshka/spec.scala index 6406caec..83bc07da 100644 --- a/core/src/test/scala/matryoshka/spec.scala +++ b/core/src/test/scala/matryoshka/spec.scala @@ -97,11 +97,6 @@ object Exp { } implicit def ExpEqual2[A](implicit A: Equal[A]): Equal[Exp[A]] = ExpEqual(A) - // NB: Something like this currently needs to be defined for any Functor in - // order to get the generalize operations for the algebra. - implicit def ToExpAlgebraOps[A](a: Algebra[Exp, A]): AlgebraOps[Exp, A] = - ToAlgebraOps[Exp, A](a) - implicit val ExpShow: Show ~> λ[α => Show[Exp[α]]] = new (Show ~> λ[α => Show[Exp[α]]]) { def apply[α](show: Show[α]) = @@ -423,11 +418,24 @@ class FixplateSpecs extends Specification with ScalaCheck with ScalazMatchers { } } + "attribute" should { + "work on simple algebra" in { + val v = mul(num(1), mul(num(2), num(3))) + val rez = Cofree[Exp, Int](6, Mul( + Cofree(1, Num(1)), + Cofree (6, Mul( + Cofree(2, Num(2)), + Cofree(3, Num(3)))))) + v.cata(eval.attribute) must equal(rez) + v.convertTo[Mu].cata(eval.attribute) must equal(rez) + } + } + "zipAlgebras" should { "both eval and find all constants" in { - mul(num(5), num(2)).cata(AlgebraZip[Exp].zip(eval, findConstants)) must + mul(num(5), num(2)).cata(eval.zip(findConstants)) must equal((10, List(5, 2))) - mul(num(5), num(2)).convertTo[Mu].cata(AlgebraZip[Exp].zip(eval, findConstants)) must + mul(num(5), num(2)).convertTo[Mu].cata(eval.zip(findConstants)) must equal((10, List(5, 2))) } } @@ -466,9 +474,11 @@ class FixplateSpecs extends Specification with ScalaCheck with ScalazMatchers { } } - def extractFactors: Coalgebra[Exp, Int] = x => + // TODO: Why do we need to explicitly call the implicit here? Is it because + // `F` is in covariant position? + def extractFactors: Coalgebra[Exp, Int] = toCoalgebra(x => if (x > 2 && x % 2 == 0) Mul(2, x/2) - else Num(x) + else Num(x)) "generalizeCoalgebra" should { "behave like ana" ! prop { (i: Int) => @@ -734,8 +744,8 @@ class FixplateSpecs extends Specification with ScalaCheck with ScalazMatchers { } // NB: This is better done with cata, but we fake it here - def partialEval[T[_[_]]: Corecursive: Recursive](t: Exp[Cofree[Exp, T[Exp]]]): - T[Exp] = + def partialEval[T[_[_]]: Corecursive: Recursive]: + Exp[Cofree[Exp, T[Exp]]] => T[Exp] = t => t match { case Mul(x, y) => (x.head.project, y.head.project) match { case (Num(a), Num(b)) => Num[T[Exp]](a * b).embed @@ -758,7 +768,8 @@ class FixplateSpecs extends Specification with ScalaCheck with ScalazMatchers { } } - def extract2and3(x: Int): Exp[Free[Exp, Int]] = + // FIXME: defining this as an algebra brings up the “cyclic aliasisng” issue + def extract2and3: Int => Exp[Free[Exp, Int]] = x => // factors all the way down if (x > 2 && x % 2 == 0) Mul(Free.point(2), Free.point(x/2)) // factors once and then stops