A Gentle Introduction to Recursion Schemes

Jean-Remi Desjardins - LambdaConf 2016

Outline

  • Motivate it
  • How it works
  • Examples
  • How to use it

Recursion Schemes

Outline

  • Motivate it
  • How it works
  • Examples
  • How to use it
  • Filesystems
  • Compilers
  • JSON
  • etc

Recursive Data Types

  • while loops 💩
  • ad hoc recursion 🙂
  • recursion schemes 💪

How do we reason, transform, process recursive data?

define recursive algorithms once and for all

Recursion Schemes

define your recursive data types in fixed point style

case class CompositeTree[A](leftChild: A, rightChild: A)

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
  • more concise and DRY code

Drawback:

  • Arguably uglier data types

Example

Modeling a tournament

Initial Model

final class Participant(value: String) extends AnyVal

sealed trait Draw
case class FutureMatch(left: Draw, right: Draw) extends Draw
case class Match(left: Participant, right: Participant) extends Draw

Match

FutureMatch

Initial Model

final class Participant(value: String) extends AnyVal

sealed trait Draw
case class FutureMatch(left: Draw, right: Draw) extends Draw
case class Match(left: Participant, right: Participant) extends Draw
def size(d: Draw): Int = {
    d match {
        case Match(_,_) => 2
        case FutureMatch(left, right) => size(left) + size(right)
    }
}

Fix Point Style

final class Participant(value: String) extends AnyVal

type Draw = Fix[DrawF]

sealed trait DrawF[A]
case class FutureMatchF[A](left: A, right: A) extends DrawF[A]
case class MatchF[A](left: Participant, right: Participant) extends DrawF[A]

Match

FutureMatch

Fix Point Style

def size(d: Draw): Int = {
    d.cata {
        case MatchF(_,_) => 2
        case FutureMatchF(left, right) => left + right
    }
}
final class Participant(value: String) extends AnyVal

type Draw = Fix[DrawF]

sealed trait DrawF[A]
case class FutureMatchF[A](left: A, right: A) extends DrawF[A]
case class MatchF[A](left: Participant, right: Participant) extends DrawF[A]

Initial model

sealed trait Draw
case class FutureMatch(left: Draw, right: Draw) extends Draw
case class Match(left: Participant, right: Participant) extends Draw
// The participant in the draw with the best chance of winning the tournament
def bestChanceOfWinning(d: Draw): Task[Participant] = {
  def bestChanceOfWinningR(d: Draw): Task[(Participant,Float)] =
    d match {
      case Match(left, right) => chanceOfWining(left, right).map(chance =>
        if (chance > 0.5) (left, chance) else (right, (1 - chance))
      )
      case FutureMatch(left, right) =>
        for {
          left  <- bestChanceOfWinningR(left)
          (leftPart, leftChance) = left
          right <- bestChanceOfWinningR(right)
          (rightPart, rightChance) = right
          chance <- chanceOfWining(leftPart, rightPart)
        } yield {
          val leftNewChance = leftChance * chance
          val rightNewChance = rightChance * (1 - chance)
          if (leftNewChance > rightNewChance) (leftPart, leftNewChance)
          else (rightPart, rightNewChance)
        }
    }
  bestChanceOfWinningR(d)._1
}
def probabilityOfWin(candidate: Participant, over: Participant): Task[Float]

Fixed Point Style

// The participant in the draw with the best chance of winning the tournament
def bestChanceOfWinning(d: Draw) =
  d.cataM{
    case MatchF(left, right) =>
      chanceOfWining(left, right).map( chance =>
        if (chance > 0.5) (left, chance) else (right, (1 - chance))
      )
    case FutureMatchF((leftPart, leftChance), (rightPart, rightChance)) =>
      chanceOfWining(leftPart, rightPart).map { chance =>
        val leftNewChance = leftChance * chance
        val rightNewChance = rightChance * (1 - chance)
        if (leftNewChance > rightNewChance) (leftPart, leftNewChance)
        else (rightPart, rightNewChance)
      }
  }._1
def probabilityOfWin(candidate: Participant, over: Participant): Task[Float]
type Draw = Fix[DrawF]

sealed trait DrawF[A]
case class FutureMatchF[A](left: A, right: A) extends DrawF[A]
case class MatchF[A](left: Participant, right: Participant) extends DrawF[A]

