From 47acaf6aeafd571b580cddb69510b3dc97c2f3e8 Mon Sep 17 00:00:00 2001 From: s-taiga <56785231+s-taiga@users.noreply.github.com> Date: Fri, 6 Sep 2024 00:57:41 +0900 Subject: [PATCH] =?UTF-8?q?10.6=E7=AB=A0=20insertion-sort=20(#84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 翻訳開始 * 翻訳完了 * セルフレビュー --- functional-programming-lean/src/SUMMARY.md | 2 +- .../src/programs-proofs/insertion-sort.md | 355 ++++++++++++++++++ 2 files changed, 356 insertions(+), 1 deletion(-) diff --git a/functional-programming-lean/src/SUMMARY.md b/functional-programming-lean/src/SUMMARY.md index 4ffa3759..acf5fd30 100644 --- a/functional-programming-lean/src/SUMMARY.md +++ b/functional-programming-lean/src/SUMMARY.md @@ -66,7 +66,7 @@ - [配列と関数の停止](programs-proofs/arrays-termination.md) - [その他の不等式](programs-proofs/inequalities.md) - [安全な配列のインデックス](programs-proofs/fin.md) - - [Insertion Sort and Array Mutation](programs-proofs/insertion-sort.md) + - [挿入ソートと配列の更新](programs-proofs/insertion-sort.md) - [Special Types](programs-proofs/special-types.md) - [Summary](programs-proofs/summary.md) diff --git a/functional-programming-lean/src/programs-proofs/insertion-sort.md b/functional-programming-lean/src/programs-proofs/insertion-sort.md index 062049c9..9a325b0d 100644 --- a/functional-programming-lean/src/programs-proofs/insertion-sort.md +++ b/functional-programming-lean/src/programs-proofs/insertion-sort.md @@ -1,87 +1,177 @@ + +# 挿入ソートと配列の更新 + + + +挿入ソートはソートアルゴリズムとしては最悪計算時間の複雑性において最適ではありませんが、それでもいくつもの有用な性質を持ちます: + + + + * 実装および理屈がシンプルで直観的であること + * in-placeアルゴリズムであり、実行時に追加の領域を必要としないこと + * 安定ソートである + * 入力がソート済みである場合は高速 + +特にin-placeアルゴリズムである点はLeanにおいてメモリ管理の方法から有用です。いくつかのケースでは、通常では配列のコピーの操作は更新に最適化されます。これは配列内の要素の交換も含みます。 + + + +JavaScriptやJVM、.NETを含むメモリを動的に管理するほとんどの言語とランタイムでは、ガベージコレクションの追跡を用います。メモリを再要求する必要がある場合、システムはいくつかの **ルート** (コールスタックやグローバル値など)から開始し、再帰的にポインタを追いかけることで到達できる値を決定します。到達できない値はすべて割り当て解除され、メモリが解放されます。 + +ガベージコレクションの追跡の代替手段として、Python・Swift・Leanを含む多くの言語では参照カウントが用いられます。参照カウントを持つシステムでは、メモリ内のオブジェクトはそれへの参照がいくつあるかを追跡するフィールドを持ちます。新しい参照が確立されるとカウントが1増えます。参照が存在しなくなるとカウントが1減ります。カウントが0になると、オブジェクトは直ちに解放されます。 + + + +参照カウントはガベージコレクションの追跡に対して1つの大きな欠点があります:循環参照によるメモリーリークです。あるオブジェクト \\( A \\) がオブジェクト \\( B \\) を参照し、オブジェクト \\( B \\) がオブジェクト \\( A \\) を参照している場合、たとえそのプログラム内で \\( A \\) と \\( B \\) に対して他に参照が無くてもこれらは決して解放されません。循環参照は制御されていない再帰か、変更可能な参照のどちらかに起因します。Leanはどちらもサポートしていないため、循環参照を構築することは不可能です。 + +参照カウントによってLeanのランタイムシステムのデータ構造の割り当て・解放のプリミティブが、あるオブジェクトの参照カウントが0になるかどうか、既存のオブジェクトを新しく割り当てることなく再利用するかをチェックできるようになります。これは大きな配列を扱う際には特に重要になります。 + + +Leanの配列についての挿入ソートの実装は以下の性質を満たさなければなりません: + + + 1. Leanがこの関数を `partial` 注釈無しで許容するべきである + 2. 他に参照がない配列が渡された場合、新しい配列を確保するのではなく、in-placeで配列を変更すること + + + +最初の性質は簡単にチェックできます:もしLeanがその定義を受け入れるならば満足します。しかし2つ目は検証方法が必要になります。Leanには下記のシグネチャを持つ `dbgTraceIfShared` という関数が組み込まれています: + ```lean {{#example_in Examples/ProgramsProofs/InsertionSort.lean dbgTraceIfSharedSig}} ``` ```output info {{#example_out Examples/ProgramsProofs/InsertionSort.lean dbgTraceIfSharedSig}} ``` + + +これは引数として文字列と値を受け取り、もしその値に複数の参照があれば渡された文字列を使用したメッセージを標準エラーに出力し、値を返します。これは厳密には純粋関数ではありません。しかし、これは関数が実際にメモリを割り当てたりコピーしたりせずにメモリを再利用できるかどうかをチェックするために、開発中にのみ使用することを意図しています。 + + +`dbgTraceIfShared` の使い方を学ぶにあたって、`#eval` がコンパイルされたコード中のものよりも多くの値が共有されていると報告することを知ることが大事です。これは混乱を招く恐れがあります。エディタで実験するよりも、`lake` を使って実行ファイルをビルドすることが重要です。 + +挿入ソートは2つのループから構成されます。外側のループはポインタをソート対象の配列中で左から右に動かします。各繰り返しの後に、配列内のポインタの左側はソートされる一方で右側は未ソートとなります。内側のループはポインタが指す要素を受け取り、それを適切な場所が見つかりループの不変条件が復元するまで左へと移動させます。言い換えると、各繰り返しは配列の次の要素をソート済み領域の適切な位置に挿入します。 + + + +## 内側のループ + + +挿入ソートの内側のループは配列と挿入する要素を引数として受け取る末尾再帰関数として実装することができます。挿入されるこの要素は、左側の要素の方が小さいときか配列の先頭にたどり着くまで繰り返し左の要素と入れ替えられ続けます。内側のループは配列のインデックスとして使われる `Fin` の中の `Nat` に対して構造的に再帰的です: + ```leantac {{#example_decl Examples/ProgramsProofs/InsertionSort.lean insertSorted}} ``` + +インデックス `i` が `0` ならば、ソート済み領域に挿入されるこの要素はすでに領域の先頭であり最小の値です。インデックスが `i' + 1` ならば、`i'` 番目の要素は `i` 番目の要素と比較されるべきです。ここで `i` は `Fin arr.size` 型ですが、`i'` は `i` の `val` フィールドから得られたものなのでただの `Nat` であることに注意してください。したがって `i'` を `arr` のインデックスとして使う前に `i' < arr.size` を証明する必要があります。 + + + +`i' < arr.size` の証明から `have` 式を取り除くと以下のゴールが現れます: + ```output error {{#example_out Examples/ProgramsProofs/InsertionSort.lean insertSortedNoProof}} ``` + + +`Nat.lt_of_succ_lt` はLeanの標準ライブラリにある定理です。このシグネチャは `{{#example_in Examples/ProgramsProofs/InsertionSort.lean lt_of_succ_lt_type}}` で確認でき、以下のようになります: + ```output info {{#example_out Examples/ProgramsProofs/InsertionSort.lean lt_of_succ_lt_type}} ``` + + +言い換えると、これは `n + 1 < m` ならば `n < m` であるということを述べています。`simp` に `*` を渡すことで最終的な証明を得るために `Nat.lt_of_succ_lt` に `i` の `isLt` フィールドが組み合わせられます。 + +`i'` が挿入される要素の左の要素を調べるために使えるようになったため、これら2つの要素を調べて比較します。左の要素が挿入される要素以下であれば、ループは終了し、ループ不変条件が復元されます。もし左の要素が挿入される要素より大きければ、要素が入れ替わり内側のループが再び始まります。`Array.swap` は `Fin` である両方のインデックスと、`have` で成立した `i' < arr.size` を利用している `by assumption` を受け取ります。次のループで調べるインデックスも `i'` ですが、この場合 `by assumption` だけでは不十分です。なぜなら、この証明は2つの要素を入れ替えた結果ではなく、もとの配列 `arr` に対して書かれたものだからです。`simp` タクティクのデータベースには、配列の2つの要素を入れ替えてもサイズが変わらないという事実が含まれており、`[*]` 引数は `have` によって導入された仮定を追加で使用するように指示します。 + + + +## 外側のループ + + +挿入ソートの外側のループでは、ポインタを左から右へ動かし、各繰り返しで `insertSorted` を呼び出してポインタの要素を配列の正しい位置に挿入します。このループの基本形は `Array.map` の実装に似ています: + ```lean {{#example_in Examples/ProgramsProofs/InsertionSort.lean insertionSortLoopTermination}} ``` + + +その結果生じるエラーも `termination_by` 節を `Array.map` に指定しない場合に生じるものと同じです。というのも再帰呼び出しのたびに減少する引数がないからです: + ```output error {{#example_out Examples/ProgramsProofs/InsertionSort.lean insertionSortLoopTermination}} ``` + + +停止についての証明を構築する前に、`partial` 修飾子を使って定義をテストし、これが期待される答えを返すことを確認しておくと便利です: + ```lean {{#example_decl Examples/ProgramsProofs/InsertionSort.lean partialInsertionSortLoop}} ``` @@ -119,19 +231,33 @@ Before constructing the termination proof, it can be convenient to test the defi {{#example_out Examples/ProgramsProofs/InsertionSort.lean insertionSortPartialTwo}} ``` + + +### 停止性 + + +ここでもまた、インデックスと配列のサイズの差が各再帰呼び出しのたびに小さくなっていくため関数は停止します。しかし、今回Leanはこの `termination_by` を受理しません: + ```lean {{#example_in Examples/ProgramsProofs/InsertionSort.lean insertionSortLoopProof1}} ``` ```output error {{#example_out Examples/ProgramsProofs/InsertionSort.lean insertionSortLoopProof1}} ``` + + +問題はLeanが `insertSorted` が渡された配列と同じサイズの配列を返すことを知る方法がないことです。`insertionSortLoop` が停止することを証明するには、まず `insertSorted` が配列のサイズを変更しないことを証明する必要があります。エラーメッセージから証明されていない停止条件を関数にコピーし、`sorry` で「証明」することで、この関数を一時的に受け入れることができます: + ```leantac {{#example_in Examples/ProgramsProofs/InsertionSort.lean insertionSortLoopSorry}} ``` @@ -139,30 +265,59 @@ Copying the unproved termination condition from the error message to the functio {{#example_out Examples/ProgramsProofs/InsertionSort.lean insertionSortLoopSorry}} ``` + +`insertSorted` は挿入される要素のインデックスに対して構造的に再帰的であるため、証明はインデックスに対する帰納法で行う必要があります。基本ケースでは配列は変更されずに返されるので、その長さは確かに変化しません。帰納法のステップでは、次の小さいインデックスで再帰的に呼び出しても配列の長さは変わらないという帰納法の仮定が成り立ちます。ここでは2つのケースを考慮します:要素がソート済みの領域に完全に挿入され、配列が変更されずに返される場合と、再帰呼び出しの前に要素が次の要素と交換される場合です。しかし、配列の2つの要素を入れ替えてもサイズは変わらないため、帰納法の仮定によれば次のインデックスによる再帰呼び出しは、引数と同じサイズの配列を返します。したがってサイズは変わりません。 + + + +この自然言語の定理文をLeanに翻訳し、本章のテクニックを使って進めば、基本ケースの証明と帰納法のステップを進めるには十分です: + ```leantac {{#example_in Examples/ProgramsProofs/InsertionSort.lean insert_sorted_size_eq_0}} ``` + + +帰納法のステップ中で `insertSorted` を使って単純化することで `insertSorted` 内のパターンマッチが現れます: + ```output error {{#example_out Examples/ProgramsProofs/InsertionSort.lean insert_sorted_size_eq_0}} ``` + + +`if` や `match` を含むゴールに直面した時、`split` タクティク(マージソートの定義で使われている `split` 関数と混同しないように)は制御フローのパスごとにゴールを新しいゴールに置き換えます: + ```leantac {{#example_in Examples/ProgramsProofs/InsertionSort.lean insert_sorted_size_eq_1}} ``` + + +さらに、それぞれの新しいゴールにはどのブランチがそのゴールにつながったかを示す仮定があり、この場合では `heq✝` と名付けられます: + ```output error {{#example_out Examples/ProgramsProofs/InsertionSort.lean insert_sorted_size_eq_1}} ``` + + +両方のケースに対して単純な証明を書くよりも、`split` の後に `<;> try rfl` を加えることで2つの単純なケースはたちまち消え、1つのゴールだけが残ります: + ```leantac {{#example_in Examples/ProgramsProofs/InsertionSort.lean insert_sorted_size_eq_2}} ``` @@ -170,84 +325,158 @@ Rather than write proofs for both simple cases, adding `<;> try rfl` after `spli {{#example_out Examples/ProgramsProofs/InsertionSort.lean insert_sorted_size_eq_2}} ``` + +残念なことに、帰納法の仮定はこのゴールを証明するには不十分です。帰納法の仮定は `arr` に対して `insertSorted` を呼び出すとサイズが変わらないことを述べていますが、証明のゴールは再帰的な呼び出しの結果と置換の結果がサイズを変えないことを示すことです。証明を成功させるには、小さい方のインデックスを引数として `insertSorted` に渡す **任意の** 配列に対して機能する帰納法の仮定が必要です。 + + + +`induction` タクティクに `generalizing` オプションを使用することで強力な帰納法の仮定を得ることができます。このオプションは、基本ケース、帰納法の仮定、および帰納法のステップで示されるゴールを生成するために使用される文にコンテキストから追加の仮定をもたらします。`arr` を一般化することで、より強力な仮定を導くことができます: + ```leantac {{#example_in Examples/ProgramsProofs/InsertionSort.lean insert_sorted_size_eq_3}} ``` + + +その結果、`arr` は帰納法の仮定の「全ての~について~」文の一部となります: + ```output error {{#example_out Examples/ProgramsProofs/InsertionSort.lean insert_sorted_size_eq_3}} ``` + + +しかし、この証明全体は手に負えなくなっていきます。次のステップは置換結果の長さを表す変数を導入し、それが `arr.size` と等しいことを示し、この変数が再帰呼び出しの結果得られる配列の長さとも等しいことを示すことです。これらの等式を連鎖させて、ゴールを証明することができます。しかし、帰納法の仮定が自動的に十分強く、変数もすでに導入されるように定理文を注意深く再定式化する方がはるかに簡単です。再定式化された文は次のようになります: + ```leantac {{#example_in Examples/ProgramsProofs/InsertionSort.lean insert_sorted_size_eq_redo_0}} ``` + + +このバージョンの定理文はいくつかの理由から証明が容易です: + + + + 1. インデックスとその有効性の証明を `Fin` にまとめるのではなく、インデックスを配列の前に持ってくる。これにより帰納法の仮定が配列と `i` が範囲内にあることの証明に対して自然に一般化されます。 + 2. 抽象的な長さ `len` が `array.size` の略として導入されました。証明の自動化は明示的な等号を扱う方が得意な場合が多いです。 + + +結果として得られる証明状態は、帰納法の仮定を生成するために使用される文と、基本ケースと帰納法のステップのゴールを示します: + ```output error {{#example_out Examples/ProgramsProofs/InsertionSort.lean insert_sorted_size_eq_redo_0}} ``` + + +この文と `induction` タクティクの結果として得られるゴールを比べてみてください: + ```leantac {{#example_in Examples/ProgramsProofs/InsertionSort.lean insert_sorted_size_eq_redo_1a}} ``` + + +基本ケースでは、`i` のそれぞれの出現が `0` に置き換えられています。各仮定の導入に `intro` を使用し、次に `insertSorted` を使用して単純化するとインデックス `zero` での `insertSorted` はその引数を変更せずに返すため、ゴールが証明されます: + ```output error {{#example_out Examples/ProgramsProofs/InsertionSort.lean insert_sorted_size_eq_redo_1a}} ``` + + +帰納法のステップにおいて、帰納法の仮定は実にちょうど良い強さを持ちます。これは配列の長さが `len` である限り **どのような** 配列に対しても有効です: + ```output error {{#example_out Examples/ProgramsProofs/InsertionSort.lean insert_sorted_size_eq_redo_1b}} ``` + + +基本ケースでは、`simp` によってゴールが `arr.size = len` へと簡約されます: + ```leantac {{#example_in Examples/ProgramsProofs/InsertionSort.lean insert_sorted_size_eq_redo_2}} ``` ```output error {{#example_out Examples/ProgramsProofs/InsertionSort.lean insert_sorted_size_eq_redo_2}} ``` + + +これは仮定 `hLen` を使って証明できます。`simp` に `*` パラメータを追加することで仮定を追加で使用するように指示することができます: + ```leantac {{#example_in Examples/ProgramsProofs/InsertionSort.lean insert_sorted_size_eq_redo_2b}} ``` + + +帰納法のステップにおいて、仮定の導入とゴールの単純化によってゴールはふたたびパターンマッチを含んだものとなります: + ```leantac {{#example_in Examples/ProgramsProofs/InsertionSort.lean insert_sorted_size_eq_redo_3}} ``` ```output error {{#example_out Examples/ProgramsProofs/InsertionSort.lean insert_sorted_size_eq_redo_3}} ``` + + +`split` タクティクを使うことで各パターンそれぞれに1つずつゴールが割り当てられます。繰り返しになりますが、最初の2つのゴールは再帰呼び出しのない分岐から得られるため帰納法の仮定は必要ありません: + ```leantac {{#example_in Examples/ProgramsProofs/InsertionSort.lean insert_sorted_size_eq_redo_4}} ``` ```output error {{#example_out Examples/ProgramsProofs/InsertionSort.lean insert_sorted_size_eq_redo_4}} ``` + + +`split` で得られる各ゴールで `try assumption` を実行すると非再帰的なゴールは両方とも無くなります: + ```leantac {{#example_in Examples/ProgramsProofs/InsertionSort.lean insert_sorted_size_eq_redo_5}} ``` @@ -255,40 +484,75 @@ Running `try assumption` in each goal that results from `split` eliminates both {{#example_out Examples/ProgramsProofs/InsertionSort.lean insert_sorted_size_eq_redo_5}} ``` + + +再帰関数に関係するすべての配列の長さに定数 `len` を使用するという新しい証明のゴールについての形式化は `simp` が解決できる類の問題にうまく当てはまります。配列の長さを `len` に関連付ける仮定が重要であるため、この証明の最終的なゴールは `simp [*]` で解くことができます: + ```leantac {{#example_decl Examples/ProgramsProofs/InsertionSort.lean insert_sorted_size_eq_redo_6}} ``` + + +最後に、`simp [*]` は仮定を使うことができるため、`try assumption` の行は `simp [*]` に置き換えることができ、証明を短くできます: + ```leantac {{#example_decl Examples/ProgramsProofs/InsertionSort.lean insert_sorted_size_eq_redo}} ``` + + +これでこの証明を使って `insertionSortLoop` の `sorry` を置き換えることができます。定理の `len` 引数に `arr.size` を与えることで最終的な結論は `(insertSorted arr ⟨i, isLt⟩).size = arr.size` となるため、非常に扱いやすい証明のゴールへと書き換えることができます: + ```leantacnorfl {{#example_in Examples/ProgramsProofs/InsertionSort.lean insertionSortLoopRw}} ``` ```output error {{#example_out Examples/ProgramsProofs/InsertionSort.lean insertionSortLoopRw}} ``` + + +証明 `{{#example_in Examples/ProgramsProofs/InsertionSort.lean sub_succ_lt_self_type}}` はLeanの標準ライブラリの一部です。この型は `{{#example_out Examples/ProgramsProofs/InsertionSort.lean sub_succ_lt_self_type}}` であり、これはまさに必要だったものです: + + ```leantacnorfl {{#example_decl Examples/ProgramsProofs/InsertionSort.lean insertionSortLoop}} ``` + + +## ドライバ関数 + + +挿入ソート自体は `insertionSortLoop` を呼び出し、配列のソート済み領域と未ソート領域を区切るインデックスを `0` に初期化します: + ```lean {{#example_decl Examples/ProgramsProofs/InsertionSort.lean insertionSort}} ``` + + +簡単なテストをいくつかやってみると、この関数は少なくともあからさまに間違ってはいません: + ```lean {{#example_in Examples/ProgramsProofs/InsertionSort.lean insertionSortNums}} ``` @@ -302,33 +566,59 @@ A few quick tests show the function is at least not blatantly wrong: {{#example_out Examples/ProgramsProofs/InsertionSort.lean insertionSortStrings}} ``` + + +## これは本当に挿入ソート? + +挿入ソートはin-placeのソートアルゴリズムとして **定義されています** 。最悪実行時間が2倍になるにも関わらず挿入ソートが有用であるのは、余分な領域を割り当てず、ほぼソート済みのデータを効率的に扱う安定したソートアルゴリズムであるからです。もし内部ループの各繰り返しが新しい配列を割り当てるのであれば、このアルゴリズムは **本物の** 挿入ソートではありません。 + + + +`Array.set` や `Array.swap` などのLeanの配列操作は対象の配列の参照カウントが1より大きいかどうかをチェックします。もし大きければ、その配列はコードの複数の個所から見えることになり、コピーしなければならないことになります。そうでなければLeanはもはや純粋関数型言語ではなくなってしまいます。しかし、参照カウントがちょうど1の場合、その値に対する潜在的な観測者はほかに存在しません。このような場合、配列のプリミティブはその場で配列を変更します。ここ以外のプログラムが知り得ないことはそれ等自身を傷つけることはありません。 + +Leanの証明論理は純粋関数型プログラムのレベルで機能するのであって、その下にある実装に対しては機能しません。つまりプログラムが不必要にデータをコピーしているかどうかを発見する最善の方法は、それをテストすることです。更新が必要な各ポイントで `dbgTraceIfShared` の呼び出しを追加すると、問題の値が複数の参照を持つ場合に提供されたメッセージが `stderr` に出力されます。 + + + +挿入ソートは変更ではなくコピーをしてしまう恐れのある箇所がまさに一か所あります:`Array.swap`の呼び出しです。`arr.swap ⟨i', by assumption⟩ i` を `((dbgTraceIfShared "array to swap" arr).swap ⟨i', by assumption⟩ i)` に置き換えることで、プログラムはプログラムを変更できない時は必ず `shared RC array to swap` を出力するようになります。しかし、追加の関数呼び出しが加わることの変化によって証明も変わってしまいます。`dbgTraceIfShared` は第2引数を直接返すため、`simp` の呼び出しに追加するだけで証明は修正されます。 + + +挿入ソートの計装用の完全なコードは以下の通りです: + ```leantacnorfl {{#include ../../../examples/Examples/ProgramsProofs/InstrumentedInsertionSort.lean:InstrumentedInsertionSort}} ``` + +計装が実際に機能するかどうかをチェックするにはちょっとした工夫が必要です。まずLeanのコンパイラはコンパイル時にすべての引数が分かっている場合、関数呼び出しを積極的に最適化します。そのため計装のために大きな配列に `insertionSort` を適用するプログラムを書くだけでは不十分です。というのもコンパイル結果のコードはソート済みの配列だけが定数として含まれる可能性があるからです。コンパイラがソートルーチンを最適化しないようにする最も簡単な方法は `stdin` から配列を読み込むことです。次に、コンパイラはデッドコードの除去を行います。もし `let` に束縛された変数が使われることがなければ、プログラムへの余分な `let` を追加しても実行中のコードの参照が増えるとは限りません。余分な参照が完全に除去されないようにするためには、余分な参照が何らかの形で使われるようにすることが重要です。 + + + +計装をテストする最初のステップは標準入力から行の配列を読み込む `getLines` を書くことです: + ```lean {{#include ../../../examples/Examples/ProgramsProofs/InstrumentedInsertionSort.lean:getLines}} ``` + + +`IO.FS.Stream.getLine` は末尾の開業を含む完全なテキスト行を返します。ファイル終端記号に到達した場合は `""` を返します。 + + +次に、2つの別々の `main` ルーチンが必要です。どちらも標準入力からソート対象の配列を読み込み、これによってコンパイル時に `insertionSort` の呼び出しが戻り値に置き換えられないようにします。どちらもコンソールに出力し、これにより `insertionSort` の呼び出しが完全に最適化されることはありません。一方はソートされた配列のみを表示し、もう一方はソートされた配列と元の配列を両方表示します。2番目の関数は `Array.swap` が新しい配列を確保しなければならなかったという警告を表示します: + ```lean {{#include ../../../examples/Examples/ProgramsProofs/InstrumentedInsertionSort.lean:mains}} ``` + + +実際の `main` は与えらえたコマンドライン引数に基づいて2つのメインアクションのうち1つを選択するだけです: + ```lean {{#include ../../../examples/Examples/ProgramsProofs/InstrumentedInsertionSort.lean:main}} ``` + + +引数無しで実行すると、期待通りの使用情報が得られます: + ``` $ {{#command {sort-demo} {sort-sharing} {./run-usage} {sort}}} {{#command_out {sort-sharing} {./run-usage} }} ``` + + +ファイル `test-data` は以下の岩についての情報を保持します: + ``` {{#file_contents {sort-sharing} {sort-demo/test-data}}} ``` + + +これらの岩石に計装用の挿入ソートを使うとアルファベット順に印字されます: + ``` $ {{#command {sort-demo} {sort-sharing} {sort --unique < test-data}}} {{#command_out {sort-sharing} {sort --unique < test-data} }} ``` + + +しかし、元の配列への参照が保持されるバージョンでは、最初に `Array.swap` を呼び出した時から `stderr` (つまり `shared RC array to swap` )に通知が行われます: + ``` $ {{#command {sort-demo} {sort-sharing} {sort --shared < test-data}}} {{#command_out {sort-sharing} {sort --shared < test-data} }} ``` + +`shared RC` が1つしか表示されないということは、配列が1度だけコピーされることを意味します。これは、`Array.swap` を呼び出した結果のコピー自体が一意であるためであり、それ以上コピーする必要はありません。命令型言語では、配列を参照渡しする前に明示的にコピーすることを忘れると微妙なバグが発生することがあります。`sort --shared` を実行すると、Leanのプログラムの純粋関数的な意味を保つために必要な分だけ配列がコピーされますが、それ以上はコピーされません。 + +## その他の更新の機会 + + + +参照が一意である場合にコピーの代わりに更新を使用するのは配列更新演算子に限ったことではありません。Leanは参照カウントが0になりそうなコンストラクタを「リサイクル」し、新しいデータを割り当てる代わりに再利用しようとします。これは例えば、`Let.map` が連結リストをその場で更新することを意味します。Leanのコードでホット・ループを最適化する最も重要なステップの1つは、変更されるデータが複数の場所から参照されないようにすることです。 + +## 演習問題 + + + + * 配列を反転させる関数を書いてください。入力配列の参照カウントが1の場合、関数が新しい配列を確保しないことをテストしてください。 + + +* 配列に対してマージソートかクイックソートのどちらかを実装してください。読者の実装が停止することを証明し、期待以上の配列を確保しないことをテストしてください。これは難しい練習問題です!