From 76e397a20844ddcf6c80b0ef0db23a18bef0e339 Mon Sep 17 00:00:00 2001 From: s-taiga Date: Sun, 1 Sep 2024 00:37:47 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=E7=BF=BB=E8=A8=B3=E9=96=8B=E5=A7=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- functional-programming-lean/src/SUMMARY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functional-programming-lean/src/SUMMARY.md b/functional-programming-lean/src/SUMMARY.md index 8cba864..1eebf60 100644 --- a/functional-programming-lean/src/SUMMARY.md +++ b/functional-programming-lean/src/SUMMARY.md @@ -63,7 +63,7 @@ - [プログラミング・証明・パフォーマンス](programs-proofs.md) - [末尾再帰](programs-proofs/tail-recursion.md) - [同値の証明](programs-proofs/tail-recursion-proofs.md) - - [Arrays and Termination](programs-proofs/arrays-termination.md) + - [配列と関数の停止](programs-proofs/arrays-termination.md) - [More Inequalities](programs-proofs/inequalities.md) - [Safe Array Indices](programs-proofs/fin.md) - [Insertion Sort and Array Mutation](programs-proofs/insertion-sort.md) From a99aa37765ff3b3470de14ef6200f2daf74243a5 Mon Sep 17 00:00:00 2001 From: s-taiga Date: Sun, 1 Sep 2024 12:11:29 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=E7=BF=BB=E8=A8=B3=E5=AE=8C=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/programs-proofs/arrays-termination.md | 260 ++++++++++++++++++ 1 file changed, 260 insertions(+) diff --git a/functional-programming-lean/src/programs-proofs/arrays-termination.md b/functional-programming-lean/src/programs-proofs/arrays-termination.md index 8595bc4..68f5bc5 100644 --- a/functional-programming-lean/src/programs-proofs/arrays-termination.md +++ b/functional-programming-lean/src/programs-proofs/arrays-termination.md @@ -1,246 +1,506 @@ + +# 配列と関数の停止 + + + +効率的なコードを書くためには、適切なデータ構造を選択することが重要です。連結リストには利点があります:アプリケーションによっては、リストの末尾を共有できることが非常に重要な場合もあります。しかし、可変長のシーケンシャルなデータコレクションを使用する場合、メモリのオーバーヘッドが少なく、局所性にも優れた配列の方が大体の場合において適しています。 + + +しかし、配列はリストに対して2つの欠点を持っています: + + + 1. 配列はパタンマッチではなくインデックスによってアクセスされます。これにあたっては安全性を維持するために [証明の義務](../props-proofs-indexing.md) が課せられます。 + 2. 配列全体を左から右に処理するループは末尾再帰関数ですが、呼び出すたびに減少する引数を持ちません。 + + + +配列を効果的に使うには、配列のインデックスが範囲内にあることをLeanに証明する方法と、配列のインデックスが配列のサイズに到達した際にプログラムが終了することの証明の方法を知る必要があります。これらはどちらも命題の等式ではなく、不等式の命題を使って表現されます。 + +## 不等式 + + + +型によって順序の概念が異なるため、不等式は `LE` と `LT` と呼ばれる2つの型クラスによって管理されています。[標準型クラス](../type-classes/standard-classes.md#equality-and-ordering) の節での表は、これらのクラスが以下の構文とどのように関係しているかを説明します: + + +| 式 | 脱糖後の式 | 型クラス名 | |------------|------------|------------| | `{{#example_in Examples/Classes.lean ltDesugar}}` | `{{#example_out Examples/Classes.lean ltDesugar}}` | `LT` | | `{{#example_in Examples/Classes.lean leDesugar}}` | `{{#example_out Examples/Classes.lean leDesugar}}` | `LE` | | `{{#example_in Examples/Classes.lean gtDesugar}}` | `{{#example_out Examples/Classes.lean gtDesugar}}` | `LT` | | `{{#example_in Examples/Classes.lean geDesugar}}` | `{{#example_out Examples/Classes.lean geDesugar}}` | `LE` | + + +言い換えると、ある型において `<` と `≤` 演算子の意味をカスタマイズすることができ、`>` と `≥` は `<` と `≤` から意味を派生させることができます。クラス `LT` と `LE` は `Bool` 値ではなく命題を返すメソッドを持ちます: + ```lean {{#example_decl Examples/ProgramsProofs/Arrays.lean less}} ``` + + +`LE` の `Nat` についてのインスタンスは `Nat.le` に委譲されています: + ```lean {{#example_decl Examples/ProgramsProofs/Arrays.lean LENat}} ``` + + +`Nat.le` を定義するにはまだ紹介していないLeanの特徴が必要です:帰納的に定義された関係です。 + +### 帰納的に定義された命題・述語・関係 + + + +`Nat.le` は **帰納的に定義された関係** (inductively-defined relation)です。`inductive` は新しいデータ型を作ることに使われるのと同じように、これは新しい命題を作るのにも使われます。命題が引数を取る場合、これは **述語** (predicate)とよばれ、引数に対して真であるものが全てでなくいくつかだけだったりします。複数の引数を取る命題は **関係** (relation)と呼ばれます。 + + +帰納的に定義された命題の各コンストラクタは、その命題を証明するための方法です。言い換えれば、命題の宣言はそれが真であることを証明する様々な形式を記述しています。1つのコンストラクタを持つ引数のない命題の証明は非常に簡単です: + ```lean {{#example_decl Examples/ProgramsProofs/Arrays.lean EasyToProve}} ``` + + +この証明は以下のコンストラクタから構成されます: + ```lean {{#example_decl Examples/ProgramsProofs/Arrays.lean fairlyEasy}} ``` + + +命題 `True` もまた簡単に証明できますが、実は `EasyToProve` と同じように定義されています: + ```lean {{#example_decl Examples/ProgramsProofs/Arrays.lean True}} ``` + +引数を取らない帰納的に定義された命題は帰納的に定義されたデータ型よりは面白みに欠けます。というのもデータはそれ自体の正しさに興味があるからです。例えば自然数 `3` と `35` は異なりますし、ピザを3枚注文して、30分後に35枚届いたら腹が立つでしょう。命題のコンストラクタはその命題が真になりうる方法を記述していますが、命題が証明されてしまえば、**どの** コンストラクタが使われたかを知る必要はありません。これが `Prop` の宇宙で興味深い帰納的に定義される型のほとんどが引数を取る理由です。 + + + +帰納的に定義された述語 `IsThree` はその引数が3であることを示します: + ```lean {{#example_decl Examples/ProgramsProofs/Arrays.lean IsThree}} ``` + +ここで使用されるメカニズムは [`HasCol` のような添字族](../dependent-types/typed-queries.md#column-pointers) と同様ですが、結果として得られる型は利用可能なデータではなく証明可能な命題です。 + + + +この述語を使って、3が本当に3であることを証明できます: + ```lean {{#example_decl Examples/ProgramsProofs/Arrays.lean threeIsThree}} ``` + + +同様に、`IsFive` は引数が `5` であることを示す述語です: + ```lean {{#example_decl Examples/ProgramsProofs/Arrays.lean IsFive}} ``` + + +もしある数値が3であるなら、これに2を加えると結果は5になるはずです。これは定理の文として表すことができます: + ```leantac {{#example_in Examples/ProgramsProofs/Arrays.lean threePlusTwoFive0}} ``` + + +結果のゴールは関数型を持ちます: + ```output error {{#example_out Examples/ProgramsProofs/Arrays.lean threePlusTwoFive0}} ``` + + +そのため、`intro` タクティクによって引数を仮定に変換できます: + ```leantac {{#example_in Examples/ProgramsProofs/Arrays.lean threePlusTwoFive1}} ``` ```output error {{#example_out Examples/ProgramsProofs/Arrays.lean threePlusTwoFive1}} ``` + + +`n` が3であるという仮定を使って、`IsFive` のコンストラクタを使って証明を完成させることができるはずです: + ```leantac {{#example_in Examples/ProgramsProofs/Arrays.lean threePlusTwoFive1a}} ``` + + +しかし、これはエラーになります: + ```output error {{#example_out Examples/ProgramsProofs/Arrays.lean threePlusTwoFive1a}} ``` + + +このエラーは `n + 2` が `5` と定義上等しくないために起こります。通常の関数定義では、仮定 `three` への依存パターンマッチによって `n` を `3` に絞り込むことができます。依存パターンマッチに相当するタクティクは `cases` で、これは `induction` と似た構文を持っています: + ```leantac {{#example_in Examples/ProgramsProofs/Arrays.lean threePlusTwoFive2}} ``` + + +これによって得られるケースにおいて `n` は `3` に絞り込まれます: + ```output error {{#example_out Examples/ProgramsProofs/Arrays.lean threePlusTwoFive2}} ``` + + +`3 + 2` は `5` に定義上等しいため、これでこのコンストラクタを適用可能です: + ```leantac {{#example_decl Examples/ProgramsProofs/Arrays.lean threePlusTwoFive3}} ``` + + +標準的な偽についての命題 `False` はコンストラクタを持ちません。これによって偽を証明するために直接根拠を提供することは不可能になっています。`False` の根拠を提供する唯一の方法は、仮定自体が不可能である場合です。これは `nomatch` を使って、到達不可能であることが型システムによってわかっているコードをマークすることができることと同様です。[証明に関する最初の幕間](../props-proofs-indexing.md#connectives) で説明したように、否定 `Not A` は `A → False` の略です。`Not A` は `¬A` と書くこともできます。 + + +4は3ではありません: + ```leantac {{#example_in Examples/ProgramsProofs/Arrays.lean fourNotThree0}} ``` + + +証明の初期のゴールは `Not` を含みます: + ```output error {{#example_out Examples/ProgramsProofs/Arrays.lean fourNotThree0}} ``` + + +これが実際には関数型であることは `simp` を使って明らかにできます: + ```leantac {{#example_in Examples/ProgramsProofs/Arrays.lean fourNotThree1}} ``` ```output error {{#example_out Examples/ProgramsProofs/Arrays.lean fourNotThree1}} ``` + + +ゴールは関数型であるため、`intro` を使って引数を仮定に変換することができます。`intro` は `Not` の定義をのものを展開することができるため、`simp` を使う必要はありません。 + ```leantac {{#example_in Examples/ProgramsProofs/Arrays.lean fourNotThree2}} ``` ```output error {{#example_out Examples/ProgramsProofs/Arrays.lean fourNotThree2}} ``` + + +この証明では、`cases` タクティクによってゴールが即座に解決されます: + ```leantac {{#example_decl Examples/ProgramsProofs/Arrays.lean fourNotThreeDone}} ``` + + +`Vect String 2` のパターンマッチに `Vect.nil` のケースを含める必要がないように、`IsThree 4` のケースによる証明に `isThree` のケースを含める必要はありません。 + +### 整数の不等式 + + + +`Nat.le` の定義にはパラメータと添字が含まれます: + ```lean {{#example_decl Examples/ProgramsProofs/Arrays.lean NatLe}} ``` + +パラメータ `n` は小さい方の数であり、添字は `n` 以上となるはずの数です。両方の数値が等しい場合は `refl` コンストラクタを使用し、添字が `n` より大きい場合は `step` コンストラクタを使用します。 + + + +根拠の観点からすると、 \\( n \leq k \\) の証明は \\( n + d = m \\) となるような \\( d \\) である数を見つけることから構成されます。Leanでは、この証明は `Nat.le.step` の \\( d \\) についてのインスタンスでラップされた `Nat.le.refl` コンストラクタから構成されます。各 `step` コンストラクタはその添字引数に1を加えるため、 \\( d \\) 回の `step` コンストラクタは大きい数に \\( d \\) を加算します。例えば、4が7以下である根拠は `refl` を囲む3つの `step` で構成されます: + ```lean {{#example_decl Examples/ProgramsProofs/Arrays.lean four_le_seven}} ``` + + +未満関係は左の数字に1を足すことで定義されます: + ```lean {{#example_decl Examples/ProgramsProofs/Arrays.lean NatLt}} ``` + + +4が7未満であるという根拠は `refl` の周りにある2つの `step` で構成されます: + ```lean {{#example_decl Examples/ProgramsProofs/Arrays.lean four_lt_seven}} ``` + + +これは `4 < 7` が `5 ≤ 7` と等しい訳です。 + +## 停止性の証明 + + + +関数 `Array.map` は配列を関数で変換し、入力配列の各要素に関数を適用した結果を含んだ新しい配列を返します。これを末尾再帰関数として書くと、出力配列をアキュムレータに渡すような関数に委譲するといういつものパターンになります。アキュムレータは空の配列で初期化されます。アキュムレータを渡す補助関数は配列の現在のインデックスを追跡する引数を取り、これは `0` から始まります: + ```lean {{#example_decl Examples/ProgramsProofs/Arrays.lean ArrayMap}} ``` + + +補助関数は各繰り返しにて、インデックスがまだ範囲内にあるかどうかをチェックする必要があります。もし範囲内であれば、変換された要素をアキュムレータの最後に追加し、インデックスを `1` だけ加算して再度ループする必要があります。そうでない場合は終了してアキュムレータを返します。このコードの初期実装ではLeanが配列のインデックスが有効であることを証明できないため失敗します: + ```lean {{#example_in Examples/ProgramsProofs/Arrays.lean mapHelperIndexIssue}} ``` ```output error {{#example_out Examples/ProgramsProofs/Arrays.lean mapHelperIndexIssue}} ``` + + +しかし、この条件式ではすでに配列のインデックスの有効性が要求する正確な条件(すなわち `i < arr.size` )をチェックしています。`if` に名前を追加することで、配列のインデックスのタクティクが使用できる仮定が追加されるため、問題は解決します: + ```lean {{#example_in Examples/ProgramsProofs/Arrays.lean arrayMapHelperTermIssue}} ``` + + +しかし、Leanはこの修正されたプログラムを受け付けません。なぜなら、再帰呼び出しが入力コンストラクタの1つの引数に対して行われていないからです。実際、アキュムレータもインデックスも縮小するのではなく、むしろ増大します: + ```output error {{#example_out Examples/ProgramsProofs/Arrays.lean arrayMapHelperTermIssue}} ``` + + +とはいえこの関数は停止するため、単に `partial` とマークするのはあまりに残念です。 + + +なぜ `arrayMapHelper` は停止するのでしょうか?各繰り返しはインデックス `i` が配列 `arr` の範囲内にあるかどうかをチェックします。もし真であれば、`i` が加算され、ループが繰り返されます。そうでなければプログラムは終了します。`arr.size` は有限な値であるため、`i` を加算できる回数も有限回です。関数を呼び出すたびに引数が減らない場合でも、`arr.size -i` は0に向かって減っていきます。 + + +Leanは定義の最後に `termination_by` 節を記述することで、停止について別の式を使うように指示することができます。`termination_by` 節には2つの要素があります:関数の引数の名前と、その名前を使っており各呼び出しのたびに値が減るべき式です。`arrayMapHelper` の場合、最終的な定義は以下のようになります: + ```lean {{#example_decl Examples/ProgramsProofs/Arrays.lean ArrayMapHelperOk}} ``` + + +同様の停止証明を使って、ある真偽値関数を満たす配列の最初の要素を見つけ、その要素をインデックスの両方を返す関数 `Array.find` を書くことができます: + ```lean {{#example_decl Examples/ProgramsProofs/Arrays.lean ArrayFind}} ``` + + +ここでもまた、`i` が増加すると `arr.size - i` が減少するのでここでも補助関数は終了します: + ```lean {{#example_decl Examples/ProgramsProofs/Arrays.lean ArrayFindHelper}} ``` + +すべての停止引数がこれほど単純であるとは限りません。しかし、関数の引数に基づいで呼び出しのたびに減少する式を特定するという基本的な構造は、全ての停止証明で発生します。時には関数が停止する理由を解明するために想像力が要求されることもありますし、またLeanが停止引数を受け入れるために追加の証明を必要とすることもあります。 + +## 演習問題 + + + + * 末尾再帰のアキュムレータを渡す関数と `termination_by` 節を使って配列に対して `ForM (Array α)` インスタンスを定義してください。 + * `termination_by` を **必要としない** 末尾再帰のアキュムレータを渡す関数を使用して配列を反転させる関数を実装してください。 + * 恒等モナドの `for ... in ...` ループを使って `Array.map` ・`Array.find` ・`ForM` インスタンスを再実装し、結果のコードを比較してください。 + * 配列の反転を恒等モナドの `for ... in ...` ループを使って再実装してください。またそれを末尾再帰版と比較してください。 From 815280fba7fe025132a77b529a8d487e64d601a0 Mon Sep 17 00:00:00 2001 From: s-taiga Date: Sun, 1 Sep 2024 12:39:45 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=E8=A8=B3=E6=B3=A8=E8=BF=BD=E8=A8=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/programs-proofs/arrays-termination.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/functional-programming-lean/src/programs-proofs/arrays-termination.md b/functional-programming-lean/src/programs-proofs/arrays-termination.md index 68f5bc5..96c5b7a 100644 --- a/functional-programming-lean/src/programs-proofs/arrays-termination.md +++ b/functional-programming-lean/src/programs-proofs/arrays-termination.md @@ -346,7 +346,7 @@ Each `step` constructor adds one to its index argument, so \\( d \\) `step` cons For example, evidence that four is less than or equal to seven consists of three `step`s around a `refl`: --> -根拠の観点からすると、 \\( n \leq k \\) の証明は \\( n + d = m \\) となるような \\( d \\) である数を見つけることから構成されます。Leanでは、この証明は `Nat.le.step` の \\( d \\) についてのインスタンスでラップされた `Nat.le.refl` コンストラクタから構成されます。各 `step` コンストラクタはその添字引数に1を加えるため、 \\( d \\) 回の `step` コンストラクタは大きい数に \\( d \\) を加算します。例えば、4が7以下である根拠は `refl` を囲む3つの `step` で構成されます: +根拠の観点からすると、 \\( n \leq k \\) の証明は \\( n + d = k \\) [^1]となるような \\( d \\) である数を見つけることから構成されます。Leanでは、この証明は `Nat.le.step` の \\( d \\) についてのインスタンスでラップされた `Nat.le.refl` コンストラクタから構成されます。各 `step` コンストラクタはその添字引数に1を加えるため、 \\( d \\) 回の `step` コンストラクタは大きい数に \\( d \\) を加算します。例えば、4が7以下である根拠は `refl` を囲む3つの `step` で構成されます: ```lean {{#example_decl Examples/ProgramsProofs/Arrays.lean four_le_seven}} @@ -504,3 +504,5 @@ Sometimes, creativity can be required in order to figure out just why a function * `termination_by` を **必要としない** 末尾再帰のアキュムレータを渡す関数を使用して配列を反転させる関数を実装してください。 * 恒等モナドの `for ... in ...` ループを使って `Array.map` ・`Array.find` ・`ForM` インスタンスを再実装し、結果のコードを比較してください。 * 配列の反転を恒等モナドの `for ... in ...` ループを使って再実装してください。またそれを末尾再帰版と比較してください。 + +[^1]: 原文では \\( n + d = m \\) となっていたが、mとkの書き間違いと思われる。 \ No newline at end of file