パターンマッチングの提案(P1371R3)を眺める(WIP)

※注意 : この記事はまだ執筆中です。タイトルの WIP が外れることを願っています。

個人的に気になったので、 P1371R3 : Pattern Matching についてメモ。
基本的に雑な和訳ですが、分かり易さのために改変したり注や例を挟むことがあります。


動機と領域

まず、既存の switchif などの条件分岐の構文では、単体の整数や真偽値による判定しかできず、標準ライブラリの提供する様々な型 (原文 : "vocabulary types") たちの検査には不十分です。

C++17 では構造化束縛宣言が導入され、tuple-like な型の要素を名前に束縛することができるようになりました。この提案では inspect 式により 検査束縛 (原文 : structured inspection) を行うことで構造化束縛の概念を自然に拡張することを目指しています。

デザイン概要

基本文法

inspect 式の基本的な文法は以下のようになります。1

inspect constexpropt ( init-statementopt condition ) trailing-return-typeopt {
    pattern guardopt => statement
    pattern guardopt => !opt { statement-seq }
    . . .
}

guard:
    if ( expression )

基本モデル

inspect 式の丸括弧の中は switchif の丸括弧の中と基本的に等価ですが、変換も整数昇格も行われません。

筆者例

char c;

// 整数昇格が発生 (char -> int)
switch (c) {
  /* ... */
}

// 変換が発生 (char -> bool)
if (c) {
  /* ... */
}

// char のまま処理される
inspect (c) {
  /* ... */
};

// 丸括弧の中身は switch や if と同じ
inspect (int m = c; long n = m) {
  /* ... */
};


inspect は全ての文脈において式であり、内包された文に依存して void または値を返します。値を返す場合、その型は内包された文から静的に推定されるか、 trailing return type によって指定されます。 pattern の返す型は全て一致している必要があります (原文 : the return types of all patterns must match) が、もし trailing return type が提供されている場合は暗黙に変換できる型である必要があります。 (この推定はラムダ式の戻り値の推定と類似しています。) また、複合文へと制御を移す pattern は void を返します。

筆者例:

inspect (/* ... */) { /* ... */ };
// セミコロン必須!              ^^^

int n;

// OK, x の型は int
auto x = inspect (n) {
   0 => 314;
   1 => 271;
  __ =>  42;
};

// NG, 型が異なる
auto y = inspect (n) {
   0 => 3.14;
   1 => 2.71;
  __ =>   42;
};

// OK, z の型は double
auto z = inspect (n) -> double {
   0 => 3.14;
   1 => 2.71;
  __ =>   42;
};


もし ! 接頭辞が複合文の前に現れた場合、その文は inspect 式の戻り値型の推定に寄与しません。そのような文は値を返すことが期待されておらず、 inspect 式を内包する関数から戻る、例外を投げる、プログラムを中断する、のいずれかによって処理を中断しなければいけません。これによってユーザーは、マッチがない場合の挙動や望まないマッチに対する挙動を inspect 式の結果に影響を与えることなく表現することができます。もし制御がこの複合文の最後に到達した場合、 std::terminate が呼ばれます。

筆者例:

// 提案書に記載されている例
enum class Op { Add, Sub, Mul, Div };
Op parseOp(Parser& parser) {
  return inspect (parser.consumeToken()) {
    '+' => Op::Add;
    '-' => Op::Sub;
    '*' => Op::Mul;
    '/' => Op::Div;
    token => !{
      std::cerr << "Unexpected: " << token;
      std::terminate();
    }
    // 筆者注:ここでは明示的に std::terminate を呼んでいるが、
    //   呼ばずとも複合文の終了時に std::terminate が呼ばれる
  };
}

// 筆者例

std::string func(int n) {
  return std::to_string(
    inspect (n) {
      // return により関数から脱出
      0 => !{ return "zero"; }
      // 例外を投げる
      1 => !{ throw std::logic_error("one"); }
      // 上2つは inspect 式の結果の型に影響しない
      v => v;
    }
  );
}


