From d946f1067c3af50279ab1a635ccbba5ae8b18666 Mon Sep 17 00:00:00 2001 From: s-taiga <56785231+s-taiga@users.noreply.github.com> Date: Sun, 25 Aug 2024 12:31:52 +0900 Subject: [PATCH] =?UTF-8?q?8.1=E7=AB=A0=20indexed-families=20(#71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 翻訳開始 * 翻訳完了 * セルフレビュー --- functional-programming-lean/src/SUMMARY.md | 2 +- .../src/dependent-types/indexed-families.md | 183 ++++++++++++++++++ 2 files changed, 184 insertions(+), 1 deletion(-) diff --git a/functional-programming-lean/src/SUMMARY.md b/functional-programming-lean/src/SUMMARY.md index cf09ee58..7d95a63d 100644 --- a/functional-programming-lean/src/SUMMARY.md +++ b/functional-programming-lean/src/SUMMARY.md @@ -53,7 +53,7 @@ - [その他の便利な機能](monad-transformers/conveniences.md) - [まとめ](monad-transformers/summary.md) - [依存型によるプログラミング](dependent-types.md) - - [Indexed Families](dependent-types/indexed-families.md) + - [添字族](dependent-types/indexed-families.md) - [The Universe Design Pattern](dependent-types/universe-pattern.md) - [Worked Example: Typed Queries](dependent-types/typed-queries.md) - [Indices, Parameters, and Universe Levels](dependent-types/indices-parameters-universes.md) diff --git a/functional-programming-lean/src/dependent-types/indexed-families.md b/functional-programming-lean/src/dependent-types/indexed-families.md index cec32b09..8db01095 100644 --- a/functional-programming-lean/src/dependent-types/indexed-families.md +++ b/functional-programming-lean/src/dependent-types/indexed-families.md @@ -1,36 +1,71 @@ + +# 添字族 + + + +多相的な帰納型は型引数を取ります。例えば、`List` はリストの要素の型を、`Except` は例外や値の型をそれぞれ決定する引数を取ります。これらの型引数は対象のデータ型のすべてのコンストラクタで共有され、**パラメータ** と呼ばれます。 + + +しかし、帰納型の引数はすべてのコンストラクタで同じである必要はありません。コンストラクタの選択によって型への引数が変化するような帰納型は **添字族** (indexed family)と呼ばれ、変化する引数は **添字** (index)と呼ばれます。添字族における「hello world」的教材は、要素の型に加えてリストの長さを含むリスト型であり、慣習的に「ベクトル」と呼ばれています: + ```lean {{#example_decl Examples/DependentTypes.lean Vect}} ``` + +関数宣言ではコロンの前にいくつかの引数を取り、定義全体で使用可能であることを示します。そしていくつかの引数はコロンの後ろに置き、この引数をパターンマッチの利用や関数ごとに定義できるようにしていることを示します。帰納的データ型でも同じ原理が働きます:引数 `α` がデータ型宣言の先頭かつコロンより前に来ている場合、これはパラメータで `Vect` 中のすべての定義の第一引数として提供されなければならないことを意味します。一方で、`Nat` 引数はコロンの後ろにあり、これは変化する可能性のあるインデックスであることを示します。実際、`nil` と `cons` コンストラクタの宣言の中で `Vect` が3回出現すると、それらすべてに一貫して `α` が第一引数として指定されますが、第二引数はそれぞれ異なります。 + + + +`nil` の宣言では、これが `Vect α 0` 型のコンストラクタであることが示されています。これは、ちょうど `List String` が期待されているコンテキストにおいて `[1, 2, 3]` がエラーになるように、`Vect String 3` 型が期待されているコンテキストで `Vect.nil` を使うと型エラーになるということです。 + ```lean {{#example_in Examples/DependentTypes.lean nilNotLengthThree}} ``` ```output error {{#example_out Examples/DependentTypes.lean nilNotLengthThree}} ``` + + +この例においての `0` と `3` の食い違いはこれら自体が型でないにもかかわらず、一般的な型のミスマッチとまったく同じ役割を果たします。 + +添字族は異なる添字の値によって使用できるコンストラクタを変えることができることから、型の **族** と呼ばれます。ある意味では、添字族は型ではありません;むしろ、関連した型のあつまりであり、添字の選択によってそのあつまりから型を選びます。 `Vect` において添字 `5` を選ぶことによってコンストラクタは `const` のみ、また添字 `0` を選ぶことによって `nil` のみがそれぞれ利用可能となります。 + + + +添字が不明である場合(例えば変数である等)、それが明らかになるまではどのコンストラクタも利用できません。長さとして `n` を用いると `Vect.nil` と `Vect.cons` のどちらも使用できません。というのも `n` が `0` と `n + 1` のどちらにマッチする `Nat` を表すのかを知るすべがないからです: + ```lean {{#example_in Examples/DependentTypes.lean nilNotLengthN}} ``` @@ -44,61 +79,120 @@ Using `n` for the length allows neither `Vect.nil` nor `Vect.cons`, because ther {{#example_out Examples/DependentTypes.lean consNotLengthN}} ``` + + +型の一部でリストの長さを保持することはその型がより有益になることを意味します。例えば、`Vect.replicate` は与えられた値のコピー回数をもとに `Vect` を作る関数です。これを正確に表す型は以下のようになります: + ```lean {{#example_in Examples/DependentTypes.lean replicateStart}} ``` + + +引数 `n` が結果の長さとして現れています。アンダースコアによるプレースホルダに紐づいたメッセージでは現在のタスクが説明されています: + ```output error {{#example_out Examples/DependentTypes.lean replicateStart}} ``` + + +添字族を扱う際に、コンストラクタはLeanがそのコンストラクタの添字が期待される型の添字と一致することを確認できた場合にのみ適用可能です。しかし、どちらのコンストラクタも `n` にマッチする添字を持っていません。`nil` は `Nat.zero` に、`cons` は `Nat.succ` にマッチします。型エラーの例のように、変数 `n` は関数に引数として渡される `Nat` によってこのどちらかを表す可能性があります。そこで解決策としてパターンマッチを使用してありうる両方のケースを考慮することができます: + ```lean {{#example_in Examples/DependentTypes.lean replicateMatchOne}} ``` + + +`n` は期待される型に含まれるため、`n` のパターンマッチによってマッチする2つのケースでの期待される型が **絞り込まれます** 。1つ目のアンダースコアでは、期待される型は `Vect α 0` になります: + ```output error {{#example_out Examples/DependentTypes.lean replicateMatchOne}} ``` + + +2つ目のアンダースコアでは `Vect α (k + 1)` になります: + ```output error {{#example_out Examples/DependentTypes.lean replicateMatchTwo}} ``` + + +パターンマッチが値の構造を解明することに加えてプログラムの型を絞り込む場合、これは **依存パターンマッチ** (dependent pattern matching)と呼ばれます。 + + +精練された型ではコンストラクタを適用することができます。1つ目のアンダースコアでは `Vect.nil` が、2つ目では `Vect.cons` がそれぞれマッチします: + ```lean {{#example_in Examples/DependentTypes.lean replicateMatchFour}} ``` + + +`.cons` の中にある1つ目のアンダースコアは `α` 型でなければなりません。ここで利用可能な `α` は存在しており、まさに `x` のことです: + ```output error {{#example_out Examples/DependentTypes.lean replicateMatchFour}} ``` + + +2つ目のアンダースコアは `Vect α k` であるべきであり、これは `replicate` を再帰的に呼び出すことで生成できます: + ```output error {{#example_out Examples/DependentTypes.lean replicateMatchFive}} ``` + + +以下が `replicate` の最終的な定義です: + ```lean {{#example_decl Examples/DependentTypes.lean replicate}} ``` + + +関数を書く間の支援に加えて、`Vect.replicate` の情報に富んだ型によってクライアントコードはソースコードを読まなくても多くの予期しない関数である可能性を除外することができます。リスト用にした `replicate` では、間違った長さのリストを生成する可能性があります: + ```lean {{#example_decl Examples/DependentTypes.lean listReplicate}} ``` + + +しかし、このミスは `Vect.replicate` では型エラーになります: + ```lean {{#example_in Examples/DependentTypes.lean replicateOops}} ``` @@ -107,29 +201,54 @@ However, making this mistake with `Vect.replicate` is a type error: ``` + + +`List.zip` は2つのリストに対して、1つ目のリストの最初の要素と2つ目のリストの最初の要素をペアに、1つ目のリストの2番目の要素と2つ目のリストの2番目の要素をペアに、という具合に結合していく関数です。`List.zip` はオレゴン州の3つの最高峰とデンマークの3つの最高峰をペアにすることができます: + ```lean {{#example_in Examples/DependentTypes.lean zip1}} ``` + + +結果は3つのペアを含むリストです: + ```lean {{#example_out Examples/DependentTypes.lean zip1}} ``` + + +リストの長さが異なる場合にどうすればいいかはやや不明確です。多くの言語のように、Leanは長い方のリストの余分な要素を無視する実装を選んでいます。例えば、オレゴン州の5つの最高峰の高度とデンマークの3つの最高峰の高度を組み合わせると3つのペアができます。具体的には: + ```lean {{#example_in Examples/DependentTypes.lean zip2}} ``` + + +は以下に評価されます。 + ```lean {{#example_out Examples/DependentTypes.lean zip2}} ``` + + +このアプローチでは必ず答えが得られる点が便利である一方、意図せず長さの異なるリストを渡した際に情報が捨てられてしまう危険性があります。F#は別のアプローチをとっています:以下の `fsi` セッションで見られるように、F#での `List.zip` では長さが異なる場合は例外を投げます: + ```fsharp > List.zip [3428.8; 3201.0; 3158.5; 3075.0; 3064.0] [170.86; 170.77; 170.35];; ``` @@ -143,56 +262,120 @@ list2 is 2 elements shorter than list1 (Parameter 'list2') at .$FSI_0006.main@() Stopped due to error ``` + + +これによって誤って情報が破棄される事態を避けられますが、その代わりにプログラムをクラッシュさせてしまうことにはそれなりの困難が伴います。Leanにおいて `Option` や `Except` モナドを使うような同じアプローチをすると安全性に見合わない負担が発生します。 + + +しかし `Vect` を使えば、両方の引数が同じ長さであることを要求する型を持つバージョンの `zip` を書くことができます: + ```lean {{#example_decl Examples/DependentTypes.lean VectZip}} ``` + + +この定義は両方の引数が `Vect.nil` であるか、`Vect.cons` である場合のパターンを持っているだけであり、Leanは `List` に対する以下の同様の定義の結果のような `missing cases` エラーを出さずに定義を受け入れます: + ```lean {{#example_in Examples/DependentTypes.lean zipMissing}} ``` ```output error {{#example_out Examples/DependentTypes.lean zipMissing}} ``` + + +これは最初のパターンで使用されているコンストラクタ `nil` または `cons` が長さ `n` に関する型チェッカが持つ情報を **洗練** させるからです。最初のパターンが `nil` の場合、型チェッカは追加で長さが `0` であることを判断でき、その結果2つ目のパターンは `nil` 以外ありえなくなります。同様に、最初のパターンが `cons` の場合、型チェッカは `Nat` のある `k` に対して `k + 1` の長さだったと判断することができ、2つ目のパターンは `cons` 以外ありえなくなります。実際、`nil` と `cons` を一緒に使うケースを追加すると、長さが一致しないために型エラーになります: + ```lean {{#example_in Examples/DependentTypes.lean zipExtraCons}} ``` ```output error {{#example_out Examples/DependentTypes.lean zipExtraCons}} ``` + + +`n` を明示的な引数にすることで、長さについての詳細化をより見やすくすることができます: + ```lean {{#example_decl Examples/DependentTypes.lean VectZipLen}} ``` + +## 演習問題 + + + +依存型を使ったプログラミングの感覚をつかむには、この節の演習問題は非常に重要です。各演習問題において、コードを試しながら、型チェッカがどのミスを捉え、どのミスを捉えられないかを試行錯誤してみてください。これはエラーメッセージに対する感覚を養う良い方法でもあります。 + + * オレゴン州の3つの最高峰とデンマークの3つの最高峰を組み合わせたときに `Vect.zip` が正しい答えを出すかダブルチェックしてください。`Vect` には `List` のような糖衣構文がないので、まず `oregonianPeaks : Vect String 3` と `danishPeaks : Vect String 3` を定義しておくと便利でしょう。 + + + + * `(α → β) → Vect α n → Vect β n` 型を持つ `Vect.map` 関数を定義してください。 + + + * `Vect` の各要素を結合する際に関数を適用する関数 `Vect.zipWith` を定義してください。これは `(α → β → γ) → Vect α n → Vect β n → Vect γ n` 型になるべきです。 + + * 要素がペアである `Vect` を `Vect` のペアに分割する関数 `Vect.unzip` を定義してください。これは `Vect (α × β) n → Vect α n × Vect β n` 型になるべきです。 + + + + * `Vect` の **末尾** に要素を追加する `Vect.snoc` を定義してください。これは `Vect α n → α → Vect α (n + 1)` 型になるべきで、`{{#example_in Examples/DependentTypes.lean snocSnowy}}` は `{{#example_out Examples/DependentTypes.lean snocSnowy}}` を返すべきです。`snoc` という名前は伝統的な関数型プログラミングのダジャレで、`cons` を逆さにしたものです。 + + + * `Vect` の順序を逆にする関数 `Vect.reverse` を書いてください。 + + + * 次の型を持つ関数 `Vect.drop` を定義してください:`(n : Nat) → Vect α (k + n) → Vect α k` 。この関数が正しく動作することを検証するには `{{#example_in Examples/DependentTypes.lean ejerBavnehoej}}` が `{{#example_out Examples/DependentTypes.lean ejerBavnehoej}}` を返すことを確認することで行えます。 + + + * 型 `(n : Nat) → Vect α (k + n) → Vect α n` で `Vect` の先頭から `n` 個の要素を返すの関数 `Vect.take` を定義してください。例をあげて動作することを確認もしてください。