std::randomを簡単に使うためのラッパーファンクタ

まえがき

 std::randomの乱数生成がめんどくさい。それだけ。deviceとengineの一連のオブジェクト生成手続きと乱数生成時の関数呼び出しがカプセル化されてないからなので次のようなクラステンプレートを書く。c++の乱数生成器の初期化とパラメータの変更において、分布ごとに必要なパラメータの数が違うからそこはvariadic templateの出番。(ちなみに調べた感じだと多くて二つ。あとはdeviceとengineの型はデフォルトテンプレート引数を使っている。変えたくなったらそこの部分をテンプレートパラメータで指定すれば良い)

#include <random>

// variadic templateで受け取った分布の初期化に必要なパラメータを下のstaticメンバに流し込んで初期化に用いる
template <typename DistType>
struct dist_creator {
    template <typename T1>
    static DistType create(T1 arg1) {
        return DistType(arg1);
    }

    template <typename T1, typename T2>
    static DistType create(T1 arg1, T2 arg2) {
        return DistType(arg1, arg2);
    }
};

// variadic templateで受け取った引数の値から、分布のパラメータを変更する用のstd::parameterクラスを生成する(この並び順の意味は各分布によって違う
template <typename DistType>
struct dist_param_creator {
    template <typename T1>
    static typename DistType::param_type create(T1 arg1) {
        return typename DistType::param_type(arg1);
    }

    template <typename T1, typename T2>
    static typename DistType::param_type create(T1 arg1, T2 arg2) {
        return typename DistType::param_type(arg1, arg2);
    }
};

// クライアントコードが使う乱数モジュール。上のstruct二人組はネストクラスでよかったかもしれない笑
template 
<
    typename DistType,
    typename EngineType = std::default_random_engine,
    typename DeviceType = std::random_device
>
class random_generator {
  public:
    template <typename... Args>
    random_generator(Args... args) 
      : seed_gen{}, engine{seed_gen()}, 
        dist{dist_creator<DistType>::create(args...)} {}

    auto operator()() {
        return dist(engine);
    }

    template <typename... Args>
    void param(Args... args) {
        dist.param(dist_param_creator<DistType>::create(args...));
    }

  private:
    DeviceType seed_gen;
    EngineType engine;
    DistType dist;
};
     

このモジュールを使う側のクライアントコードはこんな感じ。割と楽になった(?)

    ...
    random_generator<std::normal_distribution<float>> normal_dist(0, 1);
    // 平均0、分散1の正規分布による乱数を出力
    for(int i = 0; i < 10; ++i)
        std::cout << normal_dist() << std::endl;
    // パラメータを変更。平均100、分散10で出力もちゃんと反映される
    normal_dist.param(100, 10);
    for(int i = 0; i < 10; ++i)
        std::cout << normal_dist() << std::endl;
    ...

メルセンヌツイスターとかいうエンジンで形状母数、尺度母数それぞれ1.0のガンマ分布を返すようにしたければ次のように書く(返り値は今回はdouble型)

    ...
    random_generator<std::gamma_distribution<double>, std::mt19937> gdist(1.0, 1.0);
    ...

ゴールデンウィークの進捗はsilicon valleyを見たことです(白目)

C++のindex_sequenceとtupleとarrayと

まえがき

 この3つはコンパイル時に構造が確定された型であり、そのコンパイル時の構造はというと大抵関数オーバーロードした際の型推論から取得することができる。多分一番耳慣れないのはindex_sequenceだと思われるのでこれの性質についてササっと論じた後で上手いことこれを使えばtupleの前順、後ろ順でイテレートが実現でき、さらにこのイテレートを用いてある特殊な状況下でarrayもこれに参加していい感じのことが行えることを示す。

index_sequenceについて

 index_sequenceとはsize_t型(すなわち0と自然数)の任意の並びの数字列をコンパイル時の情報として格納できるvariadic templateで定義されるクラスであり、utilityヘッダの中にいる。またこれのヘルパ関数としてmake_index_sequenceがあるが、これはテンプレート実引数に整数値Nを取ることで0からN-1の整数列を型情報として持つindex_sequence型を生成する。下のような関数を定義した場合の出力を例示すればイメージが湧きやすいかもしれない。

#include <utility>
#include <iostream>

v||oid print_index_sequence(std::index_sequence<>)
{}

