ピュア/依存型システムの簡単だが完全な説明は何ですか?


32

何かが単純な場合は、いくつかの言葉で完全に説明できるはずです。これは、λ計算に対して行うことができます。

λ計算は、構文規則(基本的に構造)であり、リダクションルール(特定のパターンが出現するまで、そのようなパターンが存在しなくなるまで繰り返し検索/置換手順が適用されることを意味します)。

文法:

Term = (Term Term) | (λ Var . Term) | Var

削減ルール:

((λ var body) term) -> SUBS(body,var,term)
    where `SUBS` replaces all occurrences of `var`
    by `term` in `body`, avoiding name capture.

例:

(λ a . a)                             -> (λ a a)
((λ a . (λ b . (b a))) (λ x . x))     -> (λ b . (b (λ x x)))
((λ a . (a a)) (λ x . x))             -> (λ x . x)
((λ a . (λ b . ((b a) a))) (λ x . x)) -> (λ b . ((b (λ x . x)) (λ x . x)))
((λ x . (x x)) (λ x . (x x)))         -> never halts

多少非公式ですが、これは通常の人間がλ計算を全体として理解するのに十分な情報であり、22行のマークダウンが必要であると主張することができます。理解しようとしているイドリス/アグダと同様のプロジェクトで使用され純粋/依存型システムが、私が見つけた簡単な説明は単に簡単でした-素晴らしい論文ですが、それは多くの以前の知識を前提としているようです(Haskell、帰納的定義)私は持っていません。より簡潔で、金持ちが少ないと、これらの障壁のいくつかを取り除くことができると思います。したがって、

上記のλ計算を提示したのと同じ形式で、純粋/依存型システムの簡単で完全な説明をすることは可能ですか?


4
Pure Type Systemsのルールは非常に簡単です。Simply Easyは、依存型を実装することです。

2
攻撃的な意味では「敵対的」ではありませんが、自分で答えを見つけるのに十分な努力を払わないことに対して、私は多くのことを要求していると思いますか?もしそうなら、この質問は多くの要求をするかもしれないので、たぶんそれは悪いだけです。しかし、その背後には多くの努力もあります、私の試みで編集する必要があると思いますか?
MaiaVictor

3
私も、共著者に代わって、「単純に簡単」を作業タイトルとして置き換えた「依存型付きラムダ計算のチュートリアル実装」のテキストを書いた共著者を代表して腹を立てています。Haskellの<100行のタイプチェッカーであるコードのカーネルを作成しました。

2
それから私は確かに自分自身をひどく表現した。私は "Simply Easy"ペーパーが大好きで、数日前からすべての休憩でそれを読んでいます-世界でそれが私に部分的な感覚を与えた唯一のものです。 。しかし、私はそれが私が持っているよりも多くの知識を持つ大衆を対象としていると思います。紙の品質とは何の関係もありませんが、私自身の限界です。
MaiaVictor

1
@pigworkerとコードは、ここで尋ねたように(英語の説明との関係で)全体のはるかに短いが完全な説明であるという理由で、私のお気に入りの部分です。ダウンロードできるコードのコピーがありますか?
MaiaVictor

回答:


7

免責事項

あなたが要求したように、これは非常に非公式です。

文法

依存型付けされた言語では、型レベルと値レベルでバインダーがあります。

Term = * | (∀ (Var : Term). Term) | (Term Term) | (λ Var. Term) | Var

まあ型付けされた用語が添付型の用語であり、我々は書きますt ∈ σ

σ
t

用語tにタイプがあることを示すためσ

入力規則

単純にするために、λ v. t ∈ ∀ (v : σ). τ両方λでそれを要求し、同じ変数をバインドします(vこの場合)。

ルール:

t ∈ σ is well-formed if σ ∈ * and t is in normal form (0)

*            ∈ *                                                 (1)
∀ (v : σ). τ ∈ *             -: σ ∈ *, τ ∈ *                     (2)
λ v. t       ∈ ∀ (v : σ). τ  -: t ∈ τ                            (3)
f x          ∈ SUBS(τ, v, x) -: f ∈ ∀ (v : σ). τ, x ∈ σ          (4)
v            ∈ σ             -: v was introduced by ∀ (v : σ). τ (5)

したがって、*「すべての型の型」(1)、型(2)から型を形成し、ラムダ抽象はpi型(3)を持ち、vによって導入された場合∀ (v : σ). τvσ(5)を持ちます。

