使用からバインダーまでの関数でバインドされた変数を表現する


11

バインドされた変数を構文で表す問題、特にキャプチャ回避置換の問題はよく知られており、いくつかの解決策があります。

しかし、別のかなり明白なアプローチがあるようです。それにもかかわらず、私はどこでも使用されていません。つまり、基本構文では、「」と書かれた「変数」という用語が1つだけあり、その後、各変数をスコープ内のバインダーにマッピングする関数を個別に指定します。だから、λのような-termは、λ

λx.(λy.xy)

と書かれます。λ 、および機能は、最初のマップになる∙を最初にλ及び第第二のλ。したがって、de Bruijnインデックスのようなもので、対応するバインダーを見つけるために用語を終了するときに「λsをカウントする」だけでなく、関数を評価するだけです。(これを実装のデータ構造として表す場合、各変数用語オブジェクトに、対応するバインダー用語オブジェクトへの単純なポインター/参照を装備することを考えます。)λ.(λ.)λλλ

明らかに、これは人間が読むためのページに構文を書くのには賢明ではありませんが、どちらもde Bruijnインデックスではありません。数学的には完全に理にかなっているように思えます。特に、キャプチャ回避の置換は非常に簡単になります。置換する用語をドロップするだけで、バインディング関数を結合できます。それは「自由変数」の概念を持っていないのは事実ですが、それから(再び)de Bruijnインデックスも実際にはありません。どちらの場合でも、自由変数を含む用語は、「コンテキスト」バインダーのリストが前にある用語で表されます。

私は何かを見逃していますか、この表現が機能しない理由はありますか?他の問題よりもさらに悪化させ、検討する価値がない問題はありますか?(私が今考えることができる唯一の問題は、用語のセットが(それらの結合機能と一緒に)帰納的に定義されていないということですが、それは乗り越えられないようではありません。)または実際に使用されている場所はありますか?


2
欠点については知りません。たぶん形式化(例:証明アシスタント)はもっと重いのでしょうか?わからない...技術的に間違っていることはないことを知っている:ラムダ項を見るこの方法は、証明ネットとしての表現によって示唆されているので、証明ネットを知っている人々(私のような)が暗黙的にそれを使用するずっと。しかし、証明ネットを知っている人は非常にまれです:-)だから、それは本当に伝統の問題かもしれません。PS:質問をわかりやすくするために、2つの緩やかに関連するタグを追加しました(うまくいけば)。
ダミアーノマッツァ

このアプローチは、高次の抽象構文と同等ではありませんか(つまり、バインダーをホスト言語の関数として表す)。ある意味では、関数をバインダーとして使用すると、クロージャーの表現で暗黙的にバインダーへのポインターが確立されます。
ロドルフレピグレ

2
@RodolpheLepigreそうは思いません。特に、私の考えでは、HOASはメタ理論がかなり弱い場合にのみ正しいのに対し、このアプローチは任意のメタ理論では正しいということです。
マイクシュルマン

3
そのため、各バインダーは(ツリー内で)一意の変数名を使用します(そのポインターは自動的に1つです)。これは、Barendregtの規則です。ただし、置換する場合は、置換するものを(Cで)再構築して、一意の名前を引き続き使用する必要があります。それ以外の場合(一般的に)、複数のサブツリーに同じポインターを使用しており、変数キャプチャを取得できます。再構築はアルファの名前変更です。おそらく、ツリーのセットとしてのエンコーディングの詳細に応じて、同様のことが起こりますか?
ダンDoel

3
@DanDoelああ、面白い。言及する必要がないほど明白であると思ったので、置換される変数が出現するたびに、置換される用語の別のコピーをドロップします。そうでない場合、構文ツリーはもうありません!このコピーをアルファ名前変更と考えることは私には起こりませんでしたが、あなたがそれを指摘した今、私はそれを見ることができます。
マイクシュルマン

回答:


11

AndrejとŁukaszの回答は良い点を示していますが、コメントを追加したかったのです。

ダミアーノが言ったことをエコーするために、ポインターを使用してバインディングを表現するこの方法は証明ネットによって提案されたものですが、ラムダ用語で私が見た最も早い場所はクヌースによる古いエッセイでした:

  • ドナルド・クヌース(1970)。正式なセマンティクスの例。アルゴリズム言語のセマンティクスシンポジウム、E. Engeler(編)、数学188、スプリンガーに注意講義。