Comparison

def bestChanceOfWinning(d: Draw) =
  d.cataM{
    case FutureMatchF((leftPart, leftChance), (rightPart, rightChance)) =>
      chanceOfWining(leftPart, rightPart).map { chance =>
        val leftNewChance = leftChance * chance
        val rightNewChance = rightChance * (1 - chance)
        if (leftNewChance > rightNewChance) (leftPart, leftNewChance)
        else (rightPart, rightNewChance)
      }
  }._1
def bestChanceOfWinning(d: Draw): Task[Participant] = {
  def bestChanceOfWinningR(d: Draw): Task[(Participant,Float)] =
    d match {
      case FutureMatch(left, right) =>
        for {
          left  <- bestChanceOfWinningR(left)
          (leftPart, leftChance) = left
          right <- bestChanceOfWinningR(right)
          (rightPart, rightChance) = right
          chance <- chanceOfWining(leftPart, rightPart)
        } yield {
          val leftNewChance = leftChance * chance
          val rightNewChance = rightChance * (1 - chance)
          if (leftNewChance > rightNewChance) (leftPart, leftNewChance)
          else (rightPart, rightNewChance)
        }
    }
  bestChanceOfWinningR(d)._1
}

One More Example -Unfolds

Initial Model - unfold

sealed trait Draw
case class FutureMatch(left: Draw, right: Draw) extends Draw
case class Match(left: Participant, right: Participant) extends Draw
  a match {
    case xs if xs.size < 2 => None
    case x1 :: x2 :: Nil => Some(Match(x1, x2))
    case xs =>
      val (left,right) = xs.split(xs.size / 2)
      (fromList(left) ⊛ fromList(right))(FutureMatch)
  }
def fromList(a: List[Participant]): Option[Draw] =

Fixed point style - unfold

  Fix[DrawF].anaM(a){
    case xs if xs.size < 2 => None
    case x1 :: x2 :: Nil => Some(MatchF(x1,x2))
    case xs =>
      val (left,right) = xs.split(xs.size / 2)
      Some(FutureMatchF(left,right))
  }
def fromList(a: List[Participant]): Option[Draw] =
type Draw = Fix[DrawF]

sealed trait DrawF[A]
case class FutureMatchF[A](left: A, right: A) extends DrawF[A]
case class MatchF[A](left: Participant, right: Participant) extends DrawF[A]

unfold comparison

def fromList(a: List[Participant]): Option[Draw] =  
  Fix[DrawF].anaM(a){
    case xs if xs.size < 2 => None
    case x1 :: x2 :: Nil => Some(MatchF(x1,x2))
    case xs =>
      val (left,right) = xs.split(xs.size / 2)
      Some(FutureMatchF(left,right))
  }
def fromList(a: List[Participant]): Option[Draw] =    
  a match {
    case xs if xs.size < 2 => None
    case x1 :: x2 :: Nil => Some(Match(x1, x2))
    case xs =>
      val (left,right) = xs.split(xs.size / 2)
      (fromList(left) ⊛ fromList(right))(FutureMatch)
  }

Intuition

sealed trait DrawF[A]
case class FutureMatchF[A](left: A, right: A) extends DrawF[A]
case class MatchF[A](left: Participant, right: Participant) extends DrawF[A]
FutureMatch[Int] = FutureMatch(3,4)
FutureMatch[String] = FutureMatch("Bob", "Dylan")
val draw: FutureMatch[Draw[Draw[Draw[...]]]] // How to model a recursive draw?

Outline

  • Motivate it
  • How it works
  • Examples
  • How to use it

How it works

val draw: FutureMatch[Draw[Draw[Draw[...]]]] // How to model a recursive draw?
final case class Fix[F[_]](unFix: F[Fix[F]]) {
  def cata...
  def cataM...
  def ana...
  def anaM...
  ...
}
type Draw = Fix[DrawF]

Theory:

  • Fix is the Y combinator
  • Fix[Draw] is the fixed point of the Draw functor

How it works

val draw: Fix[DrawF]
val futureMatch: FutureMatchF[Fix[DrawF]] = draw.unfix
val leftSide: Fix[DrawF] = futureMatch.left
val result: FutureMatchF[Fix[DrawF]] = leftSide.unfix
val result1: Fix[DrawF] = result.left
val result2: MatchF[Fix[Draw]] = result1.unfix
val leftParticipant: Participant = result2.left