「標準形式」とは、削減ルールを使用してできるだけ多くの削減を実行することを意味します。

「The」削減ルール

(λ v. b ∈ ∀ (v : σ). τ) (t ∈ σ) ~> SUBS(b, v, t) ∈ SUBS(τ, v, t)
    where `SUBS` replaces all occurrences of `v`
    by `t` in `τ` and `b`, avoiding name capture.

または二次元構文で

σ
t

意味t ∈ σ

(∀ (v : σ). τ) σ    SUBS(τ, v, t)
                 ~>
(λ  v     . b) t    SUBS(b, v, t)

用語が関連するforall量指定子の変数と同じタイプの場合にのみ、用語にラムダ抽象化を適用できます。次に、前の純粋なラムダ計算と同じ方法で、ラムダ抽象化とforall量指定子の両方を削減します。値レベルの部分を差し引いた後、(4)タイピング規則が得られます。

関数適用演算子は次のとおりです。

∀ (A : *) (B : A -> *) (f : ∀ (y : A). B y) (x : A). B x
λ  A       B            f                    x     . f x

(言及しない場合は省略∀ (x : σ). τσ -> τますτx

ftypeのB y提供に対して返されます。に適用します。これは正しいタイプで、afterの代わりに使用します。したがって、〜>yAfxAyx.f x ∈ SUBS(B y, y, x)f x ∈ B x

関数適用演算子as appを省略して、それ自体に適用してみましょう。

∀ (A : *) (B : A -> *). ?
λ  A       B          . app ? ? (app A B)

?たちが提供する必要のある条件を設けます。まず、明示的に導入およびインスタンス化してA、およびB

∀ (f : ∀ (y : A). B y) (x : A). B x
app A B

今、私たちは持っているものを統一する必要があります

∀ (f : ∀ (y : A). B y) (x : A). B x

と同じです

(∀ (y : A). B y) -> ∀ (x : A). B x

そしてapp ? ?受け取るもの

∀ (x : A'). B' x

これにより

A' ~ ∀ (y : A). B y
B' ~ λ _. ∀ (x : A). B x -- B' ignores its argument

(「偏見とは?」も参照)

私たちの表現(名前の変更後)は

∀ (A : *) (B : A -> *). ?
λ  A       B          . app (∀ (x : A). B x) (λ _. ∀ (x : A). B x) (app A B)

のためにABそしてf(where f ∈ ∀ (y : A). B y

∀ (y : A). B y
app A B f

インスタンス化ABて取得することができます(f適切なタイプの場合)

∀ (y : ∀ (x : A). B x). ∀ (x : A). B x
app (∀ (x : A). B x) (λ _. ∀ (x : A). B x) f

型の署名はと同等(∀ (x : A). B x) -> ∀ (x : A). B xです。

全体の表現は

∀ (A : *) (B : A -> *). (∀ (x : A). B x) -> ∀ (x : A). B x
λ  A       B          . app (∀ (x : A). B x) (λ _. ∀ (x : A). B x) (app A B)

すなわち

∀ (A : *) (B : A -> *) (f : ∀ (x : A). B x) (x : A). B x
λ  A       B            f                    x     .
    app (∀ (x : A). B x) (λ _. ∀ (x : A). B x) (app A B) f x

値レベルでのすべての削減の後、同じappバックが得られます。

そのためapp、取得する純粋なラムダ計算ではほんの数ステップしか必要ありませんがapp app、型付き設定(特に依存型付き)では、統一に注意する必要があります。* ∈ *ます。

型チェック

  • 場合t*、その後t ∈ *で(1)
  • 場合t∀ (x : σ) τσ ∈? *τ ∈? *(約ノートを参照∈?その後、下記を)t ∈ *(2)により、
  • 場合はtあるf xf ∈ ∀ (v : σ) τいくつかのためにστx ∈? σその後、t ∈ SUBS(τ, v, x)(4)によって
  • if tが変数でありv、それまでに(5)によってv導入された∀ (v : σ). τt ∈ σ

これらはすべて推論規則ですが、ラムダに対して同じことはできません(型の推論は依存型では決定できません)。したがって、ラムダについてt ∈? σは、推測ではなく()を確認します。

  • tありλ v. b、チェックされている場合∀ (v : σ) τb ∈? τそして、t ∈ ∀ (v : σ) τ
  • if tが他のものであり、チェックされている場合は、上記の関数σt使用するタイプを推測し、σ

型の等価性チェックでは、それらの型が通常の形式である必要があるため、型があるかどうかを判断するにtは、σまずtype を持つ型をチェックしσます*。もしそうなら、それσは正規化可能(モジュロジラールのパラドックス)であり、正規化σされます(したがって(0)で整形式になります)。SUBSまた、式を正規化して保持します(0)。

これは、双方向型チェックと呼ばれます。これにより、すべてのラムダに型の注釈を付ける必要がありません:f xfが既知の場合、推論されて同等であるかどうか比較される代わりにx、引数の型に対してチェックされますf(これも非効率的です)。しかし、fがラムダである場合、明示的な型注釈が必要です(注釈は文法およびどこでも省略されます。追加Ann Term Termまたはλ' (σ : Term) (v : Var)。コンストラクタに、コンストラクタに)。

また、Simpler、Easierをご覧ください!ブログ投稿。


1
「より簡単に」を再利用します。

forallの最初の削減ルールは奇妙に見えます。ラムダとは異なり、forallsは適切に型付けされた方法で適用されるべきではありません(右?)。

@chi、あなたの言っていることがわかりません。おそらく私の表記法は悪いです:縮小規則は(λ v. b ∈ ∀ (v : σ). τ) (t ∈ σ)〜>を言っていSUBS(b, v, t) ∈ SUBS(τ, v, t)ます。
user3237465

1
この表記法は誤解を招くものです。2つのルールがあるように見えます。1つはナンセンスなルールで(∀ (v : σ). τ) t ~> ...、もう1つは意味のあるルールです(λ v. b) t ~> ...。最初のものを削除し、以下のコメントに変換します。

1
規則(1)には、その結論が前提として含まれています。動作するシステムができて初めて、システムのシンプルさと双方向バージョンを比較できます。あなたはすべてを正規化しておくと言うかもしれませんが、あなたのルールはそうではありません。

24

やってみましょう。Girardのパラドックスについては気にしません。なぜなら、それは中心的なアイデアから逸れるからです。判断や派生などに関するプレゼンテーションの仕組みを紹介する必要があります。

文法

用語:: =(Elim)| * | (Var:Term)→Term | λVar↦Term

Elim :: = Term:Term | ヴァール| エリムターム

文法には、相互に定義された2つの形式があります。「用語」は、*(タイプは物、値は物)の一般的な概念です。*(タイプのタイプ)、従属関数タイプ、 「消去」(つまり、「構築」ではなく「使用」)、最終的に関数の位置にあるものが変数またはその型で注釈が付けられた用語であるネストされたアプリケーションです。

削減ルール

(λy↦t:(x:S)→T)s↝t [s:S / y]:T [s:S / x]

(t:T)↝t

置換演算t [e / x]は、変数xのすべての出現を除去eで置き換え、名前のキャプチャを回避します。削減できるアプリケーションを作成するには、ラムダ項にそのタイプで注釈を付けて消去する必要があります。型注釈は、ラムダ抽象化に一種の「反応性」を与え、アプリケーションが続行できるようにします。これ以上アプリケーションが発生せず、アクティブなt:Tが用語構文に埋め込まれるポイントに到達したら、型注釈を削除できます。

構造的閉包によって↝縮小関係を拡張してみましょう。ルールは、左側に一致するものを見つけることができる用語と消去の内部のどこにでも適用されます。flexの再帰推移的(0以上のステップ)閉包に対して↝*を記述します。結果として生じる削減システムは、この意味でコンフルエントです:

s↝* pおよびs↝* qの場合、p↝* rおよびq↝* rであるようなrが存在します。

コンテキスト

コンテキスト:: = | コンテキスト、変数:用語

コンテキストは、変数に型を割り当てるシーケンスであり、右側に成長します。これは「ローカル」エンドと考えられ、最近バインドされた変数について通知します。コンテキストの重要な特性は、コンテキストでまだ使用されていない変数を常に選択できることです。コンテキスト内の変数に起因する型は異なるという不変式を維持します。

判断

判定:: =コンテキスト⊢用語には用語があります| コンテキスト⊢Elimは用語

それが判断の文法ですが、どのように読むのでしょうか?まず、⊢は伝統的な「ターンスタイル」シンボルであり、仮定と結論を分離します。非公式に「says」と読むことができます。

G⊢Tはt

コンテキストGを指定すると、タイプTは用語tを許可します。

G⊢eはS

は、コンテキストGが与えられ、消去eにタイプSが与えられることを意味します。

判断には興味深い構造があります。ゼロ以上の入力、1つ以上のサブジェクト、ゼロ以上の出力です。

INPUTS                   SUBJECT        OUTPUTS
Context |- Term   has    Term
Context |-               Elim      is   Term

つまり、事前に用語のタイプを提案してチェックする必要がありますが、消去のタイプを合成します。

入力規則

J-:P1;と書いて、漠然とPrologスタイルでこれらを提示します。...; Pnは、施設P1からPnも成立する場合に判断Jが成立することを示します。前提は、別の判断、または削減に関する主張になります。

条項

G⊢Tにはt-:T↝R; G⊢Rはt

G⊢*は*

G⊢*は(x:S)→T-:G⊢*はS; G、z:S!-*はT [z / x]

G⊢(x:S)→Tにはλy↦tがあります-:G、z:S⊢T [z / x]にはt [z / y]があります

G⊢Tは(e)-:G⊢eはT

消去

G⊢eはR-:G⊢eはS; S↝R

G、x:S、G '⊢xはS

G⊢fsはT [s:S / x]-:G⊢fは(x:S)→T; G⊢Sにはsがあります

以上です!

ルールは2つだけではありません構文指向で「用語をチェックするために使用する前にタイプを減らすことができる」というルールと、「消去から合成した後にタイプを減らすことができる」というルールです。実行可能な戦略の1つは、最上位のコンストラクターを公開するまで型を減らすことです。

このシステムは強力に正規化されていません(ジラードのパラドックスは、自己参照の嘘つきスタイルのパラドックスであるため)より高いレベルの型を持ち、自己参照を防ぎます。

ただし、この意味では、このシステムには型保存のプロパティがあります。

G⊢Tにt、G↝* D、T and * R、t↝rがある場合、D⊢Rにはrがあります。

G⊢eがSかつG↝* Dかつe↝fの場合、S↝* RおよびD⊢fがRであるようなRが存在します。

コンテキストは、含まれる用語の計算を許可することにより計算できます。つまり、判断が有効になった場合、その入力を好きなだけ計算し、その主題を1ステップ計算することができます。そして、その出力を計算して、結果の判断が有効であることを確認できます。この証明は、-> *が合流することを前提として、派生型の単純な帰納法です。

もちろん、ここでは機能コアのみを紹介しましたが、拡張機能はかなりモジュール化できます。ここにペアがあります。

用語:: = ... | (x:S)* T | s、t

エリム:: = ... | e.head | e.tail

(s、t:(x:S)* T).head↝s:S

(s、t:(x:S)* T).tail↝t:T [s:S / x]

G⊢*は(x:S)* Tを持ちます-:G S *はSを持ちます; G、z:S⊢*はT [z / x]

G⊢(x:S)* Tにはs、tがあります-:G⊢Sにはsがあります。G⊢T [s:S / x]はt

G⊢e.headはS-:G⊢eは(x:S)* T

G⊢e.tailはT [e.head / x]-:G⊢eは(x:S)* T


1
G, x:S, G' ⊢ x is S -: G' ⊬ x
user3237465

1
@ user3237465いいえ。ありがとう!一定。(私はhtmlの改札口でASCIIアートの改札口を交換した時(これ私の携帯電話上でそれらが見えなくなって、申し訳ありませんそれは他の場所で起こっている場合)、私はその1を逃した。)

1
ああ、私はあなたがちょうどタイプミスを指摘していると思った。ルールは、コンテキスト内の各変数について、コンテキストがそれを割り当てる型を合成すると述べています。コンテキストを導入するとき、「コンテキスト内の変数に起因する型が異なるという不変式を維持します」と言いました。シャドウイングは許可されていません。ルールがコンテキストを拡張するたびに、私たちが踏み込んでいるバインダーをインスタンス化する新しい「z」を常に選択することがわかります。シャドーイングは嫌悪感です。コンテキストx:*、x:xがある場合、よりローカルなxの型は、スコープ外のxであるため、もはや適切ではありません。

1
私はあなたと他の回答者に、私が仕事を休むたびにこのスレッドに戻ってくることを知ってほしかった。私は本当にこれを学びたいです、そして、最初の時間のために、私は実際にそれのほとんどを得るように落ちました。次のステップは、いくつかのプログラムの実装と作成です。このような素晴らしいトピックに関する情報が世界中の私のような人に提供される時代に生きることができてうれしく思います。インターネットで無料。私の質問をひどくフレージングしてすみません、ありがとうございます!
MaiaVictor

1
@codyはい、拡張はありません。不要な理由を確認するには、2つの計算ルールを使用して、用語をチェックする前に型を完全に正規化する戦略を展開し、消去から合成した直後に型を正規化することに注意してください。そのため、型を一致させる必要があるルールでは、それらは既に正規化されているため、「元の」チェックおよび合成された型が変換可能である場合は等しくなります。一方、この事実だけで等値チェックを制限することは問題ありません。Tが標準型に変換可能な場合、標準型に変換されます。
ピッグワーカー

8

Curry-Howardの対応は、論理の型システム証明システムの間に体系的な対応があると述べています。これをプログラマ中心の視点から見ると、次のように書き直すことができます。

  • 論理的証明システムはプログラミング言語です。
  • これらの言語は静的に型付けされています。
  • そのような言語における型システムの責任は、不健全な証拠を構築するプログラムを禁止することです。

この角度から見た:

  • 要約する型指定のないラムダ計算には重要な型システムがないため、それに基づいて構築された証明システムは不適切です。
  • 単純に型付けされたラムダ計算は、文論理で防音を構築するために必要なすべての型(「if / then」、「and」、「or」、「not」)を備えたプログラミング言語です。しかし、その型は、量指定子を含む証明をチェックするのに十分ではありません(「すべてのx、...」、「...などのxが存在する」)。
  • 依存型付きラムダ計算には、文型論理 1次量指定子(値の数量化)をサポートする型と規則があります。

Wikipediaの自然推論に関するエントリの図を使用して、1次論理の自然推論のルールを示します。これらは基本的に、最小限の依存型付きラムダ計算のルールでもあります。

一次自然控除

ルールにはラムダ用語が含まれていることに注意してください。これらは、そのタイプによって表される文の証明を構築するプログラムとして読むことができます(より簡潔には、プログラムは証明であると言います)。指定した同様の縮約規則は、これらのラムダ項に適用できます。


なぜこれが重要なのでしょうか?そもそも、プルーフはプログラミングに役立つツールである可能性が高く、プルーフをファーストクラスのオブジェクトとして動作させることができる言語を持つことで多くの道が開かれるからです。たとえば、関数に前提条件がある場合、それをコメントとして書き留める代わりに、実際に引数としてその証明を要求できます。

第二に、量指定子を処理するために必要な型システムの機械には、プログラミングの文脈で他の用途があるかもしれないからです。特に、依存型付け言語と呼ばれる概念を使用して普遍数量詞(「すべてのxについて、...」)を扱う依存関数型 -a機能静的タイプ結果のが依存することができますランタイム値引数のを。

これを非常に歩行者に適用するために、均一な構造のレコードで構成されるAvroファイルを読み取る必要があるコードを常に記述します。すべてが同じフィールド名とタイプのセットを共有します。これには次のいずれかが必要です。

  1. プログラム内のレコードの構造をレコードタイプとしてハードコーディングします。
    • 利点:コードがよりシンプルになり、コンパイラーがコード内のエラーをキャッチできる
    • 欠点:プログラムは、レコードタイプに一致するファイルを読み取るようにハードコードされています。
  2. 実行時にデータのスキーマを読み取り、それをデータ構造として一般的に表し、それを使用してレコードを一般的に処理します
    • 利点:私のプログラムは1つのファイルタイプだけにハードコードされていません
    • 欠点:コンパイラは多くのエラーをキャッチできません。

Avro Javaチュートリアルページで見ることができるように、これらのアプローチの両方に従ってライブラリを使用する方法を示しています。

依存する関数型を使用すると、より複雑な型システムを犠牲にして、ケーキを食べて食べることができます。Avroファイルを読み取り、スキーマを抽出し、ファイルの内容を、ファイルに保存されているスキーマに依存する静的タイプを持つレコードのストリームとして返す関数を作成できます。コンパイラは、たとえば、実行時に処理するファイルのレコードに存在しない可能性のある名前付きフィールドにアクセスしようとしたときにエラーをキャッチできます。甘いでしょ?


1
あなたが言及した方法で実行時に型を構築することは、私が考えもしなかった本当にクールなものです。むしろ甘い!洞察に満ちた答えをありがとう。
MaiaVictor
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.