inspect 式が実行されると、まず condition が評価され、パターンに対し現れた順にマッチが行われます。あるパターンがこのマッチに成功し、かつ guard 内の式が true に評価された場合 (または guard が存在しない場合) 、式の値が返るか、複合文に制御が移されます (これはその検査が値を返すかどうかに依存します) 。もし guard 内の式が false に評価された場合、制御は後続のパターンに進みます。
筆者注:後述の筆者例を参照してください


もしいずれのパターンにもマッチしなかった場合、式や複合文のいずれも実行されません。このとき、 inspect 式が void を返す場合は制御は次の文に移ります。そうでなく inspect 式が void を返さない場合、 std::terminate が呼ばれます。

void f(int n) {
  std::cout << "try inspection..." << std::endl;
  inspect (n) {
    // 常にマッチしない
    v if ( false ) => { std::cout << "never match" << std:endl; }
  };  // void が返るので制御は次の文へ
  std::cout << "inspection done" << std::endl;
}

int g(int n) {
  return inspect(n) {
    // 常にマッチしない
    v if ( false ) => 42;
  }; // int を返すべきだが、マッチなしなので std::terminate が呼ばれる
}

パターンの種類

Primary Patterns

Wildcard Pattern

ワイルドカードパターンは以下の形式を持ち、

__

どんな値 v にもマッチします。

int v = /* ... */;
inspect (v) {
    __ => { std::cout << "matches any value" << std::endl; }
//  ^^ wildcard pattern
};

この提案書ではワイルドカードの識別子として __ を採用しています。この提案書の筆者はワイルドカードとして _ を予約しようと試みましたが、 EWG に固く反対されました。

Identifier Pattern

識別子パターンは以下の形式を持ち、

identifier

どんな値 v にもマッチします。この identifierv を参照する lvalue として振る舞い、そのスコープは宣言された場所からパターンラベルに続く文の最後までです。

int v = /* ... */;
inspect (v) {
    x => { std::cout << x; }
//  ^ identifier pattern
};

ノート:もし識別子パターンがトップレベルで使われた場合、 goto のラベルと同じ構文を持ちます。2

Expression Pattern

式パターンは以下の形式を持ち、

constant-expression

constant-expressione としたとき、メンバ関数呼び出し e.match(v) または ADL のみによる非メンバ関数呼び出し match(e, v)bool に文脈的に変換可能かつ true に評価されるような値 v にマッチします。
match(x, y) のデフォルトの挙動は x == y です。

int v = /* ... */ ;

inspect (v) {
    0 => { std::cout << "got zero"; }
    1 => { std::cout << "got one"; }
// ˆ expression pattern
};
enum class Color { Red, Green, Blue };
Color color = /* ... */ ;

inspect (color) {
    Color::Red => // ...
    Color::Green => // ...
    Color::Blue => // ...
//  ˆˆˆˆˆˆˆˆˆˆˆ expression pattern
};

ノート : デフォルトでは、 identifierIdentifier Pattern です。Case Pattern を参照してください。

static constexpr int zero = 0, one = 1;
int v = 42;

inspect (v) {
    zero => { std::cout << zero; }
//  ˆˆˆˆ identifier pattern
};
// 出力 : 42

[WIP] Compound Patterns

Structured Binding Pattern

構造化束縛パターンは以下の二つの形式を持ちます。

  [ pattern0, pattern1,  . . .  , patternN ]
  [ designator0 : pattern0, designator1 : pattern1, . . . , designatorN : patternN ]

一つ目の形式は、それぞれの patternivi 番目の要素にマッチするような値 v にマッチします。ここで v の要素とは、 それぞれの __ei を説明専用の識別子としたとき auto&& [__e0, __e1, ... , __eN] = v; のような構造化束縛宣言によって与えられるものです。

std::pair<int, int> p = /* ... */ ;

inspect (p) {
    [0, 0] => { std::cout << "on origin"; }
    [0, y] => { std::cout << "on y-axis"; }
//      ˆ identifier pattern
    [x, 0] => { std::cout << "on x-axis"; }
//      ˆ expression pattern
    [x, y] => { std::cout << x << ',' << y; }
//  ˆˆˆˆˆˆ structured binding pattern
};

二つ目の形式は、それぞれの patternidesignatori で指定された identifier の名前を持つ v の直接の非静的メンバにマッチするような値 v にマッチします。

