ラムダ計算を実装してみる

まず、OCaml版のラムダ計算があればうれしいので、ラムダ計算入門 *1 を参考に、TAPLのソースにあわせる形で、Scalaに書き換えてみました。

package _01lambda

trait Term // λ式を表すデータ型
case class TmVar(s:String) extends Term // 変数 // 変数名は文字列であらわすことにする
case class TmAbs(s:String,e:Term) extends Term // λ抽象
case class TmApp(e1:Term, e2:Term) extends Term // 関数適用

class NoRuleApplies(t: Term)
  extends Exception("No rule applies for term: " + t)

object Demo {
  var counter = 0 // 整数0を持った参照セルcounterを作る
  // 新しい変数名を作る補助的な関数
  def gensym():String = {
    counter += 1 // counter の中身を一つ増やす
    "g" + counter // g1, g2, g3, ... 等の文字列を返す
  }
  // ((lambda x e1) e2)を評価するために、e1のxをe2に書き換える。
  def subst(e2:Term, x:String, e1:Term):Term = {
    e1 match {
      // e1が 変数のとき
      case TmVar(y) =>
        // 変数名が同じなら置き換え、同じでなければそのまま
        if (x == y) e2 else TmVar(y)
      // ラムダ抽象のとき ((lambda x (lambda y e)) e2) => eのyをy1に置き換えて、xをe2に置き換え、たものをe'として
      // 結果、(lambda y1 e')
      case TmAbs(y, e) =>
        // 新しい識別子を作って
        val y1 = gensym()
        // その値で、y TmVar(y1)
        TmAbs(y1, subst(e2, x, subst(TmVar(y1), y, e)))
      // 関数適用のときは、内部を置き換える。
      case TmApp(e, e1) =>
        TmApp(subst(e2, x, e), subst(e2, x, e1))
    }
  }
  // e を受け取り、e => e' となる e' のリストを返す 
  def eval1(t:Term):Term = {
    t match {
      case TmApp(TmAbs(x, e0), e2) if (isVal(e2)) => subst(e2, x, e0)
      case TmApp(e1, e2) if (isVal(e1)) => TmApp(eval1(e1), e2)
      case TmApp(e1, e2) => TmApp(e1, eval1(e2))
      case _ => throw new NoRuleApplies(t)
    }
  }
  // 簡約できなくなるまで eval1 を反復する
  def eval(t:Term):Term = {
    try {
      val t1 = eval1(t)
      eval(t1)
    } catch {
      case _: NoRuleApplies => t
    }
  }
  // 
  def isVal(t: Term): Boolean = {
    t match {
      case TmAbs(_, _) => true
      case TmVar(_)    => true
      case _           => false
    }
  }
  // 
  def main(args:Array[String]) {
    val cmds = List[Term](
      TmVar("x"),
      TmApp(TmApp(TmVar("x"),TmVar("x")),TmVar("x")),
      // (λ x. x)
      TmAbs("x", TmVar("x")),
      // (λ x. x) (λ x. x)
      TmApp(TmAbs("x", TmVar("x")), TmAbs("x", TmVar("x"))),
      // ((λx. x) (λx. x)) ((λx. x) (λx. x))
      TmApp(
        TmApp(TmAbs("x", TmVar("x")), TmAbs("x", TmVar("x"))),
        TmApp(TmAbs("x", TmVar("x")), TmAbs("x", TmVar("x")))),

      TmApp(TmAbs("x", TmVar("x")),TmVar("x")),
      TmApp(TmAbs("x", TmVar("x")),TmVar("y"))

    )
    cmds.foreach {
      case e =>
        println(e)
        println("|")
        println("V")
        val e2 = eval(e)
        println(e2)
    }
  }

}

TAPLの内容に若干近づけてみましたが、de bruijn index は使ってなくて、環境もありません。 ここに、TAPLのarithの構文木とその処理をまず追加してみます。

case object TmTrue extends Term
case object TmFalse extends Term
case class TmIf(cond: Term, t1: Term, t2: Term) extends Term

case object TmZero extends Term
case class TmSucc(t: Term) extends Term
case class TmPred(t: Term) extends Term
case class TmIsZero(t: Term) extends Term

でしばらく弄ってみました。

次に、レコード操作の構文木と処理を入れて弄ってみます。

case class TmRecord(fields: List[(String, Term)]) extends Term
case class TmProj(t: Term, proj: String) extends Term

