Recursion Schemes Workshop
Jean-Remi Desjardins - LambdaConf 2017
How do you deal with recursive data?
- while loops 💩
- ad hoc recursion 🙂
-
recursion schemes 💪
Benefits of Recursion Schemes
- define recursive algorithms only once
- decouple how a function recurses over data from what the function actually does
- avoid general recursion
- concise code
Meijer et. al go so far as to condemn functional programming without recursion schemes as morally equivalent to imperative programming with goto. While comparisons to Djikstra’s infamous letter to the ACM are often inane, the analogy is apt: just as using while and for loops rather than goto brings structure and harmony to imperative control flow, the use of recursion schemes over hand-written brings similar structure to recursive computations. This insight is so important that I’ll repeat it: recursion schemes are just as essential to idiomatic functional programming as for and while are to idiomatic imperative programming.
sumtypeofway.com
Prerequisite
trait Expr
case class NumLit(value: Int) extends Expr
case class Add(left: Expr, right: Expr) extends Expr
case class Div(num: Expr, denum: Expr) extends Expr
trait Expr[A]
case class NumLit[A](value: Int) extends Expr[A]
case class Add[A](left: A, right: A) extends Expr[A]
case class Div[A](num: A, denum: A) extends Expr[A]
data Expr
= NumLit Int
| Add Expr Expr
| Div Expr Expr
data Expr a
= NumLit Int
| Add a a
| Div a a
Functorize your data types
Exercise #1
trait Expr
case class NumLit(value: Int) extends Expr
case class Add(left: Expr, right: Expr) extends Expr
case class Div(num: Expr, denum: Expr) extends Expr
Convert to Fix Point Style
Exercise #1
Convert to Fix Point Style
Solution:
Exercise #2
trait Expr
case class NumLit(value: Int) extends Expr
case class Add(left: Expr, right: Expr) extends Expr
case class Div(num: Expr, denum: Expr) extends Expr
val expr = Add(NumLit(5), NumLit(10))
// Increments all numeric literals in provided expression
def inc(expr: Expr): Expr =
expr match {
case Let(id, expr, in) => Let(id, inc(expr), inc(in))
case Add(left, right) => Add(inc(left), inc(right))
case NumLit(value) => NumLit(value + 1)
case other => other
}
inc(expr) // Add(NumLit(6), NumLit(11))
Create a value and apply a simple transformation
Fix
trait Expr[A]
case class NumLit[A](value: Int) extends Expr[A]
case class Add[A](left: A, right: A) extends Expr[A]
case class Div[A](num: A, denum: A) extends Expr[A]
val expr: Add[NumLit[Nothing]] = Add(NumLit(5), NumLit(10))
//val expr: Expr[Expr[Expr[...]]]
val exprFix: Fix[Expr] = Fix(Add(Fix(NumLit(5)), Fix(NumLit(10))))
Catamorphism
trait Fix[F[_]] {
def cata[A](f: F[A] => A)(implicit func: Functor[F]): A
}
trait Expr[A]
case class NumLit[A](value: Int) extends Expr[A]
case class Add[A](left: A, right: A) extends Expr[A]
case class Div[A](num: A, denum: A) extends Expr[A]
val expr: Fix[Expr] = Fix(Add(Fix(NumLit(5)), Fix(NumLit(10))))
// Returns the "complexity" of the expression provided
def complexity(expr: Fix[Expr]): Int =
expr.cata {
case NumLit(value) => 1
case Add(left, right) => 1 + Math.max(left, right)
case Div(num, denum) => 1 + Math.max(num, denum)
}
Functor
object Expr {
implicit val functor: Functor[Expr] = ???
}
Exercise #2
Create a value
Solution:
Exercise #3
trait Expr[A]
case class NumLit[A](value: Int) extends Expr[A]
case class Add[A](left: A, right: A) extends Expr[A]
case class Div[A](num: A, denum: A) extends Expr[A]
val expr: Fix[Expr] = Fix(Add(Fix(NumLit(5)), Fix(NumLit(10))))
case class DivisionByZero(div: Div[Int])
def eval(expr: Fix[Expr]): DivisionByZero \/ Int = ???
cataM
Exercise #3
cataM
Solution:
Exercise #4
trait Expr[A]
case class NumLit[A](value: Int) extends Expr[A]
case class Add[A](left: A, right: A) extends Expr[A]
case class Div[A](num: A, denum: A) extends Expr[A]
val expr: Fix[Expr] = Fix(Add(Fix(NumLit(5)), Fix(NumLit(10))))
def collect(a: Fix[Expr]): List[NumLit[_]] = ???
collect(expr)
Collect nodes of a certain type
Exercise #4
Collect nodes of a certain type
Solution:
Exercise #5
trait Expr[A]
case class NumLit[A](value: Int) extends Expr[A]
case class Add[A](left: A, right: A) extends Expr[A]
case class Div[A](num: A, denum: A) extends Expr[A]
val expr: Fix[Expr] = Fix(Add(Fix(NumLit(5)), Fix(NumLit(10))))
def gen(complexity: Int): Fix[Expr]
Unfold
Quiz #1
trait Tree {
def children: List[Tree]
def transform(f: Tree => Tree): Tree
}
trait Expr extends Tree
case class Let(id: String, expr: Expr) extends Expr {
def children = List(expr)
}
case class NumLit(value: Int) extends Expr {
def children = Nil
}
case class Add(left: Expr, right: Expr) extends Expr {
def children = List(left, right)
}
What's the difference over this?
Exercise #6
trait Expr[A]
case class NumLit[A](value: Int) extends Expr[A]
case class Add[A](left: A, right: A) extends Expr[A]
case class Div[A](num: A, denum: A) extends Expr[A]
case class Id[A](value: String) extends Expr[A]
case class Let[A](id: Id[Nothing], as: A, in: A) extends Expr[A]
def eval(expr: Fix[Expr]): DivisionByZero \/ Int = ???
Beyond cata (scope)
Exercise #7
case class Fix[F[_]](unfix: ???) {
def cata[A](f: F[A] => A): A = ???
}
Roll your own
Exercise #8
beyond cata #2 (different error message)
trait Expr[A]
case class NumLit[A](value: Int) extends Expr[A]
case class Add[A](left: A, right: A) extends Expr[A]
case class Div[A](num: A, denum: A) extends Expr[A]
val expr: Fix[Expr] = add(numLit(5), div(numLit(4), numLit(2)))
case class DivisionByZero(div: Div[Fix[Expr]])
def eval(expr: Fix[Expr]): DivisionByZero \/ Int = ???
eval(expr)
Exercise #9
Cofree
Recursion Schemes
By Jean-Rémi Desjardins
Recursion Schemes
- 1,282