struct Player { std::string name; int hitpoints; int coins; };

void get_hint(const Player& p) {
  inspect (p) {
    [.hitpoints: 1] => { std::cout << "You're almost destroyed. Give up!\n"; }
    [.hitpoints: 10, .coins: 10] => { std::cout << "I need the hints from you!\n"; }
    [.coins: 10] => { std::cout << "Get more hitpoints!\n"; }
    [.hitpoints: 10] => { std::cout << "Get more ammo!\n"; }
    [.name: n] => {
      if (n != "The Bruce Dickenson") {
        std::cout << "Get more hitpoints and ammo!\n";
      } else {
        std::cout << "More cowbell!\n";
      }
    }
  };
}

ノート : 指示付き初期化とは異なり、 designator の順番はクラスのメンバが宣言された順と同じである必要はありません。

[WIP] Alternative Pattern

選択肢パターンは以下の形式を持ちます。

< auto > pattern
< concept > pattern
< type > pattern
< constant-expression > pattern

ここで、 v をマッチされようとしている値、 Vstd::remove_cvref_t<decltype(v)>Alt を山括弧の内部にあるエンティティとします。

ケース1 : std::variant-like

選択肢パターンは、 std::variant_size_v<V> が well-formed かつ整数に評価され、またAltv の現在のインデックスと互換であり patternv のアクティブな選択肢にマッチするとき、そのような v にマッチします。

v の現在のインデックスとは、メンバ関数呼び出し v.index() または ADL のみの非メンバ関数呼び出し index(v) によって与えられるもので、これを I とします。v のアクティブな選択肢とは、メンバ関数呼び出し v.get<I>() または ADL のみの非メンバ関数呼び出し get<I>(v) によって初期化された std::variant_alternative_t<I, V>& のことです。

以下の四つのいずれかを満たすとき、 AltI と互換です。

  • Altauto である
Before After
std::visit(
  [&](auto&& x) {
    strm << "got auto: " << x;
  },
v);
inspect (v) {
  <auto> x => {
    strm << "got auto: " << x;
  }
};
  • Altconcept であり、 std::variant_alternative_t<I, V> がその concept を満たす
Before After
std::visit([&](auto&& x) {
  using X = std::remove_cvref_t<decltype(x)>;
  if constexpr (C1<X>()) {
    strm << "got C1: " << x;
  } else if constexpr (C2<X>()) {
    strm << "got C2: " << x;
  }
}, v);
inspect (v) {
  <C1> c1 => {
    strm << "got C1: " << c1;
  }
  <C2> c2 => {
    strm << "got C2: " << c2;
  }
};
  • Alttype であり std::is_same_v<Alt, std::variant_alternative_t<I, V>>true である
Before After
std::visit([&](auto&& x) {
  using X = std::remove_cvref_t<decltype(x)>;
  if constexpr (std::is_same_v<int, X>) {
    strm << "got int: " << x;
  } else if constexpr (std::is_same_v<float, X>) {
    strm << "got float: " << x;
  }
}, v);
inspect (v) {
  <int> i => {
    strm << "got int: " << i;
  }
  <float> f => {
    strm << "got float: " << f;
  }
};
std::variant<int, int> v = /* ... */ ;

std::visit(
  [&](int x) {
    strm << "got int: " << x;
  },
v);
std::variant<int, int> v = /* ... */ ;

inspect (v) {
  <int> x => {
    strm << "got int: " << x;
  }
};
  • Altconstant-expression であり、それが switch で使用可能かつ I と同じ値である
Before After
std::variant<int, int> v = /* ... */ ;

std::visit([&](auto&& x) {
  switch (v.index()) {
    case 0: {
      strm << "got first: " << x; break;
    }
    case 1: {
      strm << "got second: " << x; break;
    }
  }
}, v);
std::variant<int, int> v = /* ... */ ;

inspect (v) {
  <0> x => {
    strm << "got first: " << x;
  }
  <1> x => {
    strm << "got second: " << x;
  }
};
ケース2 : std::any-like
< type > pattern