(λy.λz.yz)x

$(\ lambda y。\ lambda z.yz)x $のKnuthの図

ラムダ項のこの種のグラフィカルな表現は、1970年代初頭にクリストファーワズワース(1971、ラムダ計算の意味論と語用)とリチャードスタットマン(1974、構造的複雑性)の2つの論文で独立して(そしてより深く)研究されましたの証明)。現在、このような図は「λグラフ」と呼ばれることがよくあります(たとえば、このペーパーを参照)。

Knuthの図の用語は線形であることに注意してください。すべての自由変数またはバインド変数が1回だけ発生するという意味で、他の人が述べたように、この種の表現をnon -線形項。

α


10

変数からバインダーへの関数がどのように表現されるか、どのような目的でそれを使用したいかはわかりません。バックポインターを使用している場合、Andrejが指摘したように、置換の計算の複雑さは従来のアルファ名変更よりも優れていません。

Andrejの答えに対するあなたのコメントから、ある程度あなたは共有に興味があると推測します。ここで入力できます。

典型的な型付きラムダ計算では、他の規則に反して、弱化と収縮には構文がありません。

Γt:TΓ,x:At:TW
Γ,x1:A,x2:At:TΓ,x:At:TC

いくつかの構文を追加しましょう:

Γt:TΓ,x:AWx(t):TW
Γ,x1:A,x2:At:TΓ,x:ACxx1,x2(t):TC

Cab,c()ab,c

この構文では、すべての変数が2回使用されます。1回はバインドされ、もう1回は使用されます。これにより、特定の構文から距離を置き、変数と用語がエッジであるグラフとして用語を見ることができます。

アルゴリズムの複雑さから、変数からバインダーへではなく、バインダーから変数へのポインターを使用できるようになり、一定の時間で置換を行うことができます。

さらに、この再定式化により、消去、コピー、および共有をより忠実に追跡できます。サブタームを共有しながら、タームをインクリメンタルにコピー(または消去)するルールを作成できます。それには多くの方法があります。一部の制限された設定では、勝利は非常に驚くべきものです。

これは、インタラクションネット、インタラクションコンビネータ、明示的置換、線形ロジック、Lampingの最適評価、グラフの共有、ライトロジックなどのトピックに近づいています。

これらのトピックはすべて私にとって非常にエキサイティングであり、より具体的な参照を喜んで提供しますが、これがあなたにとって有用であるかどうか、そしてあなたの興味は何ですか?


6

データ構造は機能しますが、ベータ削減ごとにすべての引数をコピーする必要があり、バインドされた変数の発生と同じ数のコピーを作成する必要があるため、他のアプローチよりも効率的ではありません。このようにして、サブターム間のメモリ共有を破壊し続けます。ポインター操作を伴う非純粋なソリューションを提案しているため、エラーが非常に発生しやすいという事実と相まって、おそらくトラブルに見合う価値はありません。

しかし、実験を見てうれしいです!lambdaデータ構造を使用して実装できます(OCamlにはポインターがあり、参照と呼ばれます)。多かれ少なかれ、あなただけ交換する必要がsyntax.mlnorm.mlあなたのバージョンで。コードは150行未満です。


ありがとう!私は実際に実装について真剣に考えていたのではなく、主にde Bruijnの簿記やアルファ名の変更を気にせずに数学的な証明を行うことができると考えていたことを認めます。しかし、「必要になるまで」コピーを作成しないことで、つまり、コピーが互いに分岐するまで、実装がメモリ共有を保持できる可能性はありますか?
マイクシュルマン

β(λx.e1)e2e1e2

2
数学的証明に関しては、型理論的な構文の正式な形式化をかなり行ってきました。私の経験では、セットアップをより具体的にするのではなく、セットアップを一般化してより抽象化すると利点が得られます。たとえば、「バインディングを処理する適切な方法」を使用して構文をパラメーター化できます。そうすると、間違いを犯すのが難しくなります。また、ドブルーインインデックスを使用して型理論を形式化しました。特に、無意味なことをするのを妨げる依存型がある場合は、それほどひどくはありません。
アンドレイバウアー

