質問で引用された両方の側面が非決定論の形式として表示されることは事実ですが、実際には、それらがどのように機能するか、目標においてもかなり異なります。したがって、答えは必ず2つの部分に分割する必要があります。
評価順序
Haskellは、基本的に2つの理由により、サンクの評価において特定の実行順序を要求していません。
- まず第一に、Haskellは純粋に関数型の言語なので、参照の透明性が保証されます(
unsafePerformIO
&coをいじらない場合)。任意の式の評価、というこれは、手段などを f x
想定し(同じ結果で、それが評価された回数に関係なく、それが評価されたプログラムの一部に関係なくなりますf
とx
の考慮スコープで同じ値にバインドコース)。したがって、特定の実行順序を強制しても、変更してもプログラムの結果に目に見える影響が生じないため、目的はありません。これに関して、これは実際には非決定性の形式ではなく、少なくとも観察可能な形式ではありません。 1つは、プログラムのさまざまな実行方法がすべて意味的に同等であるためです。
ただし、実行順序の変更はプログラムのパフォーマンスに影響を与える可能性があり、
GHCのようなコンパイラーがこのような高パフォーマンスのコンパイルを取得できる驚くべきパフォーマンスを達成するには、コンパイラーに自由に順序を操作する自由を任せることが基本です。レベル言語。例として、古典的なストリームとフュージョンの変換について考えてみましょう。
map f . map g = map (f.g)
この等価性は、2つの関数をリストにmap
適用すると、2つの関数の合成を1回適用するのと同じ結果になることを単に意味します。これは、参照の透過性のためにのみ当てはまり、コンパイラが常に行うことができる一種の変換です何があっても適用します。3つの関数の実行順序を変更しても式の結果に影響があった場合、これは不可能です。一方、最初の形式ではなく2番目の形式でコンパイルすると、1つの一時リストの作成が回避され、リストを1回しかトラバースしないため、パフォーマンスに大きな影響を与える可能性があります。GHCがこのような変換を自動的に適用できるという事実は、参照の透明性と固定されていない実行順序の直接の結果であり、Haskellが達成できる優れたパフォーマンスの主要な側面の1つです。
- Haskellは遅延言語です。これは、結果が実際に必要でない限り、特定の式を評価する必要がないことを意味します。怠惰は時々議論される機能であり、他の一部の関数型言語はそれを避けたり、オプトインに制限したりしますが、Haskellのコンテキストでは、言語の使用と設計の方法の重要な機能です。怠惰は、コンパイラのオプティマイザの手に渡るもう1つの強力なツールであり、最も重要なことは、コードを簡単に構成できるようにすることです。
合成のしやすさの意味を理解するためにproducer :: Int -> [Int]
、入力引数からある種のデータのリストを計算する複雑なタスクを実行する関数がある場合の例を考えてください。consumer :: [Int] -> Int
これは、リストの結果を計算する別の複雑な関数です。入力データ。それらを個別に作成し、テストし、非常に慎重に最適化し、さまざまなプロジェクトで分離して使用しました。次のプロジェクトでは、次consumer
の結果を呼び出す必要がありますproducer
。非レイジー言語では、一時的なリスト構造を構築せずに結合タスクを最も効率的に実装できる場合があるため、これは最適ではない可能性があります。最適化された実装を取得するには、結合されたタスクを最初から再実装し、再テストして、再最適化する必要があります。
haskellではこれは不要であり、2つの関数の合成を呼び出すことconsumer . producer
は完全に問題ありません。その理由は、プログラムは実際にproducer
それを与える前に全体の結果を生成する必要がないからconsumer
です。実際、consumer
入力リストの最初の要素が必要になるとすぐに、対応するコードproducer
がそれを生成するために必要なだけ実行され、それ以上は実行されません。2番目の要素が必要な場合は、計算されます。で必要のない要素がある場合、その要素はまったくconsumer
計算されないため、無駄な計算を効果的に節約できます。実行consumer
とproducer
効率的にインターリーブされ、中間リスト構造のメモリ使用量を回避するだけでなく、無用な計算も回避できます。実行は、おそらく自分で記述しなければならない手書きの結合バージョンに似ています。これは私が作曲によって意味したものです。十分にテストされたパフォーマンスの高いコードが2つあり、それらを組み合わせて、十分にテストされたパフォーマンスの高いコードを無料で入手できます。
非決定的モナド
Listモナドや類似のモナドによって提供される非決定的な動作の使用は、代わりに完全に異なります。ここで重要なのは、コンパイラーにプログラムを最適化する手段を提供することではなく、本質的に非決定論的な計算を明確かつ簡潔に表現することです。
私の意味の例は、Data.Boolean.SatSolver
ライブラリーのインターフェースによって提供されます。Haskellに実装された非常にシンプルなDPLL SATソルバーを提供します。ご存知かもしれませんが、SAT問題を解決するには、ブール式を満たすブール変数の割り当てを見つける必要があります。ただし、そのような割り当ては複数ある場合があり、アプリケーションに応じて、それらのいずれかを見つけるか、それらすべてを反復する必要がある場合があります。したがって、多くのライブラリには、getSolution
およびのような2つの異なる関数がありgetAllSolutions
ます。このライブラリには、代わりにsolve
次のタイプの関数が1つだけあります。
solve :: MonadPlus m => SatSolver -> m SatSolver
ここでは、結果はSatSolver
未指定の型のモナド内にラップされた値ですが、MonadPlus
型クラスを実装するように制限されています。この型クラスは、リストモナドによって提供される一種の非決定性を表すクラスであり、実際にはリストはインスタンスです。SatSolver
値を操作するすべての関数は、MonadPlus
インスタンスにラップされた結果を返します。したがって、数式がp || !q
ありq
、trueに設定された結果を制限することによってそれを解決する場合、使用法は次のとおりです(変数は名前で識別されるのではなく番号が付けられます)。
expr = Var 1 :||: Not (Var 2)
task :: MonadPlus m => m SatSolver
task = do
pure newSatSolver
assertTrue expr
assertTrue (Var 2)
モナドインスタンスとdo表記が、関数がSatSolver
データ構造を管理する方法のすべての低レベルの詳細をマスクし、目的を明確に表現できることに注意してください。
ここで、すべての結果を取得したい場合solve
は、結果がリストでなければならないコンテキストで使用するだけです。以下はすべての結果を画面に出力します(存在しないのShow
インスタンスを想定していますがSatSolver
、この点は許してください)。
main = sequence . map print . solve task
ただし、のインスタンスはリストだけではありませんMonadPlus
。Maybe
別のインスタンスです。したがって、1つのソリューションだけが必要な場合は、どのソリューションでもsolve
、Maybe SatSolver
値を返したかのように使用できます。
main = case solve task of
Nothing -> putStrLn "No solution"
Just result -> print result
ここで、2つのタスクが作成され、task
とがありtask2
、いずれかのソリューションを取得したいとします。もう一度、すべてが一緒になって、既存のビルディングブロックを作成できるようにします。
combinedTask = task <|> task2
ここで、<|>
によって提供される二進演算でAlternative
のスーパークラスである型クラスは、MonadPlus
。これにより、変更を加えずにコードを再利用して、意図を明確に表現できました。非決定性はコードで明確に表現されており、非決定性が実際にどのように実装されているかという詳細に埋もれていません。Alternative
型クラスの上に構築されたコンビネーターを見て、さらに例を取得することをお勧めします。
リストのような非決定論的なモナドは、優れた演習を表現する方法であるだけでなく、本質的に非決定論的なタスクの実装の意図を明確に表現するエレガントで再利用可能なコードを設計する方法を提供します。