template <std::size_t Head, std::size_t... Tails>
void print_index_sequence(std::index_sequence<Head,Tails...>)
{
    std::cout << Head << std::endl;
    print_index_sequence(std::index_sequence<Tails...>());
}

 コンパイル時計算を行う際のポイントとしては、基本的に関数型的思考でコーディングを行う。関数オーバーロード型推論を行うことで取り出せるコンパイル時型情報(上の例ではstd::size_t型のHeadとstd::size_t型のパラメータパックであるTails...)のHeadを取り出したうえで、次は再帰的にTails...を使って自分自身(ここで言う自分とは関数print_index_sequenceのこと)に適用していく。Schemeのcarやcdr、Hadkellのheadやtailをイメージしてもらえればわかりやすいと思う。まず何かしらの「集合」が与えられて「集合」の先頭であるところの「要素」に何かしら関数を適用(上だとcout)、そしてその後、先頭要素を外した後ろに残っている「集合」に関数を適用して、、、というようなサイクルであり、末端に達するすなわち関数を適用すべき集合が空集合になった(基底)場合は再帰の終了である。(上側の何もしない関数)。ただしコンパイル時の型情報ということで、動的な意味でのオブジェクトの情報は一切取得していない。関数テンプレートに型推論を行わせ、その推論結果だけを用いているところはC++っぽいところ。
 上記関数に対してmain()関数内で次のように書くと

int main()
{
    print_index_sequence(std::make_index_sequence<10>());
    print_index_sequence(std::index_sequence<43,23,1,999>());
}

0から9までの整数と43,23,1,999が改行されて出力される。

tuple型内部のデータを前、後順で出力する。

 これでtupleに対して何かしらのイテレート操作を行う準備はできた。簡単な話で、variadic template関数でtupleが保持する型情報をパラメータパックとして渡したうえで、index_sequenceをヘルパー関数内の引数の初期化にtupleオブジェクトと共に呼び出し、Headをstd::get関数から使えばいい。print_tuple関数に対してtuple型を渡した場合、そこに格納されているすべての型がoperator<<をオーバーロードしているという条件で、格納されたオブジェクトを出力してくれる。(この緩めの型制約はEffective C++で暗黙のインターフェースとかいう言われ方をされてる)

template <typename TupleLike>
void print_tuple_helper(const TupleLike& tup, std::index_sequence<>)
{}

template <typename TupleLike, std::size_t Head, std::size_t... Tails>
void print_tuple_helper(const TupleLike& tup, std::index_sequence<Head,Tails...>)
{
    std::cout << std::get<Head>(tup) << std::endl;
    print_tuple_helper(tup, std::index_sequence<Tails...>());
}

template <typename... Types>
void print_tuple(const std::tuple<Types...>& tup)
{
    print_tuple_helper(tup, std::make_index_sequence<sizeof...(Types)>());
}

 これに対してmain()関数内で次のように書くと

int main()
{
  auto tup = std::make_tuple(1, 3.14, "hello world!", 'a');
}

出力は1, 3.13, hello world!, aが順番に改行されて出力される。
 逆順イテレートもここまで来れば簡単で、木構造のtraverse実装における「帰りがけ」をイメージしてreverse_print_tuple_helper要素への関数適用と再帰呼び出しの順序を逆転させればすぐに実現できる。

template <typename TupleLike>
void reverse_print_tuple_helper(const TupleLike& tup, std::index_sequence<>)
{}

template <typename TupleLike, std::size_t Head, std::size_t... Tails>
void reverse_print_tuple_helper(const TupleLike& tup, std::index_sequence<Head,Tails...>)
{
    reverse_print_tuple_helper(tup, std::index_sequence<Tails...>()); 
    std::cout << std::get<Head>(tup) << std::endl;
}

template <typename... Types>
void reverse_print_tuple(const std::tuple<Types...>& tup)
{
    reverse_print_tuple_helper(tup, std::make_index_sequence<sizeof...(Types)>());
}

Heterogeneousな複数の型のメソッドが返すHomogeneousな型の値の集合をarrayに格納する

 今out()という共通のシグニチャを持つメソッドが実装された型が複数定義されているとする。

