↓ Twitter slide number ↓
So, what are these wooden nesting dolls, and how do they relate to programming?- https://github.com/slamdata/matryoshka
- https://github.com/sellout/recursion-scheme-talk
- separation of concerns
- meant to invoke “finite nesting” (induction)
- also handles infinite recursion (coinduction)
- Ed Kmett – https://github.com/ekmett/recursion-schemes
- Patrick Bahr – https://github.com/pa-ba/compdata
- Patrick Thomson – http://blog.sumtypeofway.com
- the papers
- Functional Programming with Bananas, Lenses, Envelopes and Barbed Wire
- Unifying Structured Recursion Schemes
- Typelevel (http://typelevel.org)
sealed abstract class Expr
final case class Num(v: Int) extends Expr
final case class Mul(a: Expr, b: Expr) extends Expr
val eval: Expr => Int = {
case Num(v) => v
case Mul(a, b) => eval(a) * eval(b)
}
https://www.youtube.com/watch?v=BHjIl81HgfE
sealed abstract class Expr[A]
final case class Num[A](v: Int) extends Expr[A]
final case class Mul[A](a: A, b: A) extends Expr[A]
def eval: Expr[Int] => Int = {
case Num(v) => v
case Mul(a, b) => a * b
}
Expr[A]
, and to make it recursive like it was before, we need to turn that A
into a recursive structure again.
sealed abstract class Expr[A]
type ExprR0 = Expr[Expr] // 🚫
type ExprR1 = Expr[Expr[Expr[Expr[Expr[Unit]]]]] // ❓
sealed abstract class Expr[A]
final case class Fix[F[_]](unFix: F[Fix[F]])
type ExprR = Fix[Expr]
Fix
, there are a couple other fixed-point operators: Mu
and Nu
. They externally look exactly the same, but have different implementations. Basically, Mu
is for finite structures and Nu
is for potentially-infinite structures (like streams).
Fix[F]
: simple to implement, not (yet) stack-stafeMu[F]
: inductive (finite) structuresNu[F]
: coinductive (maybe-infinite) structures
@typeclass trait Recursive[T] {
type Base[A]
def project(t: T)(implicit BF: Functor[Base])
: Base[T]
}
implicit def fixRec[F[_]]: Recursive.Aux[Fix[F], F] =
new Recursive[Fix[F]] {
type Base[A] = F[A]
def project(t: Fix[F])(implicit BF: Functor[F])
: F[Fix[F]] =
t.unFix
}
And then we notice that this extends to other recursive structures as well, like Free
and Cofree
.
sealed abstract class ListF[A, B]
final case class NilF[A, B]()
extends ListF[A, B]
final case class ConsF[A, B](head: A, tail: B)
extends ListF[A, B]
type List[A] = Mu[ListF[A, ?]]
type Colist[A] = Nu[ListF[A, ?]] // a.k.a. “Stream”
implicit def listRec[A]: Recursive[List[A]] =
new Recursive[List[A]] {
type Base[B] = ListF[A, B]
def project(t: T)(implicit BF: Functor[Base])
: ListF[A, List[A]] =
t match {
case Nil => NilF()
case h :: t => ConsF(h, t)
}
}
T | Base[_] |
---|---|
Fix[F] | F |
Mu[F] | F |
Nu[F] | F |
List[A] | ListF[A, ?] |
Colist[A] | ListF[A, ?] |
Stream[A] | (A, ?) |
Cofree[F, A] | EnvT[A, F, ?] |
Free[F, A] | CoEnv[A, F, ?] |
Nat | Option |
A | Const[A, ?] |
- arbitrary AST isn’t
Foldable
, but it isRecursive
– so now you have generalized folds over any AST
@typeclass trait Corecursive[T] {
type Base[A]
def embed(ft: Base[T])(implicit BF: Functor[Base])
: T
}
implicit def fixCo[F[_]]: Corecursive[Fix[F]] =
new Corecursive[Fix[F]] {
type Base[A] = F[A]
def embed(ft: F[Fix[F]])(implicit BF: Functor[F])
: Fix[F] =
Fix(ft)
}
cata
.
def cata(t: T)(φ: Base[A] => A): A =
φ(t.project ∘ (cata(_)(φ)))
def eval: Expr[Int] => Int = {
case Num(v) => v
case Mul(a, b) => a * b
}
cata(someExpr)(eval)
def ana(a: A)(ψ: A => Base[A]): T = // dual
(ψ(a) ∘ (ana(_)(ψ))).embed
val eval: Expr[Int] ⇒ Int = {
case Num(v) ⇒ v
case Mul(a, b) ⇒ a * b
}
val expr =
Mul(
Num(2),
Mul(Num(2), Mul(Num(2), Mul(Num(2), Num(3)))))
expr.cata(eval) // 48
48.ana(factor)
48
|
Mul(2, 24)
/ \
Num(2) Mul(2, 12)
/ \
Num(2) Mul(2, 6)
/ \
Num(2) Mul(2, 3)
/ \
Num(2) Num(3)
def foldRight[A, B]
(list: List[A], z: B)
(f: (A, B) => B)
: B =
list.cata {
case NilF() => z
case ConsF(elem, acc) => f(elem, acc)
}
Foldable
, you have to throw away all of the non-list-like information. This actually abstracts from having Nil/Cons cases to having cases for any set of nodes in your data type.
You might be looking at this and saying something like “sure, it works for simple cases, but I sometimes want to have a recursive function that depends on the partial result of another recursive function” or, “I need to look at the original structure as I go – it’s not enough to simply have the results thus far”
Well, have I got news for you!
These recursion schemes generalize in a number of ways. The “simplest” way is that they can all be Kleislied – Rob will touch on a bit of that tomorrow. But, very quickly,
(F[A] => M[A]) => Fix[F] => M[A]
(A => M[F[A]]) => A => M[Fix[F]]
F[(Fix[F], A)] => A A => F[Fix[F] \/ A]
F[W[A]] => A A => F[M[A]]
You can also turn that disjunction into an arbitrary monad – each one has its own behaviors. A few of the most useful comonads and monads have been given particular names.
and there’s another family, like
W[F[A]] => A A => M[F[A]]
A => M[Fix[F]]
, whereas an elgot unfold gives you A => Fix[F]
.
Scott Maher (1:40) and Rob Norris (2:10) and both have talks scheduled tomorrow that go into some other neat things you can do with these, so I won’t try to cover all of it.
“Meijer et. al go so far as to condemn functional programming without recursion schemes as morally equivalent to imperative programming with goto.” —Patrick Thomson in An Introduction to Recursion Schemes
- annotate arbitrary structures (Rob Norris)
def eval: Expr[Int] => Int
def pprint: Expr[String] => String
pprint zip eval: Expr[(String, Int)] => (String, Int)
a.ana(ψ).cata(φ)
A ↘ ↗ Fix[F] ↘ ↗ B
↘ ↗ ↘ ↗
↘ ↗ ↘ ↗
ψ ↘ ↗ Fix() unFix ↘ ↗ φ
↘_↗ ↘_↗
a.hylo(φ, ψ)
A ↘ ↗ B
↘ ↗
↘ hylo ↗
ψ ↘ ↗ φ
↘_↗
We do this by … adding another type parameter!
Previously, we had a proper type that we could extract a functor from. Now we’re going to have a functor thet we can extract a … higher-order functor from.
In our case, we had a functor, where we could have the fixed-point of the same functor as the A
parameter. But now we need to carry some extra information around – the “sort” of data that’s allowed at any point. Here’s an example from comp-data:
(maybe merge this into one datatype, depending on whether we cover Coproduct before this)
sealed abstract class Sig[A[_], I]
case class Pair[A[_], I, J](fst: A[I], snd: A[J])
extends Sig[A, (I, J)]
case class Const[A[_]](v: Int) extends Sig[A, Int]
case class Add[A[_]](a: A[Int], b: A[Int])
extends Sig[A, Int]
case class Fst[A[_], I, J](p: A[(I, J)])
extends Sig[A, I]
case class Snd[A[_], I, J](p: A[(I, J)])
extends Sig[A, J]
A
is now a functor that is parameterized by the type of the value that will be evaluated to. But this technique is more general than that. You could easily usee final case object `Integer
instead of the Scala ~Int type. In this case, that might make it harder to interpret the program completely, but the index or “sort” doesn’t necessaryly map to the type of the expression. At SlamData, we use sorts to differentiate between literal values, mapping operations, and dimuntsional transforms.
Rather than thinking of it as a higher-order fixed-point + a higher-order functor, think of it as a functor from which we can extract a higher-order functor:
@typeclass trait HRecursive[T[_]] {
type Base[G[_], A]
def hproject(implicit BF: HFunctor[Base])
: T ~> Base[T, ?]
}
“It’s just a monoid functor in the category of endofunctors.”
First, you need to have both the directly-recusive and functorized structures. Similiar to the `ListF` case. Now, you can implement Recursive
and Corecursive
instances on your directly recursive type, with the functorized version as Base
. Ok, you’re done – start writing algebras using the functor, and all of the folds exist on your directly-recursive type.
sealed abstract class ExprF[A]
final case class NumF[A](v: Int) extends ExprF[A]
final case class MulF[A](a: A, b: A) extends ExprF[A]
implicit def exprRec: Recursive.Aux[Expr, ExprF] =
new Recursive[Expr] {
type Base[A] = ExprF[A]
def project(t: Expr)(implicit BF: Functor[ExprF])
: ExprF[Expr] =
t match {
case Num(v) => NumF(v)
case Mul(a, b) => MulF(a, b)
}
}
// F ⊣ G
abstract class Adjunction[F[_], G[_]] {
...
def monad(implicit G: Functor[G]): Monad[(G ∘ F)#λ]
def comonad(implicit F: Functor[F])
: Comonad[(F ∘ G)#λ]
...
}
Also, this gives us folds and unfolds that aren’t possible with the direct comonadic that Matryoshka currently uses. E.g., the “mutumorphism”, which is a generalization of the zygomorphism that allows both algebras to refer to the results of the other – giving us mutual recursion in recursion schemes. We do have mutu
, but it’s implemented outside of the nice generalized model we have.
SlamData has an ABT library, but it requires writing things in a style that is distinct from both direct recursion and recursion schemes. I think we could approach it in a way that takes advantage of recursion schemes.
sealed abstract class ABT[F[_], A]
final case class Var[F[_], A](v: String)
extends ABT[F, A]
final case class Abs[F[_], A](v: String, term: A)
extends ABT[F, A]
final case class Term[F[_], A](tm: F[A])
extends ABT[F, A]
- I’m happy to talk to anyone about any of this stuff. There is a lot here that I glossed over or didn’t even mention.
Again, not my ideas – look to Phil Wadler, Ed Kmett, Erik Meijer, etc. But it’s been fun to explore them in Scala.
GitHub (and Gitter) – slamdata/matryoshka
Greg Pfeil – greg@slamdata.com