Alttype で、ADL のみの非静的メンバ関数呼び出し any_cast<Alt>(&v) が有効なものであった場合にその結果を p とすると、選択肢パターンは pbool に文脈的に変換可能で かつ true に評価され、 pattern*p にマッチするとき、マッチします。

Before After
std::any a = 42;

if (int* i = any_cast<int>(&a)) {
  std::cout << "got int: " << *i;
} else if (float* f = any_cast<float>(&a)) {
  std::cout << "got float: " << *f;
}
std::any a = 42;

inspect (a) {
  <int> i => {
    std::cout << "got int: " << i;
  }
  <float> f => {
    std::cout << "got float: " << f;
  }
};
[WIP] ケース3 : 多層的な型
< type > pattern

Alttype で、std::is_polymorphic_v<V> が true であるとき、 decltype(&v) と同じcv修飾子を持つ Alt' に対して dynamic_cast<Alt'*>(&v)p とすると、選択肢パターンは pbool に文脈的に変換可能で かつ true に評価され、 pattern*p にマッチするとき、マッチします。

[WIP] Parenthesized Pattern

[WIP] Case Pattern

[WIP] Dereference Pattern

[WIP] Extractor Pattern

[WIP] パターンガード


  1. 元々は expression form と statement form の二種類があったようですが、 R3 で統一を図ったと思われます (情報求む)

  2. ここは意味がよく掴めませんでした。inspect 式と goto との関係性についての議論なども行われていそうなので、後々仕様が固まるかもしれません。

メタ関数のカリー化

序文

メタ関数をカリー化したい、と誰もが一度は思うはずです。
(え、思わない?そうですか…)
なので早速やっていきます。

メタ関数の作成

まずやりたい事を明確にします。カリー化というぐらいですから、以下のように書きたいですね。

template <class T, class U> struct X {};
using curried = curry<X>::type;
static_assert(std::is_same<curried<T><U>, X<T, U>>::value);

しかし curried<T><U> などという書き方は出来ませんから、 curried::ident<T>::ident<U> のように識別子を挟んでやる必要があります。
取り敢えずこれを作ることを目標にします。

では出力されて欲しい型を考えてみます。
先程のようにしたければ素直に

struct curried {
  template <class T>
  struct impl {
    template <class U>
    using ident = X<T, U>;
  };
  template <class T>
  using ident = impl<T>;
};

という型があれば良いですが、上記のままだと引数が増えた場合

template <class T1, class T2, class T3> struct Y {};

struct curried {
  template <class T1>
  struct impl1 {
    template <class T2>
    struct impl2 {
      template <class T3>
      using ident = Y<T1, T2, T3>;
    };
    template <class T2>
    using ident = impl2<T2>;
  };
  template <class T>
  using ident = impl1<T>;
};

のようになり、生成しようとすると impl が衝突して厄介なことになりそうです。
なので一旦以下のような構造を考えます。

struct curried {
  using type = struct {
    template <class T>
    struct ident {
      using type = struct {
        template <class U>
        struct ident {
          using type = X<T, U>;
        };
      };
    };
  };
};

それぞれの階層に type を挟むことで衝突は避けれましたが、使おうとすると curried::type::ident<T>::type::ident<U>::type のようになってしまいます。
しかし単純な再帰的構造で実装は楽そうですので、一度これで実装します。

template <class...> using void_t = void;

template <class, template <class...> class T, class... Ts>
struct curried {
  using type = struct {
    template <class U>
    using ident = curried<void, T, Ts..., U>;
  };
};

template <template <class...> class T, class... Ts>
struct curried<void_t<T<Ts...>>, T, Ts...>  {
  using type = T<Ts...>;
};

template <template <class...> class T>
struct curry { using type = curried<void, T>; };

template <template <class...> class T> 
using curry_t = typename curry<T>::type;

仕組みとしては、テンプレートテンプレートパラメータの T に対しパラメータパック Ts... を与えて実体化可能ならば適用し、不可能ならばパラメータパックに型を追加するための ident を提供する、といったものになります。

ここで、 curry_t<X>::type::ident<T>::type::ident<U>::typecurry_t<X>::type 以降は ident<T>::type の繰り返しなので、curry に手を加えて

