rewrite チュートリアル
このチュートリアルでは、算術項の簡約化規則を書き、それを 1 ステップずつ適用し、ステップ上限のもとで正規形まで実行し、トレースを読んでどの規則がどこで発火したかを確認する。項は演算子を値とする Term[String] であり、 は Apply(Value("*"), [Variable(x), Value("1")]) である。
クイックスタート
moon add Luna-Flow/type_theory@0.2.0
import {
"Luna-Flow/type_theory/core",
"Luna-Flow/type_theory/syntax",
"Luna-Flow/type_theory/rewrite",
}
規則とは、項をその根で書き換えるか None を返す関数である。規則 は を 2 ステップで簡約化する。
fn times_one(t : @syntax.Term[String]) -> @syntax.Term[String]? {
match t {
Apply(Value("*"), [other, Value("1")]) => Some(other)
_ => None
}
}
test "quick start: x * 1 * 1" {
let x = @syntax.Term::Variable(@core.Name::new("x"))
let term : @syntax.Term[String] = Apply(Value("*"), [
Apply(Value("*"), [x, Value("1")]),
Value("1"),
])
let rule = @rewrite.RuleName::unsafe_new("times_one")
let step = (t : @syntax.Term[String]) => @rewrite.top_down_once(t, rule, times_one)
assert_eq(@rewrite.normalize(term, step, 10), NormalForm(term=x, steps=2))
}
日常的な作業
これらの例では、いくつかの補助関数と二つ目の規則 を共有する。
fn op(name : String, a : @syntax.Term[String], b : @syntax.Term[String]) -> @syntax.Term[String] {
@syntax.Apply(@syntax.Value(name), [a, b])
}
fn lit(text : String) -> @syntax.Term[String] {
@syntax.Value(text)
}
fn var_(text : String) -> @syntax.Term[String] {
@syntax.Term::Variable(@core.Name::new(text))
}
fn zero_plus(t : @syntax.Term[String]) -> @syntax.Term[String]? {
match t {
Apply(Value("+"), [Value("0"), other]) => Some(other)
_ => None
}
}
1 ステップ実行し、どこで起きたかを見る
top_down_once は最外最左のマッチを書き換え、そこへのパスを報告する。
test "one step with its position" {
let term = op("*", lit("2"), op("+", lit("0"), var_("y")))
let rule = @rewrite.RuleName::unsafe_new("zero_plus")
match @rewrite.top_down_once(term, rule, zero_plus) {
Reduced(after~, path~, rule=used, ..) => {
assert_eq(after, op("*", lit("2"), var_("y")))
assert_eq(path.to_array(), [@rewrite.ApplyArgument(1)])
inspect(used.value(), content="zero_plus")
}
NoStep => fail("expected a step")
}
}
ApplyArgument(1) は根の * の第 2 引数である。
ステップ上限付きで正規化する
ステップ上限は止まらない規則から身を守る。どちらの結果が得られたかを確認すること。
test "normal form or limit" {
let term = op("+", lit("0"), op("+", lit("0"), var_("z")))
let rule = @rewrite.RuleName::unsafe_new("zero_plus")
let step = (t : @syntax.Term[String]) => @rewrite.top_down_once(t, rule, zero_plus)
match @rewrite.normalize(term, step, 1) {
StepLimitReached(term~, steps~) => {
assert_eq(steps, 1)
assert_eq(term, op("+", lit("0"), var_("z")))
}
NormalForm(..) => fail("one step is not enough")
}
assert_eq(@rewrite.normalize(term, step, 5), NormalForm(term=var_("z"), steps=2))
}
トレースを読む
trace はすべてのステップを保持するので、簡約化の過程を説明できる。
test "explain a simplification" {
let term = op("+", lit("0"), op("+", lit("0"), var_("z")))
let rule = @rewrite.RuleName::unsafe_new("zero_plus")
let step = (t : @syntax.Term[String]) => @rewrite.top_down_once(t, rule, zero_plus)
let run = @rewrite.trace(term, step, 10)
let lines = run
.steps()
.map(s => match s {
Reduced(rule~, path~, ..) => "\{rule.value()} at depth \{path.to_array().length()}"
NoStep => "none"
})
inspect(lines.join("; "), content="zero_plus at depth 0; zero_plus at depth 0")
}
最外か最内かを選ぶ
簡約基が入れ子になっているとき、top_down_once は外側を、bottom_up_once は内側を選ぶ。合流性を持つ規則集合では両者は同じ正規形に到達するが、ステップは異なる。
test "outermost and innermost first steps" {
let inner = op("+", lit("0"), var_("z"))
let term = op("+", lit("0"), inner)
let rule = @rewrite.RuleName::unsafe_new("zero_plus")
match @rewrite.top_down_once(term, rule, zero_plus) {
Reduced(path~, ..) => assert_eq(path.to_array().length(), 0)
NoStep => fail("expected a step")
}
match @rewrite.bottom_up_once(term, rule, zero_plus) {
Reduced(path~, ..) => assert_eq(path.to_array(), [@rewrite.ApplyArgument(1)])
NoStep => fail("expected a step")
}
}
複数の規則を組み合わせる
規則を順に試すことで一つにまとめる。まとめた規則は一つの名前を持つので、そのことが分かる名前を付けること。
fn simplify(t : @syntax.Term[String]) -> @syntax.Term[String]? {
match zero_plus(t) {
Some(u) => Some(u)
None => times_one(t)
}
}
test "two rules, one normal form" {
let term = op("+", lit("0"), op("*", var_("a"), lit("1")))
let rule = @rewrite.RuleName::unsafe_new("simplify")
let step = (t : @syntax.Term[String]) => @rewrite.top_down_once(t, rule, simplify)
assert_eq(@rewrite.normalize(term, step, 10), NormalForm(term=var_("a"), steps=2))
}
どの規則が発火したかを知る必要がある場合は、規則ごとに top_down_once を一度ずつ(それぞれ固有の名前で)呼び出し、最初の Reduced を返すステップ関数を書く。この方法では、外側の位置にある二つ目の規則よりも、項のどこかにある一つ目の規則が優先される。
さらに進んで
束縛子の下の規則
走査は束縛子の本体に入るので、規則は開いた項を見ることになる。上の二つのように項の形だけを見る規則は、どこで使っても問題ない。束縛子をまたいで部分項を移動・複製する規則は、通常は代入を使って、束縛を自分で扱わなければならない。@lambda の β 規則が標準的な例である。
独自の AST を書き換える
generic_top_down_once と generic_normalize は @syntax.BindingSyntax を実装する任意の AST を受け付ける。規則は独自の型に対して書けばよい。完全な例は adapter チュートリアルにある。
戦略
eval はこれらの走査を名前付きの戦略(正規順序、適用順序、弱頭部)としてまとめ、1 つの evaluate 関数の背後に置く。
よくある落とし穴
- 進展しない規則。 自身の入力 に対して
Some(t)を返す規則や、別の規則を打ち消す規則は、正規化器をステップ数の上限まで空回りさせる。StepLimitReachedを確認すること。 - 合流しない規則。 2 つの規則が重なり合っていて合流しない場合、戦略によって異なる正規形が得られることがある。トレースを使って重なりを見つけること。
- 入力に対する
unsafe_new。RuleName::unsafe_new("")はプログラムを中断する。リテラルでない名前にはRuleName::newを使うこと。 - 曖昧な構築子。 期待される型がない場合、
@syntax.Variable(x)は曖昧である。@syntax.Term::Variable(x)と書くこと。
次のステップ
- rewrite API:すべての型と関数。
- rewrite の設計:書き換え理論、単一簡約基契約、正規形補題。
- eval チュートリアル:名前付きの簡約戦略。