LoginSignup
6
7

More than 5 years have passed since last update.

D言語の文字列mixinについて。

Posted at

D言語には文字列mixinという機能がある。
これは、動的な言語におけるeval関数のようなものである。つまり、与えられた文字列(D言語コードとして妥当なコード(これはその場に実際にコードとして記述した場合、コンパイル可能であることを意味する。
一つ注意が必要なのは、Dのコードとして妥当であるということは、D言語の文法に従っていることであり、エラーが起きないということではない。
もちろん、静的な範囲では通常のDのコードしてエラーチェックは行われるが、動的なエラーについては、普通のDコードと同様、実行時にしか発生しない)。
ただし、D言語は強い静的型付けの言語であるから、全ての型はコンパイル時に決定される。
したがって、コンパイル時に与えられた文字列が決定されていなければならない。
つまり、動的型付けの言語のように標準入力をevalするというようなことはできない。
文字列mixinには幾つかの使い方がある。標準ライブラリでもよく使われているような評価式を文字列として渡すような使い方もあるが、ある意味C言語のマクロ的な使い方も可能である。

ここでは、評価式を文字列として渡すようなコードとマクロ的な使い方についての説明を行う。

評価式を文字列として渡す

評価式というのを、条件式と読み替えても良い。即ち、a < b, a == bなどといったようなものである。
評価式を文字列として渡すと何が嬉しいのだろう。その答えは関数を汎用的に扱うこと、すなわち抽象化出来るということにある。

たとえば、2項演算(引き数が2つの手続き)を行うbinFunを考えてみよう。プログラムにおいて最も簡単かつ身近でありながら、あまり多くの人が2項を引数に取る手続きとして意識していることは少ないであろう演算といえば、四則演算があるだろう。
すなわち、a + b, a - b, a * b, a / b。ここでは、この四則演算も関数のように考えよう。
そうすると、+ (a, b), - (a, b), * (a, b), / (a, b)左のように書くと、ポーランド記法で上の式を書き直したような感じになる。このように書けば、四則演算を2項を引数に取る手続きとしてみることが出来るだろう。
プログラムにおいて一般的な2引き数の関数を呼び出す場合には、2つの項を引数に取る関数f,gがあるとしてf (a, b), g (a, b)としてよびだすだろう。

さて、2項を引数に取る関数を抽象化しよう。2つの項を引数に取る関数をFとし、その引き数となる項をa, bとする。
抽象化するとこのようになる。

import std.traits;//ReturnType, isCallable

ReturnType!f binFun(alias f, T, U)(T a, U b) if (isCallable!f){
  return f(a, b);
}

unittest {
  int f(int a, int b) {
    return a * b;
  }

  string g(string a, string b) {
    return a ~ b;
  }


  assert (binFun!f(10, 20) == 200);
  assert (binFun!g("abc", "def") == "abcdef");
}

さて、ここで一つ問題が生ずる。

先ほど、僕は四則演算を関数のように書いてみたが、これをDのコードでやろうとすると失敗する。
具体的には、binFun!+(10, 20) == 30というコードはD言語として妥当なコードではない。
というのも、D言語では+,-,*,/は関数としては提供されていない。よって、+はテンプレートの引き数としては渡すことが出来ない。では、どのように解決すればよいのだろうか。

ここで、ラムダ式(無名関数)の利用が思い浮かぶことだろう。
すなわち、binFun!+(10, 20)のかわりに、 binFun!((int a, int b) => a + b)(10, 20)とかけばよい。

まだ文字列mixinが出てきてないが、とりあえずこのようにすれば抽象化出来ることはわかっていただけただろう。

さて、次は文字列mixinを用いた説明を行う。

上の例で見たように、四則演算(うえでは説明しなかったが、加えて比較演算子も同様)を評価式として渡す場合、ラムダ式を渡せば、上の例では対応できた。しかし、裏を返せばラムダ式を渡せねばならない、ラムダ式を書かねばならないのである。
D言語には型推論もあり、静的なリフレクションも可能な言語であるため、文字列mixinを用いることでラムダ式の代わりに、評価式そのものを渡すことが可能になるのだ。
まだ実装していないため次に書くコードは現時点では動かないが、このようなことが可能になる:
binFun!"a + b"(10, 20) == 30

では、実装を行おう。
D言語のテンプレートと型推論を活用することで、上のコードが実現可能になる。

実装は次の通り:

auto binFun(string s, T, U)(T a, U b) if (is(typeof(mixin(s)))) {
  return mixin(s);
}

このような実装をすることで、binFun!"a + b"ということが可能になる。
じつは、上に書いたようなbinFunと同じように動くテンプレートがD言語の標準ライブラリでstd.functional.binaryFunとして提供されている。

上のコードを解説していこう。
まず、binFunというテンプレートはテンプレート引数として(文字列s, 型T, 型U)を要求する。要求と書いたが、推論可能な型に関するパラメーターは省略可能なので、実質的に文字列sを渡すことでテンプレート引数の部分は解決する。
また、binFunは関数テンプレートなので、関数としての引数も要求し、それは(T型のa, U型のb)である。
そして、その後に続くifはTemplateConstraintとよばれ、テンプレート引数に対する制約を課すことが出来る。そして、その制約とはifの中の式である。
半ばイディオム的に使われているがis(typeof(expr))とは、exprがD言語として妥当な式である場合に、trueを返す。ここでは、sという文字列で与えられた式がD言語のコードとして解釈可能かを判断している。ここでようやくmixinが登場する。
上で述べたように、文字列mixinはその場に文字列を埋め込むことが出来る。よって、binFun!"a+b"と書いた場合、if(is(typeof(a + b)))というように展開される。
ここで、2つの変数を足すというコードはD言語の式として妥当であるため、a + bというコードはD言語のコードとしては妥当である。
注意すべきは、typeof(expr)はexprのを返す。よってここではa + bの型を返す。そして、その方が妥当である場合isが真を返す。あくまでもTemplate Constraintには型に関する条件のみを書くことができる。よって、その変数の値にアクセスすることは出来ない(a * b < 20みたいな式は書くことが出来ないということ)。理由は単純でコンパイル時にアクセス出来ないからだ。

Template Constraintの場所と同様に、関数テンプレートの本体、returnのところにもmixin(s)とあるが、ここも同様にa + bと展開される。

D言語の多くのテンプレートは先ほどの文字列をテンプレート引数で渡すことをサポートしている。
とくに、std.algorithmの関数を使う時は有用である。
例えば、std.algorithm.iterationmapfilterなど頻出するテンプレートでは非常に有用だ。

import std.algorithm,//filter
       std.range,//iota
       std.stdio;

void main() {
  /*
    N.iotaは[0, N)
  */
  //E = {a | 0 <= a< 100, a %2 == 0}
  writeln(100.iota.filter!"!(a%2)");
  //O = {a | 0 <= a< 100, a %2 == 1}
  writeln(100.iota.filter!"a%2");
}

一つだけ注意が必要なのは、文字列として渡す評価式に既存の関数の呼び出しを含めることは出来ない。
たとえば、

import std.algorithm,//filter
       std.range,//iota
       std.stdio;

bool f(int x) {
  return true;
}

void main() {
  writeln(100.iota.filter!"f(a)");
}

これをするとfがundefinedとなり、コンパイルエラーが発生する。
この場合は

import std.algorithm,//filter
       std.range,//iota
       std.stdio;

bool f(int x) {
  return true;
}

void main() {
  writeln(100.iota.filter!(x => f(a)));
}

とラムダ式を書く必要がある。

このような、関数を引数に取るような関数を高階関数という。関数を高階関数にすることは、プログラムをFlexibleにし、抽象化をもたらし、再利用性が向上する。

文字列mixinをマクロ的に使う

ここまでを読んでいただけると、mixinがどういうものかは理解していただけただろう。
さて、文字列mixinをマクロ的に使おう。
つまり、あとでmixinする文字列を生成するCTFEに対応した関数を作ればコンパイル時に自動でコードを生成することが出来る。
コンパイル時にコードを生成することが出来ると何が嬉しいかについて、色々と例を上げてみよう。そして最後に、文字列mixinを応用した例として無名クラス(D言語にも無名クラスは存在する)を実現するテンプレートと、さらに静的リフレクションを活用した例として、任意の関数をカリー化するテンプレートについて説明する。

簡単な例を見てみよう。

import std.stdio;

string generateFunction_0(string funcName, string functionBody) {
  return
    "auto " ~ funcName ~ "() {"
    ~ functionBody
  ~ "}";
}

mixin(generateFunction_0("f", "return 100 * 200;"));

void main() {
  writeln(f);
}

これは、引き数を取らない関数の文字列を生成する関数があり、その関数が返す文字列をmixinすることで、コンパイル時に関数fを生成し、mixinでそれを実体化している。
もちろん、UFCSが使えるから

"g".generateFunction_0("return 100 * 200;")

というように書くことだって出来るのだ。

さて、これを引き数を取れるようにして汎用的な関数を作ってみよう。

generateFunction.d
import std.stdio;
import std.algorithm,
       std.typecons,
       std.array,
       std.conv;

static string generateFunction(string funcName, string functionBody) {
  return
    "auto " ~ funcName ~ "() {"
    ~ functionBody
  ~ "}";
}

static string generateFunction(ArgsList)(string funcName, string functionBody) {
  static if ({
    foreach (field; ArgsList.fieldNames) {
      if (field == "") {
        throw new Error("Invalid ArgsList was given");
      }
    }

    return true;
  }()) {
    enum argsLabels = {
      string[] argsLabels;

      foreach (field; ArgsList.fieldNames)
        argsLabels ~= field;

      return argsLabels;
    }();

    return generateFunction!(ArgsList, argsLabels)(funcName, functionBody);
  }
}

static string generateFunctionParameters(ArgsList, string[] argsLabels)() {
  enum types = {
    string temp;

    foreach (i, type; ArgsList.Types) {
      temp ~= (i ? "," : "") ~ type.stringof;
    }

    return temp;
  }().split(",");

  enum parameters = {
    string temp;

    foreach (i, argLabel; argsLabels) {
      temp ~= (i ? ", " : "") ~ types[i] ~ " " ~ argLabel;
    }

    return temp;
  }();

  return parameters;
}

static string generateDelegate(string functionBody) {
  return "{" ~ functionBody ~ "};";
}

static string generateDelegate(ArgsList, string[] argsLabels)(string functionBody) {
  enum parameters = generateFunctionParameters!(ArgsList, argsLabels);

  return "(" ~ parameters ~ ") {"
        ~  functionBody
        ~ "}";
}

static string generateFunction(ArgsList, string[] argsLabels)(string funcName, string functionBody) {
  return "auto " ~ funcName ~ generateDelegate!(ArgsList, argsLabels)(functionBody);
}

void main() {
  mixin(generateFunction("f", "return 100 * 200;"));
  writeln(f());
  mixin("g".generateFunction("return 100 * 200;"));
  writeln(g());

  alias ixiy = Tuple!(int, "x", int, "y");
  alias ii   = Tuple!(int, int);

  mixin(generateFunction!(ii, ["x", "y"])("Z1", "return x * y;"));
  Z1(20, 20).writeln;

  mixin(generateFunction!(ixiy)("Z2", "return x * y;"));
  Z2(30, 30).writeln;

  mixin("auto dlg = " ~ generateDelegate(`return "abc";`));
  writeln(dlg());
}

えっと... 気がついたらこのようなコードが完成していた...
これは汎用的な関数とdelegateを生成するテンプレート群である。
使い方はサンプルにある通り。
テンプレート内でenumな値をやり取りしているのはCTFEのため。

この例はわかりにくい(いきなり応用になってしまった)ので、別の例を見てみよう。

次のコードは、拙作のJSONパーサーでD側の内部表現のためのクラスを実装した際にもmixinをマクロ的に使ったコードである。

private static genSetValueString(T, alias R, alias L, R2)() {
    return "void setValue(" ~ T.stringof ~ " value) {"
         ~ "this.type        = JSONNodeValueType." ~ R.stringof ~ ";"
         ~ "this." ~ L.stringof ~ " = new " ~ R2.stringof ~ "(value);"
         ~ "}";
  }

  private static genSetValueString(T, alias R, alias L)() {
    return "void setValue(" ~ T.stringof ~ " value) {"
         ~ "this.type        = JSONNodeValueType." ~ R.stringof ~ ";"
         ~ "this." ~ L.stringof ~ " = value;"
         ~ "}";
  }

  mixin(genSetValueString!(float,  JSONNodeValueType.Numeric, jsonNumeric, JSONNumeric));
  mixin(genSetValueString!(string, JSONNodeValueType.String,  jsonString,  JSONString));
  mixin(genSetValueString!(bool,   JSONNodeValueType.Boolean, jsonBoolean, JSONBoolean));
  void setValue(JSONNULL   value) { this.type = JSONNodeValueType.NULL; }
  mixin(genSetValueString!(JSONArray,  JSONNodeValueType.Array,      jsonArray));
  mixin(genSetValueString!(JSONObject, JSONNodeValueType.JSONObject, jsonObject));

クラスのsetterとかgetterを書く場合に、非常に便利だと思う。

上で既に静的リフレクションと文字列mixinを応用した例を提示してしまったため(関数を生成する文字列mixin)、さらなる応用というわけではないがその他の例として無名クラスを文字列mixinで実現してみた例と任意の関数をカリー化するテンプレートを最後に付録として掲載する。

無名クラスを文字列mixinで実現する例

import std.algorithm,
       std.typecons,
       std.string,
       std.array,
       std.range,
       std.conv;

private class EmptyArgument {}

private template AnonymousClassImpl(
    string BaseClassName,
    string bodyString,
    alias  arguments = tuple()
  ) {
  enum AnonymousClassImpl = () {

    string generateString(
          string parameters      = null,
          string argumentLabels  = null,
          string argumentValues  = null
        ) {
      if (
          parameters     !is null
       && argumentLabels !is null
       && argumentValues !is null) {
        return 
          "{"
        ~   "class AnonymousClassMain : " ~ BaseClassName ~ "{"
        ~     "this(" ~ parameters ~ ") {"
        ~       "super(" ~ argumentLabels ~ ");"
        ~     "}"
        ~     bodyString
        ~   "}"
        ~   "return new AnonymousClassMain(" ~ argumentValues ~ ");"
        ~ "}()";
      } else {
        return 
          "{"
        ~   "class AnonymousClassMain : " ~ BaseClassName ~ "{"
        ~     "this() {"
        ~       "super();"
        ~     "}"
        ~     bodyString
        ~   "}"
        ~   "return new AnonymousClassMain();"
        ~ "}()";
      }
    }

    static if (arguments.length == 0) {
      return generateString;
    } else {
      immutable types = {
        string temp;

        foreach (i, argument; arguments.Types) {
          temp ~= (i ? "," : "") ~ argument.stringof;
        }

        return temp;
      }().split(",");
      immutable argumentLabels = arguments.length.iota.map!(i => " arg" ~ i.to!string).array;
      immutable argumentValues = arguments.stringof.replace("Tuple", "");
      immutable parameters = {
        string temp;

        foreach (i, argument; arguments) {
          temp ~= (i ? "," : "") ~ types[i].to!string ~ argumentLabels[i];
        }

        return temp;
      }();

      return generateString(
            parameters,
            argumentLabels.join(","),
            argumentValues
          );
    }
  }();
}

auto AnonymousClass(
      string BaseClassName,
      string bodyString,
      alias  arguments = tuple()
    )() {
  static if (arguments.length == 0) {
    return mixin(AnonymousClassImpl!(BaseClassName, bodyString));
  } else {
    return mixin(AnonymousClassImpl!(BaseClassName, bodyString, arguments));
  }
}

import std.stdio;

class T1 {
  this(string v) {
    writeln("T1! - ", v);
  }

  void func() {}
}

class Tx1 {
  this() {
    writeln("Tx1");
  }
  void func() {}
}

void main() {
  T1 t = AnonymousClass!("T1", q{
        override void func() {
          writeln("Overrided T2.func!");
        }
      },
      tuple("T2"));
  Tx1 tx = AnonymousClass!("Tx1", q{
      override void func() {
          writeln("Overrided Tx2.func!");
      }
    });

  t.func;
  tx.func;
}

任意の関数をカリー化するテンプレート

import std.algorithm,
       std.conv,
       std.range,
       std.traits;

template curry(alias func){ 
  immutable lambdaStr = {
    alias argsintuple = ParameterTypeTuple!func;
    enum  lamArgs     = argsintuple.length.iota.map!(i => "arg" ~ i.to!string);
    string temp;

    foreach(i, e; argsintuple) {
      temp ~= "(" ~ e.stringof ~ " " ~ lamArgs[i] ~ ") => ";
    }

    return temp ~ "func(" ~ lamArgs.join(", ") ~ ")";
  }();

  enum curry = mixin(lambdaStr);
}

int f(int x, int y, int z) {
  return x * y * z;
}

void main() {
  import std.stdio;
  writeln(curry!f(1)(2)(3) == 6);
}
6
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
7