この記事は、
「アインシュタインのプロローグ問題」という記事への回答です。 その中で、著者は、Prologはこの問題を解決するのに非常に適しており、行の総数がタスクの条件にほぼ一致すると書いています。 ここで、C#でコードの行数がほぼ同じになることを示したいと思います。 Prologにソリューションをコピーして、構文を少し変更します。 最初に、最終結果を示し、次に関数を書き留めます。 起こったことは次のとおりです。
public static IEnumerable<bool> Einstein(object houses) { return YP.unify(houses, List(_, _, _, _, _)) .And(Nth1(1, houses, List("norwegian", _, _, _, _))) .And(Member(List("englishman", _, _, _, "red"), houses)) .And(Nextto(List(_, _, _, _, "green"), List(_, _, _, _, "white"), houses)) .And(Member(List("dane", _, _, "tea", _), houses)) .And(Neighbors(List(_, _, "marlboro", _, _), List(_, "cat", _, _, _), houses)) .And(Member(List(_, _, "dunhill", _, "yellow"), houses)) .And(Member(List("german", _, "rothmans", _, _), houses)) .And(Nth1(3, houses, List(_, _, _, "milk", _))) .And(Neighbors(List(_, _, "marlboro", _, _), List(_, _, _, "water", _), houses)) .And(Member(List(_, "bird", "pallmall", _, _), houses)) .And(Member(List("swede", "dog", _, _, _), houses)) .And(Neighbors(List("norwegian", _, _, _, _), List(_, _, _, _, "blue"), houses)) .And(Member(List(_, "horse", _, _, "blue"), houses)) .And(Member(List(_, _, "winfield", "beer", _), houses)) .And(Member(List(_, _, _, "coffee", "green"), houses)); }
そして、ソリューションリクエスト自体:
var houses = new Variable(); var owner = new Variable(); Einstein(houses) // ? .And(Member(List(owner, "fish", _, _, _), houses)) // . .And(WriteLine(() => owner)) // . .And(WriteLine(() => houses)) .Count();
プログラムの結果:
ドイツ人
((ノルウェー語、猫、ダンヒル、水、黄色)、(デーン、馬、マールボロ、茶、青)、(英語、鳥、パルマル、ミルク、赤)、(ドイツ語、魚、ロスマン、コーヒー、緑)、(スウェーデン人、犬、ウィンフィールド、ビール、白))
コードの行数は、Prologソリューションとほぼ同じです。
ボンネットの下で何が起こりますか? このソリューションでは、
YieldPrologライブラリを使用し
ました 。これにより、パラメーターの統合と戻り値のある検索が可能になります。 prologプログラムの中間コンパイルはありません。 これは、C#の正直な解決策です。
パラメーターは、YP.unify(arg1、arg2)関数を使用して統合されます。 これが主な機能であり、ソリューション全体の中核と言えます。 実装は非常に単純ですが、ドキュメントで説明されているものよりも優れているため、まだ書きません。 そして、ドキュメント自体は小さいです。 したがって、私は彼女に言及しています。
ソリューションの最初の行は、houses変数を無関係な変数のリストに正確にバインドすることです。 リストを定義するために、タスクではすべてのリストが5つの要素のみで構成されるため、5つの引数を持つファンクターを使用します。 Listメソッドのコードは次のとおりです。
public static object List(params object[] list) { return Functor.make("", list); }
ここでFunctorはYieldPrologライブラリのクラスです。
これで、長いエントリFunctor.make(new [] {new Variable()、new Variable()、...})の代わりに、短いリスト(new Variable()、new Variable()、...)を使用できます。 リストをよりエレガントに見せるために、プロパティを追加しました:
public static Variable _ { get { return new Variable(); } }
統合変数のリストは、リスト(_、_、...)のように記述されます。
次に、述語メンバー(Elem、List)、nth1(N、List、Elem)、nextto(X、Y、List)、および隣接(X、Y、List)について説明します。 私は特に[head | しかし、私は通常の配列を使用します。Prologでの説明と同じ方法でこれらの述語の説明を繰り返すのに問題はないからです。
メンバー(Elem、List)は次のようになります。
public static IEnumerable<bool> Member(object elem, object list) { foreach (var e in YP.getFunctorArgs(list)) foreach (var l1 in YP.unify(elem, e)) yield return false; }
ここではすべてが非常に簡単です。 ループでは、リストアイテムを並べ替え、各アイテムをリストアイテムに統合します。
建設:
foreach (var l1 in YP.unify(elem, e)) yield return false;
2つの変数を統合するのに役立ちます。
nth1(N、List、Elem)は次のようになります。
public static IEnumerable<bool> Nth1(object n, object list, object elem) { int i = 0; foreach (var e in YP.getFunctorArgs(list)) { i++; foreach (var l1 in YP.unify(elem, e)) foreach (var l2 in YP.unify(i, n)) yield return false; } }
ここでも、リストの各要素を反復処理し、要素ごとに要素elemでそれを統合します。 ある値に関連付けられたパラメーターが既にあり、この値がリストのある要素の値と一致する場合、リスト内のこの要素の番号をパラメーターnで統一します。
nextto(X、Y、List)は次のように書かれています:
public static IEnumerable<bool> Nextto(object x, object y, object list) { var nativeList = YP.getFunctorArgs(list); var e1 = nativeList.FirstOrDefault(); foreach (var e2 in nativeList.Skip(1)) { foreach(var l1 in YP.unify(x, e1)) foreach(var l2 in YP.unify(y, e2)) yield return false; e1 = e2; } }
1つの要素ではなく2つで並べ替え、リストの2つの隣接する要素をパラメーターxとyで統合します。
ネイバー(X、Y、リスト)は、Prologソリューションと同じ方法で実装されます。
public static IEnumerable<bool> Neighbors(object x, object y, object list) { foreach (var l1 in Nextto(x, y, list)) yield return false; foreach (var l1 in Nextto(y, x, list)) yield return false; }
イテレータの使用は、ソリューションの非常に重要な部分です。 リターンを使用して検索を行うことができます。 2つの変数の統合が成功した場合、反復の次のステップが発生し、ネストされたループへの移行(存在する場合)が行われます。 統合が成功しない場合、このサイクルは中断され、外部サイクルの次の反復が進行します。
この問題を解決するには、15の述部が連続します。 YieldPrologでは、このために15のネストされたループを記述する必要があります。
foreach (var l1 in Pred1(...)) foreach (var l2 in Pred2(...)) … foreach (var l15 in Pred15(...)) yield return false;
それは不快でいです。 したがって、述語のチェーンを構築する補助メソッドAndを追加します。
public static IEnumerable<bool> And(this IEnumerable<bool> source, IEnumerable<bool> inner) { foreach (bool l1 in source) foreach (bool l2 in inner) yield return false; }
以上です。 Einstein関数を呼び出すことができます:
var houses = new Variable(); Einstein(houses);
そして、何も起こりません。 すべては、単にyieldを使用してイテレータを作成したが、反復ループを作成しなかったためです。 明示的に作成できます:
foreach (var l1 in Einstein(houses)) Console.WriteLine(houses);
または、Countメソッドを呼び出すだけで、イテレータを通過して反復回数をカウントできます。
さらに、イテレータを開始する前に、新しい条件を決定に追加できます。
Einstein(houses) // ? .And(Member(List(owner, "fish", _, _, _), houses)) // . .And(WriteLine(() => owner)) // . .And(WriteLine(() => houses)) .Count();
ヘルパー関数WriteLine:
public static IEnumerable<bool> WriteLine(Func<object> s) { Console.WriteLine(s()); yield return false; }
履歴書の代わりに
この記事は、論理タスクをC#で簡単に記述できるという質問から生まれました。 そして、ここで方法の一つをもたらしました。 私の目標は、最高のパフォーマンスまたはメモリソリューションを得ることではありませんでした。 Prologでプログラムを作成し、コンパイルし、ライブラリとしてプロジェクトに接続できるライブラリがあります。 組み込みのC#ツールを使用してソリューションを取得したかった。 インターネットを検索すると、この問題を解決するYieldPrologライブラリが見つかりました。 つまり、通常の構文を使用して、通常の言語で論理タスクを記述できます。 同時に、Prologの説明と同様にタスクを説明します。 はい、C#のタイプと機能を使用する機能があります。 はい、必要に応じてソリューションを最適化する機能があります。
もし興味があるなら、プログラムは私の古いAMD Turion 1.8GHzラップトップで0.1秒で実行できます。