非決定論をHaskellの機能にする理由は何ですか?


8

私たちは中にいることを知っているプロローグ - 非決定論的述語がダウン削るために使用される機能です組合せの問題

Haskell では、List Monadの Prolog と同様の非決定論的な動作が見られます。

Haskellでは、サンクの評価順序の選択に非決定性も見られます。

ただし、これらのサンクのどれを最初に評価するかをGHCに指示するものは何もないため、GHCは最初に評価するサンクを自由に選択できます。

これは魅力的です-そして多少解放されます。これが機能する原理は何でしょうか(8クイーンのようなロジックの問題を回避することは別として)。非決定論で解決しようとしていた大きなアイデアや大きな問題はありましたか?

私の質問は非決定論をHaskellの機能にする理由は何ですか?


Haskellサンク評価は参照的に透過的(純粋)なので、順序は結果に影響を与えません。多くの可能な結果の1つが選択されるのは、Prologの意味での非決定的プログラミングではありません。
ジャック

もちろん、コンパイラを設計しているときは、非決定性を追加するのはもっと難しい作業でしょうか?余分な作業を行う動機は何ですか?
hawkeye 2017

2
標準では、評価の順序を定義していません。標準は、コンパイラに特定の順序を選択することを強制しません。コンパイラがなんらかの方法でランダムに選択しているという意味ではありません。コンパイラーは、独自の規則に従って、決定論的に順序を選択します。ルールが標準で定義されておらず、コンパイラー実装の詳細として残されているだけです。
Fyodor Soikin 2017

@FyodorSoikinに感謝-それは役に立ちます。コンパイラなのか標準なのかは関係ありません。単に設計上の選択の背後にある意図です。なんで?これから何を得るのですか?
hawkeye 2017

1
標準に評価の特定の順序を含めないことにより、コンパイラーが自由に変更を自由にできるようになり、最適化が可能になります。これはかなり標準的なものです。マイクロプロセッサでさえ、結果に影響を及ぼさないことが証明できれば、命令の順序が混乱します。
Fyodor Soikin 2017

回答:


19

質問で引用された両方の側面が非決定論の形式として表示されることは事実ですが、実際には、それらがどのように機能するか、目標においてもかなり異なります。したがって、答えは必ず2つの部分に分割する必要があります。

評価順序

Haskellは、基本的に2つの理由により、サンクの評価において特定の実行順序を要求していません。

  1. まず第一に、Haskellは純粋に関数型の言語なので、参照の透明性が保証されます(unsafePerformIO&coをいじらない場合)。任意の式の評価、というこれは、手段などを f x想定し(同じ結果で、それが評価された回数に関係なく、それが評価されたプログラムの一部に関係なくなりますfxの考慮スコープで同じ値にバインドコース)。したがって、特定の実行順序を強制しても、変更してもプログラムの結果に目に見える影響が生じないため目的はありません。これに関して、これは実際には非決定性の形式ではなく、少なくとも観察可能な形式ではありません。 1つは、プログラムのさまざまな実行方法がすべて意味的に同等であるためです。

ただし、実行順序の変更はプログラムのパフォーマンスに影響を与える可能性があり、 GHCのようなコンパイラーがこのような高パフォーマンスのコンパイルを取得できる驚くべきパフォーマンスを達成するには、コンパイラーに自由に順序を操作する自由を任せることが基本です。レベル言語。例として、古典的なストリームとフュージョンの変換について考えてみましょう。

map f . map g = map (f.g)