An instance of Functor is required

def cata[F[_]: Functor, A](t: T[F])(f: F[A] => A): A =
    f(project(t) ∘ (cata(_)(f)))

Outline

  • Motivate it
  • How it works
  • Examples
  • How to use it

AST Example (stolen from SumTypeOfWay)

data Expr  
  = Index Expr Expr
  | Call Expr [Expr]
  | Unary String Expr
  | Binary Expr String Expr
  | Paren Expr
  | Literal Lit
  deriving (Show, Eq)
-- this would turn the expression  
--    (((anArray[(10)])))
-- into
--    anArray[10]

flatten :: Expr -> Expr  
flatten (Literal i) = Literal i
flatten (Paren e) = flatten e
flatten (Index e i)     = Index (flatten e) (flatten i)  
flatten (Call e args)   = Call (flatten e) (map flatten args)  
flatten (Unary op arg)  = Unary op (flatten arg)  
flatten (Binary l op r) = Binary (flatten l) op (flatten r)

Fixed point stye

data Expr a  
  = Index a a
  | Call [a]
  | Unary String a
  | Binary a String a
  | Paren a
  | Literal Lit
  deriving (Show, Eq, Functor)
-- this would turn the expression  
--    (((anArray[(10)])))
-- into
--    anArray[10]

flatten :: Term Expr -> Term Expr  
flatten (In (Paren e)) = e  -- remove all Parens  
flatten other = other       -- do nothing otherwise

Example in Quasar

// final case class InvokeF[A](func: Func, values: List[A]) extends LogicalPlan[A]

def simpleEvaluation(lp: Fix[LogicalPlan]): FileSystemErrT[InMemoryFs, Vector[Data]] = {
  val optLp = Optimizer.optimize(lp)
  EitherT[InMemoryFs, FileSystemError, Vector[Data]](State.gets { mem =>
    import quasar.LogicalPlan._
    import quasar.std.StdLib.set.{Drop, Take}
    import quasar.std.StdLib.identity.Squash
    optLp.para[FileSystemError \/ Vector[Data]] {
      case ReadF(path) =>
        // Documentation on `QueryFile` guarantees absolute paths, so calling `mkAbsolute`
        val aPath = mkAbsolute(rootDir, path)
        fileL(aPath).get(mem).toRightDisjunction(pathErr(pathNotFound(aPath)))
      case InvokeF(Drop, (_,src) :: (Fix(ConstantF(Data.Int(skip))),_) :: Nil) =>
        src.flatMap(s => skip.safeToInt.map(s.drop).toRightDisjunction(unsupported(optLp)))
      case InvokeF(Take, (_,src) :: (Fix(ConstantF(Data.Int(limit))),_) :: Nil) =>
        src.flatMap(s => limit.safeToInt.map(s.take).toRightDisjunction(unsupported(optLp)))
      case InvokeF(Squash,(_,src) :: Nil) => src
      case ConstantF(data) => Vector(data).right
      case other =>
            queryResponsesL
              .get(mem)
              .mapKeys(Optimizer.optimize)
              .get(Fix(other.map(_._1)))
              .toRightDisjunction(unsupported(optLp))
    }
  })
}

Example in Quasar

def para[F[_]: Functor, A](t: Fix[F])(f: F[(Fix[F], A)] => A): A

Outline

  • Motivate it
  • How it works
  • Examples
  • How to use it

All the Recursions

Matryoshka

libraryDependencies += "com.slamdata" %% "matryoshka-core" % "0.11.1"

Recursion Schemes (Haskell)

Acknowledgment

A Gentle Introduction to Recursion Schemes

By Jean-Rémi Desjardins

A Gentle Introduction to Recursion Schemes

Recursion Schemes, popularized by the notorious paper Functional Programming with Bananas, Lenses, Envelopes and Barbed Wire, is an very elegant technique for expressing recursive algorithms on arbitrarily nested data structures. This talk will offer a gentle introduction to the subject. We will begin by motivating its use and then follow through with some intuition for the concepts and finally walktrough some concrete examples. Only basic familiarity with functional programming abstractions will be assumed. The audience can expect to leave with a burning desire to write all their recursive data types in fixed point style.

  • 1,347