diff --git a/functional-programming-lean/src/SUMMARY.md b/functional-programming-lean/src/SUMMARY.md index d97e7e3..683e1df 100644 --- a/functional-programming-lean/src/SUMMARY.md +++ b/functional-programming-lean/src/SUMMARY.md @@ -18,7 +18,7 @@ - [ステップ・バイ・ステップ](./hello-world/step-by-step.md) - [プロジェクトの開始](./hello-world/starting-a-project.md) - [Worked Example: `cat`](./hello-world/cat.md) - - [Additional Conveniences](./hello-world/conveniences.md) + - [その他の便利機能](./hello-world/conveniences.md) - [まとめ](./hello-world/summary.md) - [休憩:命題・証明・リストの添え字アクセス](props-proofs-indexing.md) - [オーバーロードと型クラス](type-classes.md) diff --git a/functional-programming-lean/src/hello-world/conveniences.md b/functional-programming-lean/src/hello-world/conveniences.md index e78c810..f3c99b2 100644 --- a/functional-programming-lean/src/hello-world/conveniences.md +++ b/functional-programming-lean/src/hello-world/conveniences.md @@ -1,55 +1,114 @@ + +# その他の便利機能 + +## ネストされたアクション + + + +`feline` の関数の多くは `IO` アクションの結果に名前を付けてすぐ一度だけ使うというパターンを繰り返していました。例えば、`dump` の場合: + ```lean {{#include ../../../examples/feline/2/Main.lean:dump}} ``` + + +`stdout` のところでこのパターンが発生しています: + ```lean {{#include ../../../examples/feline/2/Main.lean:stdoutBind}} ``` + + +同様に、`fileStream` も以下のコード片を含みます: + ```lean {{#include ../../../examples/feline/2/Main.lean:fileExistsBind}} ``` + + +Leanが `do` ブロックをコンパイルする時、括弧のすぐ下にある左矢印からなる式は、それを包含する最も近い `do` に持ち上げられ、その式の結果が一意な名前に束縛されます。この一意名でもともとの式が置き換えられます。つまり、`dump` は次のように書くこともできます: + ```lean {{#example_decl Examples/Cat.lean dump}} ``` + +このバージョンの `dump` では、一度しか使わない名前の導入を避けることができるため、プログラムを大幅に簡略にすることができます。ネストされた式のコンテキストからLeanが持ち上げた `IO` アクションは **ネストされたアクション** と呼ばれます。 + + + +`fileStream` も同じテクニックを使って簡略にできます: + ```lean {{#example_decl Examples/Cat.lean fileStream}} ``` + + +ここでは、`handle` というローカル名をネストされたアクションを使って削除することもできますが、その場合は式が長く複雑になってしまいます。ネストされたアクションを使うことが良い場合が多いとはいえ、中間的な結果に名前を付けておくと便利なこともあります。 + + +しかし、ネストされたアクションは `do` ブロックの中で起こる `IO` アクションの短縮表記に過ぎないことを覚えておくことが重要です。アクションの実行に伴う副作用は同じ順序で発生し、副作用の実行が式の評価と混在することはありません。これが混乱を招く可能性がある例として、実行されたことを世界に通知した後にデータを返す以下の補助的な定義を考えてみましょう: + ```lean {{#example_decl Examples/Cat.lean getNumA}} {{#example_decl Examples/Cat.lean getNumB}} ``` + +これらの定義はユーザ入力を検証したり、データベースを読み込んだり、ファイルを開いたりするようなより複雑な `IO` コードの代用を意図しています。 + + + +数字Aが5の時は `0` を、それ以外の時は `B` の数値を表示するプログラムは次のように書けます: + ```lean {{#example_decl Examples/Cat.lean testEffects}} ``` + + +しかし、このプログラムはおそらく意図した以上の副作用(ユーザ入力のプロンプトやデータベースの読み取りなど)を持ちます。`getNumA` の定義では、常に `5` を返すことが明確になっているので、このプログラムではBの数値を読み込むべきではありません。しかし、プログラムを実行すると次のような出力が得られます: + ```output info {{#example_out Examples/Cat.lean runTest}} ``` @@ -57,19 +116,36 @@ However, running the program results in the following output: ```lean {{#example_decl Examples/Cat.lean testEffectsExpanded}} ``` + +これはネストされたアクションは **最も近い** `do` ブロックに持ち上げられるルールによるものです。なぜならここでの文は `a` を定義している `let` であって `if` は `do` ブロックの中の文ではないため、この `if` の分岐は暗黙的に `do` ブロックで囲まれないのです。実際、条件式の型は `IO Nat` ではなく `Nat` であるため、このようにラップすることはできません。 + + + +## `do` の柔軟なレイアウト + +Leanにおいて、`do` 式は空白に対して敏感です。`do` の各 `IO` アクションや局所的な束縛はそれぞれ独立した行で始まり、インデントも同じでなければなりません。ほとんどすべての `do` はこのように書くべきです。しかしまれに空白やインデントを手動で制御する必要や、複数の小さなアクションを1行に書くと便利な場合があります。このような場合、改行はセミコロンに、インデントは波括弧に置き換えることができます。 + + + +例えば、以下のプログラムはすべての等価です: + ```lean {{#example_decl Examples/Cat.lean helloOne}} @@ -78,28 +154,58 @@ For instance, all of the following programs are equivalent: {{#example_decl Examples/Cat.lean helloThree}} ``` + + +慣用的なLeanの `do` のコードでは波括弧を使うことはほとんどありません。 + +## `#eval` による `IO` アクションの実行 + + + +Leanの `#eval` コマンドは単に式の評価だけでなく、`IO` アクションを実行するために使用することができます。通常、`#eval` コマンドをLeanファイルに追加すると、Leanは指定された式を評価し、結果の値を文字列に変換して、その文字列をツールチップやinfoウィンドウに表示します。`IO` アクションは文字列に変換できないからといって失敗させるのではなく、`#eval` はそのアクションを実行し、その副作用を執り行います。実行結果が `Unit` 型の値 `()` の場合、結果の文字列は表示されませんが、文字列に変換できる型であればLeanは結果の値を表示します。 + + +つまり、`countdown` と `runActions` の定義があらかじめ与えられているとした時: + ```lean {{#example_in Examples/HelloWorld.lean evalDoesIO}} ``` + + +は以下を出力します。 + ```output info {{#example_out Examples/HelloWorld.lean evalDoesIO}} ``` + +これは `IO` アクションを実行することによって生成される出力であり、アクション自体の不透明な表現ではありません。言い換えれば、`IO` アクションに対して `#eval` は指定された式の **評価** と、結果のアクションの値の **実行** をどちらも行います。 + + + +`#eval` を使って `IO` アクションを素早くテストすることはプログラム全体をコンパイルして実行することよりもはるかに便利です。しかし、いくつかの制限があります。例えば、標準入力からの読み込みは単に空の入力を返すだけになります。さらに、`IO` アクションはLeanがユーザに提供する新参情報を更新する必要があるたびに再実行され、これによって実行タイミングが予測できなくなります。例えば、ファイルを読み書きするアクションは、都合の悪い時に実行される可能性があります。