次は文字列の構文木とその処理を入れてみます。殆ど、処理はないんですね。 インタプリタだと楽だなぁ。

最後に、letの構文木と処理を入れて弄ってみます。

case class TmLet(l: String, t1: Term, t2: Term) extends Term

と言う事をやってみた結果のソースが以下になります。

package _01lambda2

trait Term // λ式を表すデータ型
case class TmVar(s:String) extends Term // 変数 // 変数名は文字列であらわすことにする
case class TmAbs(s:String,e:Term) extends Term // λ抽象
case class TmApp(e1:Term, e2:Term) extends Term // 関数適用

case object TmTrue extends Term
case object TmFalse extends Term
case class TmIf(cond: Term, t1: Term, t2: Term) extends Term

case object TmZero extends Term
case class TmSucc(t: Term) extends Term
case class TmPred(t: Term) extends Term
case class TmIsZero(t: Term) extends Term

case class TmRecord(fields: List[(String, Term)]) extends Term
case class TmProj(t: Term, proj: String) extends Term

case class TmString(s: String) extends Term

case class TmLet(l: String, t1: Term, t2: Term) extends Term

class NoRuleApplies(t: Term) extends Exception("No rule applies for term: " + t)

object Demo {
  var counter = 0 // 整数0を持った参照セルcounterを作る
  // 新しい変数名を作る補助的な関数
  def gensym():String = {
    counter += 1 // counter の中身を一つ増やす
    "g" + counter // g1, g2, g3, ... 等の文字列を返す
  }
  // ((lambda x e1) e2)を評価するために、e1のxをe2に書き換える。
  def subst(e2:Term, x:String, e1:Term):Term = {
    e1 match {
      // e1が 変数のとき
      case TmVar(y) =>
        // 変数名が同じなら置き換え、同じでなければそのまま
        if (x == y) e2 else TmVar(y)
      // ラムダ抽象のとき ((lambda x (lambda y e)) e2) => eのyをy1に置き換えて、xをe2に置き換え、たものをe'として
      // 結果、(lambda y1 e')
      case TmAbs(y, e) =>
        // 新しい識別子を作って
        val y1 = gensym()
        // その値で、y TmVar(y1)
        TmAbs(y1, subst(e2, x, subst(TmVar(y1), y, e)))
      // 関数適用のときは、内部を置き換える。
      case TmApp(e, e1) =>
        TmApp(subst(e2, x, e), subst(e2, x, e1))

      case TmTrue           => TmTrue
      case TmFalse          => TmFalse
      case TmIf(t1, t2, t3) => TmIf(subst(e2, x, t1), subst(e2, x, t2), subst(e2, x, t3))

      case TmZero           => TmZero
      case TmSucc(t1)       => TmSucc(subst(e2, x, t1))
      case TmPred(t1)       => TmPred(subst(e2, x, t1))
      case TmIsZero(t1)     => TmIsZero(subst(e2, x, t1))

      case TmProj(t1, l)    => TmProj(subst(e2, x, t1), l)
      case TmRecord(fields) => TmRecord(fields.map { case (l, t) => (l, subst(e2, x, t)) })

      case t: TmString      => t

      case TmLet(y, t1, t2) =>
        val y1 = gensym()

        TmLet(y1, subst(e2, x, t1), subst(e2, x, subst(TmVar(y1), y, t2)))

    }
  }
  // e を受け取り、e => e' となる e' のリストを返す
  def eval1(t:Term):Term = {
    t match {
      case TmApp(TmAbs(x, e0), e2) if (isVal(e2)) => subst(e2, x, e0)
      case TmApp(e1, e2) if (isVal(e1)) => TmApp(e1, eval1(e2))
      case TmApp(e1, e2) => TmApp(eval1(e1), e2)

      case TmIf(TmTrue, t2, t3) => t2
      case TmIf(TmFalse, t2, t3) => t3
      case TmIf(t1, t2, t3) => TmIf(eval1(t1), t2, t3)

      case TmSucc(t1) => TmSucc(eval1(t1))
      case TmPred(TmZero) => TmZero
      case TmPred(TmSucc(nv1)) if (isNumericVal(nv1)) => nv1
      case TmPred(t1) => TmPred(eval1(t1))
      case TmIsZero(TmZero) => TmTrue
      case TmIsZero(TmSucc(nv1)) if (isNumericVal(nv1)) => TmFalse
      case TmIsZero(t1) => TmIsZero(eval(t1))

      case TmRecord(fields) =>
        def evalAField(l: List[(String, Term)]): List[(String, Term)] = l match {
          case Nil                          => throw new NoRuleApplies(t)
          case (l, v1) :: rest if isVal(v1) => (l, v1) :: evalAField(rest)
          case (l, t1) :: rest              => (l, eval1(t1)) :: rest
        }
        TmRecord(evalAField(fields))
      case TmProj(v1 @ TmRecord(fields), l) if isVal(v1) =>
        fields.find { _._1 == l } match {
          case Some((_, ti)) => ti
          case None          => throw new NoRuleApplies(t)
        }
      case TmProj(t1, l) =>
        TmProj(eval1(t1), l)

      case TmLet(x, v1, t2) if isVal(v1) => subst(v1, x, t2)
      case TmLet(x, v1, t2) => TmLet(x, eval(v1), t2)

      case _ => throw new NoRuleApplies(t)
    }
  }
  // 簡約できなくなるまで eval1 を反復する
  def eval(t:Term):Term = {
    try {
      val t1 = eval1(t)
      eval(t1)
    } catch {
      case _: NoRuleApplies => t
    }
  }
  // 
  def isNumericVal(t: Term): Boolean = {
    t match {
      case TmZero     => true
      case TmSucc(t) => isNumericVal(t)
      case _          => false
    }
  }
  // 
  def isVal(t: Term): Boolean = {
    t match {
      case TmAbs(_, _) => true
      case TmVar(_)    => true

      case TmTrue      => true
      case TmFalse     => true

      case t if (isNumericVal(t)) => true

      case TmRecord(fields) => fields.forall { case (_, ti) => isVal(ti) }

      case TmString(_) => true

      case _           => false
    }
  }
  // 
  def main(args:Array[String]) {
    val cmds = List[Term](
      TmVar("x"),
      TmApp(TmApp(TmVar("x"),TmVar("x")),TmVar("x")),
      // (λ x. x)
      TmAbs("x", TmVar("x")),
      // (λ x. x) (λ x. x)
      TmApp(TmAbs("x", TmVar("x")), TmAbs("x", TmVar("x"))),
      // ((λx. x) (λx. x)) ((λx. x) (λx. x))
      TmApp(
        TmApp(TmAbs("x", TmVar("x")), TmAbs("x", TmVar("x"))),
        TmApp(TmAbs("x", TmVar("x")), TmAbs("x", TmVar("x")))),
      // 
      TmApp(TmAbs("x", TmVar("x")),TmVar("x")),
      TmApp(TmAbs("x", TmVar("x")),TmVar("y")),

      TmZero,
      TmTrue,
      TmFalse,
      TmIsZero(TmZero),
      TmIsZero(TmSucc(TmZero)),
      TmIsZero(TmPred(TmSucc(TmZero))),

      TmProj(TmRecord(List("a"->TmZero,"b"->TmZero)),"b"),
      TmString("mojirtu"),
      TmApp(TmAbs("x", TmString("mojiretu")),TmVar("y")),

      TmApp(TmApp(TmAbs("x",TmAbs("y",TmApp(TmVar("x"),TmVar("y")))),TmZero),TmSucc(TmZero)),
      TmLet("x",TmString("X"),TmVar("x"))
    )
    cmds.foreach {
      case e =>
        println(e)
        println("|")
        println("V")
        val e2 = eval(e)
        println(e2)
    }
  }

}

こういうソースを書く事で、untypedの実装について詳しくなる事が出来ました。 ブールとペアノ数、レコード、文字列、letが追加されています。

次はここに、Mapを使った環境を入れてみようと思います。 この辺は、何度もいろいろ実装してみる事で理解が深まるんじゃないかと思います。

TAPLの実装では、substでの再帰処理は、実は似たような操作を複数書く為に、 tmMapと言う関数にビジターパターンを作るような感じで書いてあります。 高階関数を使う事でスッキリする例ですね。

マクロで書かれていた物のうちで、高階関数を使えばマクロを使わなくてすむ処理は 結構あるように思います。

自分の場合は、とにかく、ラムダ計算の実装になれる事を目標にしているので、 de bruijn indexを使わないでsimpleboolの実装をしてみたいと思います。

簡単な実装になれてしまえば、難しい実装も簡単に見えるはずです。 そして、実装が分かっていれば、理論もまた飲み込みやすいはずです。