この等価性は、2つの関数をリストにmap適用すると、2つの関数の合成を1回適用するのと同じ結果になることを単に意味します。これは、参照の透過性のためにのみ当てはまり、コンパイラが常に行うことができる一種の変換です何があっても適用します。3つの関数の実行順序を変更しても式の結果に影響があった場合、これは不可能です。一方、最初の形式ではなく2番目の形式でコンパイルすると、1つの一時リストの作成が回避され、リストを1回しかトラバースしないため、パフォーマンスに大きな影響を与える可能性があります。GHCがこのような変換を自動的に適用できるという事実は、参照の透明性と固定されていない実行順序の直接の結果であり、Haskellが達成できる優れたパフォーマンスの主要な側面の1つです。

  1. Haskellは遅延言語です。これは、結果が実際に必要でない限り、特定の式を評価する必要がないことを意味します。怠惰は時々議論される機能であり、他の一部の関数型言語はそれを避けたり、オプトインに制限したりしますが、Haskellのコンテキストでは、言語の使用と設計の方法の重要な機能です。怠惰は、コンパイラのオプティマイザの手に渡るもう1つの強力なツールであり、最も重要なことは、コードを簡単に構成できるようにすることです。

合成のしやすさの意味を理解するためにproducer :: Int -> [Int]、入力引数からある種のデータのリストを計算する複雑なタスクを実行する関数がある場合の例を考えてください。consumer :: [Int] -> Intこれは、リストの結果を計算する別の複雑な関数です。入力データ。それらを個別に作成し、テストし、非常に慎重に最適化し、さまざまなプロジェクトで分離して使用しました。次のプロジェクトでは、次consumerの結果を呼び出す必要がありますproducer。非レイジー言語では、一時的なリスト構造を構築せずに結合タスクを最も効率的に実装できる場合があるため、これは最適ではない可能性があります。最適化された実装を取得するには、結合されたタスクを最初から再実装し、再テストして、再最適化する必要があります。

haskellではこれは不要であり、2つの関数の合成を呼び出すことconsumer . producerは完全に問題ありません。その理由は、プログラムは実際にproducerそれを与える前に全体の結果を生成する必要がないからconsumerです。実際、consumer入力リストの最初の要素が必要になるとすぐに、対応するコードproducerがそれを生成するために必要なだけ実行され、それ以上は実行されません。2番目の要素が必要な場合は、計算されます。で必要のない要素がある場合、その要素はまったくconsumer計算されないため、無駄な計算を効果的に節約できます。実行consumerproducer効率的にインターリーブされ、中間リスト構造のメモリ使用量を回避するだけでなく、無用な計算も回避できます。実行は、おそらく自分で記述しなければならない手書きの結合バージョンに似ています。これは私が作曲によって意味したものです。十分にテストされたパフォーマンスの高いコードが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

ただし、のインスタンスはリストだけではありませんMonadPlusMaybe別のインスタンスです。したがって、1つのソリューションだけが必要な場合は、どのソリューションでもsolveMaybe SatSolver値を返したかのように使用できます。

main = case solve task of 
  Nothing -> putStrLn "No solution"
  Just result -> print result

ここで、2つのタスクが作成され、taskとがありtask2、いずれかのソリューションを取得したいとします。もう一度、すべてが一緒になって、既存のビルディングブロックを作成できるようにします。

combinedTask = task <|> task2

ここで、<|>によって提供される二進演算でAlternativeのスーパークラスである型クラスは、MonadPlus。これにより、変更を加えずにコードを再利用して、意図を明確に表現できました。非決定性はコードで明確に表現されており、非決定性が実際にどのように実装されているかという詳細に埋もれていません。Alternative型クラスの上に構築されたコンビネーターを見て、さらに例を取得することをお勧めします。

リストのような非決定論的なモナドは、優れた演習を表現する方法であるだけでなく、本質的に非決定論的なタスクの実装の意図を明確に表現するエレガントで再利用可能なコードを設計する方法を提供します。


あなたのtask実装は完全に正しいとは思いません。assertTrue2つのパラメーターを取り、1つだけを与えます。表記法SatSolverを使用する場合は、関数間の値をもう少し明示的にパイプする必要がありますdo
キャッスル

ああ!私が書いていた最初のバージョンは>> =のチェーンを使用したので、引数に名前を付けることを避けることができました(必要がありました)。これは、do表記がより冗長な場合のようです。自由に編集してください。編集しない場合は、オフィスに戻ったらすぐに編集します。
ギガバイト
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.