template <template <class...> class T>
struct curry { using type = typename curried<void, T>::type; };

さらに ident<T>::type に当たる部分をまとめて apply<T> とでも名付けてしまえば

template <class, template <class...> class T, class... Ts>
struct curried {
  using type = struct {
    template <class U>
    using apply = typename curried<void, T, Ts..., U>::type;
  };
};

晴れて curry_t<X>::apply<T>::apply<U> のように書けるようになりました。

後は適宜ヘルパーメタ関数を用意してしまえば

template <class T, class... Us>
struct apply_to {};

template <class T>
struct apply_to<T> {
  using type = T;
};

template <class T, class U>
struct apply_to<T, U> {
  using type = typename T::template apply<U>;
};

template <class T, class U1, class U2, class... Us>
struct apply_to<T, U1, U2, Us...>
    : apply_to<typename apply_to<T, U1>::type, U2, Us...> {};

template <class T, class... Us>
using apply_to_t = typename apply_to<T, Us...>::type;

だいぶ使いやすくなると思います。

ラムダ計算

流石に作っただけでは味気ないので、何かやってみましょう。
手頃なものとしてはラムダ計算でしょうか。
まずは自然数を定義してみましょう。
もちろん自然数はゼロから1ですので、はじめにゼロを作ります。

// λfx.x
template <class F, class X> using zero = X;
using _0 = curry_t<zero>;

そして succ 関数も作れば

// λnfx.f(nfx)
template <class N, class F, class X>
using succ_t = apply_to_t<F, apply_to_t<N, F, X>>;
using succ_c = curry_t<succ_t>;

自然数が作れるようになりました。

using _1 = succ_c::apply<_0>;
using _2 = succ_c::apply<_1>;
using _3 = succ_c::apply<_2>;
using _4 = succ_c::apply<_3>;
using _5 = succ_c::apply<_4>;

利便性のため integral_constant への変換も作っておきます。

template <int X> using int_t = std::integral_constant<int, X>;
template <class T> struct inc : int_t<T::value + 1> {};
using inc_c = curry_t<inc>;
template <class T> using to_num = apply_to_t<T, inc_c, int_t<0>>;

add mul pow を作れば、

template <class L, class R, class F, class X>
using add_t = apply_to_t<L, F, apply_to_t<R, F, X>>;
using add_c = curry_t<add_t>;

template <class L, class R, class F>
using mul_t = apply_to_t<L, apply_to_t<R, F>>;
using mul_c = curry_t<mul_t>;

template <class L, class R>
using pow_t = apply_to_t<R, L>;
using pow_c = curry_t<pow_t>;

後は思う存分ラムダ計算を楽しめます。

static_assert(to_num<apply_to_t<add_c, _2, _3>>::value == 5);

using f = mul_c::apply<_2>;
static_assert(to_num<f::apply<_3>>::value == 6);
static_assert(to_num<f::apply<_5>>::value == 10);

template <class F, class X, class Y>
using flip_t = apply_to_t<F, Y, X>;
using flip_c = curry_t<flip_t>;

using g = apply_to_t<flip_c, pow_c, _3>;
static_assert(to_num<g::apply<_2>>::value == 8);
static_assert(to_num<g::apply<_3>>::value == 27);

蛇足

技術系記事の執筆は初めてなので拙いものとなりましたがご容赦ください。
マサカリお待ちしています。


  1. ここではペアノ公理に従います

ブログ、始動…?

取り敢えず、はてなブログとやらに登録してブログを書いてみることにした。

と言っても、かなり不定期更新になるだろう。

 

競技プログラミングの参加記及び精進録、またプログラミングの話題を綴ることが多いと思うが、日常的な話も挟んでいきたい。とはいえ、日常的に何か話すことがあるのかと言われるとその自信は無いのだが。

 

文体はその日の気分によって変わる可能性が大いにあるので、気にしないで欲しい。兎に角今回はこの文体の気分だ。ツイッターが主な生息地である以上其処での呟きを拡張したものとして使われる可能性もあるのだ、いちいち気にしてはきりが無いだろう。

 

ひとまず、最初の投稿はこの様な具合だろうか。ではまた。