Dでのコンパイル時の関数型プログラミング

今日は、D言語の主な機能の1つを検討します。これは、それが作成されたためです。これは、コンパイル段階での高度なプログラミングです。 一部の人は、C ++またはより複雑なLifeゲームの実装で階乗がどのように計算されるかを思い出し、怖がってしまうかもしれません。 Dのテンプレートは、C ++のアナログよりも桁違いにシンプルで強力ですが、それでも特別なアプローチが必要です。したがって、順応のために、素材の複雑さが徐々に増していきます。







問題の声明





Dは、たとえば、 構造型付け (静的型付けのアヒル型付けに似ています)を使用して、たとえば、操作タイプがforeachステートメントでそれをサポートしているかどうかを確認します。

import std.range; static assert(isInputRange!(uint[])); // true static assert(isInputRange!string); // true static assert(!isInputRange!void); // false
      
      







static assertは従来のassertの変形ですが、コンパイル段階で実行され、falseに等しい式が渡されると、コンパイルを停止します。 また、 isInputRangeは 、必要なメソッドの存在をチェックするテンプレートとして宣言されています(次の例に詳しく入ることはできません。すべての概念をさらに検討します)。

 template isInputRange(R) { enum bool isInputRange = is(typeof( (inout int = 0) { R r = void; // can define a range object if (r.empty) {} // can test for empty r.popFront(); // can invoke popFront() auto h = r.front; // can get the front of the range })); }
      
      







また、コンパイル時のインターフェイスごとに、1つ以上のチェックパターンを実行する必要があります。 これは少し面倒ですが、次のようにコンパイル時インターフェイスの実装を確認したいと思います。

 //    struct CInterface { string method1(); bool method2(); } //  / struct A {} static assert(isExpose!(A, CInterface));
      
      







ここでisExpose関数を実装し、同時にテンプレートプログラミングを詳しく調べます。



ウォームアップ



まず、テンプレートの階乗を計算しましょう:

  //       template factorial(uint n) { // - private template inner(ulong acc, uint n) { //       static  if static if(n == 0) enum inner = acc; //  else enum inner = inner!(acc * n, n-1 ); //    } //       1 enum factorial = inner!(1, n); } static assert(factorial!5 == 120);
      
      







テンプレートを記述するための鍵は、テンプレートの名前と同じ名前を持つ定数またはエイリアスを宣言することです;これは、通常の関数での戻り値に類似しています。 このテンプレートは、もう1つの内部を使用して(バッテリーを介して)末尾再帰を編成します。



基本的な型、型、型のリスト、そして最も興味深いことに、spiks式の値をテンプレートに転送できます。 値と型を使用すると、すべてが明確になります。これは多くの言語で行われますが、式リストを明確にする必要があります。

  template test(T...) {} alias a1 = test!(ulong, float, double); //    alias a2 = test!("hi!", 23+42, [true, false], float); //   
      
      





式リストを使用すると、コンパイル段階で計算できるテンプレートに必要なものを渡すことができます。 全体として、ほぼどこでも式のリストを処理します。



キャラクター操作



必要なisExposeテンプレートの収集を始めましょう:

  //    ,      template isExpose(Type, Interfaces...) { //     ,     1  private template isExposeSingle(Interface) { } //  ,     1     enum isExpose = allSatisfy!(isExposeSingle, Interfaces); }
      
      







allSatisfyテンプレートを見てみましょう。標準ライブラリで宣言されています:

 template allSatisfy(alias F, T...) { static if (T.length == 0) { enum allSatisfy = true; } else static if (T.length == 1) { enum allSatisfy = F!(T[0]); } else { enum allSatisfy = allSatisfy!(F, T[ 0 .. $/2]) && allSatisfy!(F, T[$/2 .. $ ]); //   // enum allSatisfy = F!(T[0]) && allSatisfy!(F, T[1 .. $ ]); } }
      
      





aliasキーワードで宣言される最初のパラメーターとして別のテンプレートを使用します。これは「名前で渡す」ことを意味します。 このキーワードがないと、コンパイラはFテンプレートが誤って適用されたことを誓い、 エイリアスを使用して、関数型言語での遅延計算の類似物取得します。 allSatisfyTの各要素にFを適用し、パターンFtrueを返すたびにチェックしますelseブランチで引数のリストを分割するのも奇妙に思えるかもしれません 。 この手法により、リストから1つの要素を直線的に噛み合わせる代わりに、バランスのとれた「呼び出しツリー」を構築するため、コンパイラー保護の応答を無限再帰に大幅に遅らせることができます。 それでも明確でない場合は、図を示します。







ここで、1つのコンパイル時インターフェイスの存在について、型チェックサブタスクを解決する必要があります。 まず、明示的に新しい式リストを作成する機能が必要です。これは、トリッキーなトリックで実行できます。

 //    template List(T...) { //     alias List = T; }
      
      







ここで、コンパイラのヘルプを使用して、インターフェイスメンバ(メソッドとフィールド)のリストを見つけます。

  template getMembers(T) { //     List alias getMembers = List!(__traits(allMembers, T)); }
      
      







__traits(allMembers、T)、T型のすべての内部要素のリストを返します 。特性の詳細については、 こちらを参照してください 。 これで、コンパイル時インターフェイスのメソッドとフィールドの名前はわかりましたが、これだけでは十分ではありません。インターフェイス要素の名前とチェック対象の型は一致しますが、型は一致しません。 要素タイプを名前にアタッチするには、単純なパイプラインを編成しますが、最初にいくつかの補助テンプレートが必要です。



引数をn回繰り返し、このコピーのリストを返すテンプレート:

コード
 template staticReplicate(TS...) { // is(T)  true,  T    static if(is(TS[0])) alias T = TS[0]; else //    enum T = TS[0]; enum n = TS[1]; static if(n > 0) { alias staticReplicate = List!(T, staticReplicate!(T, n-1)); } else { alias staticReplicate = List!(); } } /// Example unittest { template isBool(T) { enum isBool = is(T == bool); } static assert(allSatisfy!(isBool, staticReplicate!(bool, 2))); static assert([staticReplicate!("42", 3)] == ["42", "42", "42"]); }
      
      









2つのパラメーターを持つテンプレートをリストに適用するテンプレート:

コード
 template staticMap2(alias F, T...) { static assert(T.length % 2 == 0); static if (T.length < 2) { alias staticMap2 = List!(); } else static if (T.length == 2) { alias staticMap2 = List!(F!(T[0], T[1])); } else { alias staticMap2 = List!(F!(T[0], T[1]), staticMap2!(F, T[2 .. $])); } } /// Example unittest { template Test(T...) { enum Test = T[0] && T[1]; } static assert([staticMap2!(Test, true, true, true, false)] == [true, false]); }
      
      









テンプレートのアナログ折りたたみまたは縮小:

コード
 template staticFold(alias F, T...) { static if(T.length == 0) // invalid input { alias staticFold = List!(); } else static if(T.length == 1) { static if(is(T[0])) alias staticFold = T[0]; else enum staticFold = T[0]; } else { alias staticFold = staticFold!(F, F!(T[0], T[1]), T[2 .. $]); } }
      
      









複数のリストをテンプレートに転送すると、それらは自動的に開かれ、接着され、多くの場合、複数のリストでの操作の実装を妨げます。そのため、サブパターンが明示的に呼び出されたときに開かれる別の「ハード」ラッパーを宣言します:

 template StrictList(T...) { alias expand = T; }
      
      





このテンプレートでは、 StrictListという名前のエイリアスを宣言していません。このエイリアスを使用すると、このテンプレートをこのエイリアスに自動的に置き換えることはできません。 StrictList!(T、U).expandを呼び出すと、サブパターンとメソッドの類推もできます。TとUからリストを取得します。



前のテンプレートを使用して、最後の補助テンプレートを実装します。 彼は式のリスト(!)のリストを取り、引数の要素が順番に含まれる新しいリストを作成します(関数型言語のzip関数に類似):

コード
 //    StrictList,    template staticRobin(SF...) { //        private template minimum(T...) { enum length = T[1].expand.length; enum minimum = T[0] > length ? length : T[0]; } //          enum minLength = staticFold!(minimum, size_t.max, SF); //  ,    ,       private template robin(ulong i) { //       i private template takeByIndex(alias T) { //   ,       static if(is(T.expand[i])) alias takeByIndex = T.expand[i]; else enum takeByIndex = T.expand[i]; } static if(i >= minLength) { alias robin = List!(); } else { // staticMap!(takeByIndex, SF)    i-    alias robin = List!(staticMap!(takeByIndex, SF), robin!(i+1)); } } //   alias staticRobin = robin!0; } /// Example unittest { alias test = staticRobin!(StrictList!(int, int, int), StrictList!(float, float)); static assert(is(test == List!(int, float, int, float))); alias test2 = staticRobin!(StrictList!(1, 2), StrictList!(3, 4, 5), StrictList!(6, 7)); static assert([test2]== [1, 3, 6, 2, 4, 7]); }
      
      









コンベアに必要なレンガがすべて揃ったら、その図を描くことができます:





コンベアの最初の部分は次のように実装されます。

  alias intMembers = StrictList!(getMembers!Interface); alias intTypes = StrictList!(staticReplicate!(Interface, intMembers.expand.length)); alias pairs = staticMap2!(bindType, staticRobin!(intTypes, intMembers)); private template bindType(Base, string T) { alias bindType = List!(typeof(mixin(Base.stringof ~ "." ~ T)), T); }
      
      







インターフェース要素のタイプを取得するために、ドットを介してインターフェース名をメソッド名に接続する不純物を使用しました。 そしてtypeof演算子の助けを借りて、混合物で生成された式のタイプを取得します。 次に、タイプと名前のペアがテストされたクラス/構造に実際に存在するかどうかを確認します。

  template checkMember(MemberType, string MemberName) { static if(hasMember!(Type, MemberName)) { enum checkMember = is(typeof(mixin(Type.stringof ~ "." ~ MemberName)) == MemberType); } else { enum checkMember = false; } } enum isExposeSingle = allSatisfy2!(checkMember, pairs);
      
      







すべてのパズルのピースが所定の位置に収まり、完全なテンプレートコードが合計されました。

 template isExpose(Type, Interfaces...) { private template getMembers(T) { alias getMembers = List!(__traits(allMembers, T)); } private template isExposeSingle(Interface) { alias intMembers = StrictList!(getMembers!Interface); alias intTypes = StrictList!(staticReplicate!(Interface, intMembers.expand.length)); alias pairs = staticMap2!(bindType, staticRobin!(intTypes, intMembers)); private template bindType(Base, string T) { alias bindType = List!(typeof(mixin(Base.stringof ~ "." ~ T)), T); } template checkMember(MemberType, string MemberName) { static if(hasMember!(Type, MemberName)) { enum checkMember = is(typeof(mixin(Type.stringof ~ "." ~ MemberName)) == MemberType); } else { enum checkMember = false; } } enum isExposeSingle = allSatisfy2!(checkMember, pairs); } enum isExpose = allSatisfy!(isExposeSingle, Interfaces); }
      
      







そして使用例:

  struct CITest1 { string a; string meth1(); bool meth2(); } struct CITest2 { bool delegate(string) meth3(); } struct CITest3 { bool meth1(); } struct Test1 { string meth1() {return "";} bool meth2() {return true;} string a; bool delegate(string) meth3() { return (string) {return true;}; }; } static assert(isExpose!(Test1, CITest1, CITest2)); static assert(!isExpose!(Test1, CITest3));
      
      







おわりに



強力なメタプログラミングに基づいて、ボイラープレートコードを排除する便利なDSLまたはテンプレートを作成できます。 このアプローチを実践する素晴らしい例は、 ペグ化されたコンパイル時パーサージェネレーターです。



All Articles