2
さらに、基本的にこの手法を使用した実装(ただし、ポインターではなく一意の整数とマップを使用)に取り組んできましたが、実際にはお勧めしません。私たちには間違いなく、適切にクローンを作成するのを逃した多くのバグがありました(可能な場合は回避しようとするため、少なからず)。しかし、いくつかのGHCの人々がそれを提唱する論文があると思う(彼らはハッシュ関数を使って一意の名前を生成したと思う)。正確に何をしているかに依存する場合があります。私の場合、それは型推論/チェックであり、そこではあまり適していないようです。
ダンDoel

@MikeShulman合理的な(基本)複雑さ(かなりの量のコピーと消去)のアルゴリズムの場合、Lampingの最適な削減のいわゆる「抽象部分」は、必要になるまでコピーを作成しません。計算を支配できるいくつかの注釈を必要とする完全なアルゴリズムとは対照的に、抽象的な部分も議論の余地のない部分です。
ルカシュルー

5

その他の回答は、主に実装の問題について議論しています。あなたはあなたの主な動機をあまりにも多くの簿記なしで数学的な証明をすることとして言及しているので、ここでそれが私が見る主な問題です。

「各変数をスコープ内のバインダーにマッピングする関数」と言うと、この関数の出力タイプは確かにそれよりも少し微妙です!具体的には、関数は「検討中の用語のバインダー」などの値を取得する必要があります。つまり、用語に応じて変化するセットです(明らかに有用な方法でより大きなアンビエントセットのサブセットではありません)。そのため、置換では、「バインディング関数の結合」を行うことはできません。元の用語のバインダーから置換の結果のバインダーへのマップに従って、値のインデックスを再作成する必要があります。

これらのインデックスの再作成は、ラグの下にスイープするか、何らかの機能性または自然性の観点から適切にパッケージ化できるという意味で、必ず「ルーチン」である必要があります。ただし、名前付き変数の操作に関連する簿記についても同様です。全体として、このアプローチには、より標準的なアプローチと同じくらいの簿記が含まれているように思われます

ただし、これは概念的に非常に魅力的なアプローチであり、慎重に解決されることを楽しみにしています。標準のアプローチとは異なる構文の側面に光を当てることができると想像できます。


実際、各変数のスコープを追跡するにはブックキーピングが必要ですが、適切な構文に制限する必要があるという結論に飛び付かないでください!置換やベータ削減などの操作は、範囲が限定された用語でも定義できますが、このアプローチ(これは、実際には証明ネット/「λ-グラフ」のアプローチ)を形式化する場合、証明アシスタントの場合、まずより一般的な操作を実装してから、それらが適切なスコープのプロパティを保持していることを証明します。
ノアム・ツァイルバーガー

(試してみる価値があることに同意します...誰かがすでに証明ネット/λグラフを形式化する文脈で持っていても驚かないでしょう。)
Noam Zeilberger


5

λLazy.t

全体として、これはクールな表現であると思いますが、バインディングリンクの破損を防ぐために、ポインターを使用したブックキーピングが必要です。可変フィールドを使用するようにコードを変更することは可能ですが、Coqでのエンコードは直接的ではありません。ポインター構造は明示的にされていますが、これはHOASに非常に似ていると今でも確信しています。ただし、の存在はLazy.t、一部のコードが間違った時間に評価される可能性があることを意味します。私のコードではこれは当てはまりません。なぜなら、変数の変数への置換は、force一度に発生する可能性があるからです(たとえば、評価は行われません)。

(* Representation of a term of the λ-calculus. *)
type term =
  | FVar of string      (* Free variable  *)
  | BVar of bvar        (* Bound variable *)
  | Appl of term * term (* Application    *)
  | Abst of abst        (* Abstraction    *)

(* A bound variable is a pointer to the corresponding binder. *)
and bvar = abst

(* A binder is represented as its body in which the bound variable points to
   the binder itself. Note that we need to use a thunk to be able to work
   underneath a binder (for substitution, evaluation, ...). A name can be
   given for easy printing, but no renaming is done. Only “visual capture”
   can happen since pointers are established the right way, even if names
   can clash. *)
