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,156