struct out1 { int out() const { return 1; } };
struct out2 { int out() const { return 2; } };
struct out3 { int out() const { return 3; } };
struct out4 { int out() const { return 4; } };
struct out5 { int out() const { return 5; } };

 これらは型として眺めた場合は互いに異なるが、共通のメソッドを持ち、なおかつそれらの返り値も同じ型である。例えばこれらのout()メソッドの出力をarrayに引数順に格納して取得しようと思うならば、これまでのテクニックを使うと次のように実装できる。outcalls2array関数は任意の数のint out()シグニチャを満たすメソッドが提供された型を受け取り、その結果を引数に受け取ったオブジェクトの順にそれらが呼び出すout()メソッドの値を格納したarrayを返す。実際に受け取った全てのオブジェクトがoutをメソッドとして持っていて、それがint型を返すかどうかはコンパイル時にチェックされる。

template <typename ArrayLike, typename TupleLike>
void tuplecalls2array_helper(Array& arr, const Tuple& tup, std::index_sequence<>)
{}

template <typename ArrayLike, typename TupleLike, std::size_t Head, std::size_t... Tails>
void tuplecalls2array_helper(ArrayLike& arr, const TupleLike& tup, std::index_sequence<Head,Tails...>)
{
    arr[Head] = std::get<Head>(tup);
    tuplecalls2array_helper(arr,tup,std::index_sequence<Tails...>());
}

template <typename TupleLike>
auto tuplecalls2array(const TupleLike& tup)
{
    auto outval = std::get<0>(tup).out();
    std::array<decltype(outval), std::tuple_size<Tuple>::value> result{};
    tuplecalls2array_helper(result, tup, typename std::make_index_sequence<std::tuple_size<Tuple>::value>());
    return result;
}

template <typename... T>
auto outcalls2array(const T&... params)
{
    auto tup = std::make_tuple(params...);
    return tuplecalls2array(tup);
}

今outcalls2array関数を用いてmain()を次のように書くと

int main()
{
    auto outs = outcalls2array(out1{}, out2{}, out5{});
    for(const auto elem: outs)
        std::cout << elem << std::endl;

out1,out2,out5が提供している共通のメソッドout()の出力である1,2,5が順番に出力される。パラメータパックに対するmake_tuple、variadic templateの先頭要素+後方集合への分解と型推論はクライアントコードから見たモジュールの扱いやすさをかなり向上させてくれている。

Paul GrahamのAmazonレビュー→『計算機プログラムの構造と解釈』

https://www.amazon.com/review/R3G05B1TQ5XGZP/ref=cm_cr_dp_title?ie=UTF8&ASIN=0262510871&nodeID=283155&store=books

ハッカーと画家」で有名なPaul Grahamさんが計算機プログラムの構造と解釈のAmazonレビューをされていたので翻訳

 

-この本は計算機科学の古典のうちで最良のものの一つである。私は15年前にこの本の第一版を購入したが、この本が教えてくれるべき全てのことはまだ学べていないと感じている(レビュー当時2000年)。私は既にLispに関して4つか5つの星がつくほどの二冊の本を書けるようになったが、我々の世界の聖書であるところのSICPが星3つだって?なんでそんなことがありえるんだ?

 レビューを読んで何が起こっているのかはっきりわかったよ。どこかの楽観的な大学教授が、まだ準備のできていない生徒達にそれを与えてきたんだ。でも、どれほど多くの思慮深い人がこの本の輪郭をはっきりと描きだす為に名乗り出てきたかを見ると、非常に励みになる。

 このことを大学生にもわかるように置き直した場合をちょっと見てみようじゃないか -- 問題は設定された。

1、Keneth Clarkは頭の良い大勢の人々があなたが好きでない何かを好きである場合、彼らがその中に何を見ているのかを理解すべきだと言った。SICPの擁護者の人たちの主張を10個ほどリストアップしてみよう。

2、(Donald) Knuthや(Dennis) Ritchie、(Brian) Kernighanが書いたようなアルゴリズムの教科書とは違う、SICPが重点を置いている部分は何だろう?

3、他のどんな本がこの要求を満たしているか?

4、1980年代中頃に出版されたプログラミングの本で、他に今なお適切であるようなものがあるかい?

5、この本で出てきている概念がScheme以上により良く表せるのか?

6、alって誰だ?何で奴の名前は小文字なんだ?

ブログ開設

最近、会社で色々と勉強することが増えてきたので、アウトプットの場としてプログラミング関係の記事や日々の雑感でも書こうかなと思い始めました。

ブログ自体初めてなのですがちょくちょくやっていこうと思います(できるのか?笑

よろしくお願いします