and abst = { body : term Lazy.t ; name : string }

(* Terms can be built with recursive values for abstractions. *)

(* Krivine's notation is used for application (function in parentheses). *)

let id    : term = (* λx.x        *)
  Abst(let rec id = {body = lazy (BVar(id)); name = "x"} in id)

let idid  : term = (* (λx.x) λx.x *)
  Appl(id, id)

let delta : term = (* λx.(x) x *)
  Abst(let rec d = {body = lazy (Appl(BVar(d), BVar(d))); name = "x" } in d)

let weird : term = (* (λx.x) λy.(λx.(x) x) (C) y *)
  Appl(id, Abst(let rec x = {body = lazy (Appl(delta, Appl(FVar("C"),
    BVar(x)))); name = "y"} in x))

let omega : term = (* (λx.(x) x) λx.(x) x *)
  Appl(delta, delta)

(* Printing function is immediate. *)
let rec print : out_channel -> term -> unit = fun oc t ->
  match t with
  | FVar(x)   -> output_string oc x
  | BVar(x)   -> output_string oc x.name
  | Appl(t,u) -> Printf.fprintf oc "(%a) %a" print t print u
  | Abst(f)   -> Printf.fprintf oc "λ%s.%a" f.name print (Lazy.force f.body)

(* Substitution of variable [x] by [v] in the term [t]. Occurences of [x] in
   [t] are identified using physical equality ([BVar] case). The subtle case
   is [Abst], because we need to reestablish the physical link between the
   binder and the variable it binds. *)
let rec subst_var : bvar -> term -> term -> term = fun x t v ->
  match t with
  | FVar(_)   -> t
  | BVar(y)   -> if y == x then v else t
  | Appl(t,u) -> Appl(subst_var x t v, subst_var x u v)
  | Abst(f)   ->
      (* First compute the new body. *)
      let fv = subst_var x (Lazy.force f.body) v in
      (* Reestablish the physical link, using [subst_var] itself again. This
         requires a second traversal of the term. We could probably do both
         at once, but who cares the complexity is linear in [t] anyway. *)
      Abst(let rec g = {f with body = lazy (subst_var f fv (BVar(g)))} in g)

(* Actual substitution function. *)
let subst : abst -> term -> term = fun f v ->
  subst_var f (Lazy.force f.body) v

(* Normalization function (all the way, even under binders). *)
let rec eval : term -> term = fun t ->
  match t with
  | Appl(t,u) ->
      begin
        let v = eval u in
        match eval t with
        | Abst(f) -> eval (subst f v)
        | t       -> Appl(t,v)
      end
  | Abst(f)   ->
      (* Actual computation in the body. *)
      let fv = eval (Lazy.force f.body) in
      (* Here, the physical link is reestablished, but it is important to note
         that the computation of evaluation is done above. So the part below
         only takes a linear time in the size of the normal form of the body
         of the abstraction. *)
      Abst(let rec g = {f with body = lazy (subst_var f fv (BVar(g)))} in g)
  | _         ->
      t

let _ = Printf.printf "id         = %a\n%!" print id
let _ = Printf.printf "eval id    = %a\n%!" print (eval id)

let _ = Printf.printf "idid       = %a\n%!" print idid
let _ = Printf.printf "eval idid  = %a\n%!" print (eval idid)

let _ = Printf.printf "delta      = %a\n%!" print delta
let _ = Printf.printf "eval delta = %a\n%!" print (eval delta)

let _ = Printf.printf "omega      = %a\n%!" print omega
(* The following obviously loops. *)
(*let _ = Printf.printf "eval omega = %a\n%!" print (eval omega)*)

let _ = Printf.printf "weird      = %a\n%!" print weird
let _ = Printf.printf "eval weird = %a\n%!" print (eval weird)

(* Output produced:
id         = λx.x
eval id    = λx.x
idid       = (λx.x) λx.x
eval idid  = λx.x
delta      = λx.(x) x
eval delta = λx.(x) x
omega      = (λx.(x) x) λx.(x) x
weird      = (λx.x) λy.(λx.(x) x) (C) y
eval weird = λy.((C) y) (C) y
*)
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.