安全なunion、SumTypeの紹介と使い方

# はじめに

__注意__
2021/06/05 にリリースされた dmd v2.097.0 にて `sumtype` は `std.sumtype` として標準ライブラリに取り込まれました。
今後はライブラリとしての参照を追加する必要はありません。

記事の大部分はそのまま残しておきますが、最新版のコンパイラを利用していれば `import std.sumtype` として利用してください。

当面はライブラリのほうも同じように保守されるようなので、そのまま使うことも可能です。

---


今回は、個人的に最近よく使うようになってきた `sumtype` というライブラリの紹介と使い方メモです。

`@nogc` や `@safe` などにも対応し、シンプルに便利でメタプログラミングの面白さが溢れるライブラリだと思いますので、ぜひ使ってみてください！

- `sumtype` パッケージのページ
    - http://code.dlang.org/packages/sumtype
- `std.sumtype` の公式ドキュメント
    - https://dlang.org/phobos/std_sumtype.html

# 概要
できることは至ってシンプルで、「『指定した型のうちいずれか』を表す型」を安全に構築し、扱えるようにします。

安全に扱うための `match` 関数が特徴で、他の言語で言うところの `match` 式と似たようなことができます。

下手な説明より見た方が早い説があるので、ちょっと補足しながらコードを眺めたいと思います。

dubなどの構築は済んでいる前提で、ざっくりプロジェクトを初期化するところからです。

```console:プロジェクト準備
dub init sandbox-sumtype -n
cd sandbox-sumtype
dub add sumtype // 最新版のコンパイラを使っていれば、このステップは不要です
```

と、これでプロジェクトはできるので後はお好みのエディタで開いてコードを書いていきましょう。

まずはよくある例ということで、以下のようなサンプルです。

- 矩形と円形を定義、それのいずれかが入るShape型を定義
- Shape型の引数を受け取って面積を計算する`area`関数を定義
    - `match`を使って分岐を掛けたうえで面積を計算する

```d:利用例
import std.sumtype; // 最新版のコンパイラを利用している場合
import sumtype;     // 旧版のコンパイラを利用している場合

struct Rectangle
{
    float width;
    float height;
}

struct Circle
{
    float radius;
}

// 矩形 または 円 を表す抽象的な「図形」というレベルの型を定義します
alias Shape = SumType!(Rectangle, Circle);

// 図形の面積を計算します
float area(Shape shape)
{
    import std.math : PI;

    return shape.match!(
        (Rectangle rect) => rect.width * rect.height,
        (Circle circle) => PI * circle.radius ^^ 2
    );
}

void main()
{
    import std.stdio;

    Shape shape = Rectangle(10, 5);
    shape.area().writeln();

    shape = Circle(3);
    shape.area().writeln();
}
```

```console:出力
50
28.2743
```


# ライブラリ機能
提供される機能は基本3つしかないのですぐに覚えられます。

## `SumType`

可変長のテンプレート引数を指定して、「そのいずれかである型」を生成するためのテンプレートです。

サンプルの通り、 `alias S = SumType!(string, int);` といった使い方をします。
また可変長の引数を持ったテンプレートですので、扱う型はお好きなだけ指定して大丈夫です！（フラグ）


なお、あんまりハマらないと思いますが、使う上での注意点として「引数に同じ型は含められない」というのがあります。

最小限の例を見れば分かるのですが、以下のような状況です。

```d:NG
alias S = SumType!(string, string);
```

```console:エラーメッセージ
onlineapp.d(6,11): Error: template instance `sumtype.SumType!(string, string)` does not match template declaration `SumType(TypeArgs...)`
  with `TypeArgs = (string, string)`
  must satisfy the following constraint:
`       allDistinct!TypeArgs`
dmd failed with exit code 1.
```


「`TypeArgs = (string, string)` です。 `allDistinct!TypeArgs` を満たす必要があります。」という感じでコンパイルエラーになります。
（なんか最近エラーメッセージが綺麗になりました？）

`allDistinct` なので、「すべての型が異なることを期待している」ということですね。

もしメタメタなプログラミングをしていてパラメーターの重複が怪しいときは、 `std.meta` に `NoDuplicates` というテンプレートがあるので、これを使って重複を除去しておくといい感じだと思います。

- https://dlang.org/phobos/std_meta.html#NoDuplicates

これに加えて `std.variant` の `This` という型を使って自分の型を使う再帰した型が作れるのですが、ちょっと複雑で長くなるのとあまり使い慣れていないので割愛します。

## `match`

`match` は `SumType` で生成した型を受け取るテンプレート関数です。

扱う型毎にハンドラーを指定して `final switch` で分岐するような関数です。
ハンドリングされない型があるとコンパイルエラーになるため、処理漏れがないのが良いところです。

```d:
alias S = SumType!(string, int);

S data = "text";
data.match!(
    (string s) { ... },
    (int n) { ... },
);
```

また、一部のみ処理して他は特に気にしない場合、「引数の型」を省略したラムダ式でハンドラーを指定することで一律処理できます。

```d:
alias S = SumType!(string, byte, short, int);

S data = 100;

auto text = data.match!(
    (string s) => "text: " ~ s,
    _ => "some numbers",
);

writeln(text); // some numbers
```

これは要するに何にでもマッチするラムダ式なので switch文における `case default` のようなものです。
「得に使わない値」ということでアンダーバーを指定しておく慣例があるみたいです。


## `tryMatch`

`match` と違い、ハンドラーで処理されない型が来ると例外を発生させるバージョンです。
こちらはハンドラーの種類が不足していても大丈夫で、スクリプト的に使う場合は楽そうですね。

```d:
alias S = SumType!(string, int);

S data = 10;

// 実行時にMatchExceptionが発生
data.tryMatch!(
    (string s) { writeln("string: ", s); },
);
```

# 簡単な仕組み（タグ付きunionとは）

上記の例を何も考えずにやろうとすると、 `union` などを使って以下のようになると思います。

```d:
import std;

struct Rectangle
{
    float width;
    float height;
}

struct Circle
{
    float radius;
}

enum ShapeType
{
    Rectangle,
    Circle,
}

struct Shape
{
    ShapeType tag;
    union
    {
        Rectangle rect;
        Circle circle;
    }

    void opAssign(Rectangle rect)
    {
        this.tag = ShapeType.Rectangle;
        this.rect = rect;
    }
    // その他省略
}

void main()
{
    Shape shape;
    shape = Rectangle(10, 5);
    // shape = Circle(4);

    final switch (shape.tag)
    {
    case ShapeType.Rectangle:
        writefln!"Rectangle: %f x %f"(shape.rect.width, shape.rect.height);
        break;
    case ShapeType.Circle:
        writeln("Circle: radius = %f", shape.circle);
        break;
    }
}
```

`Shape` 構造体の定義を見ればわかる通り、 `union` に加え、どのフィールドを使っているか表す `tag` を持った構造体なので「Tagged Union (タグ付きunion)」と言われます。

これはしっかり `final switch` を使っているので良いですが、そういうことをしなくても `.circle` にアクセスできたり、 `.tag` だけ書き換えたりできるので危険ですよね。

要は「タグとunionのフィールドが独立している」というのが問題で、`SumType` テンプレートはテンプレート引数の型を使い、メタプログラミングでより安全に使える型を自動的に作り出します！

# 作者のとあるブログ記事
ある日作者がC++の `std::visit` 、Cの`union`、Dの`std.variant` および `sumtype` で書いたコードをアセンブリレベルで比較した記事を投稿しました。

- Beating std::visit Without Really Trying
    - https://pbackus.github.io/blog/beating-stdvisit-without-really-trying.html

最近よく見るラノベ風に意訳すると、「本気でやったわけではないのだが、気づいたら `std::visit` を破っていた」という感じのタイトルです。

Cの `union` が最も効率の良い結果を生成しており、C++がそれにどこまで近いのか、Dで書いたものがどれくらい近いのか、というのを整理されています。

要旨は「C++ほど頑張らなくてもC並に最適化された結果が得られ、しかもC++より効率が良い」という点なのですが、
作者は「`sumtype` は最初からこれを気にして作ったものではなく、書けそうだったから書いてみただけで、実験したらこの結果だった」という点を強調しています。

比較の過程で生成されるアセンブリに対する細かい話もありますが、最終的に「これぞゼロコスト抽象化ではないだろうか」という感じで記事は締めくくられています。

## 補足

`@nogc` や `pure` などへの対応で色々ありますが、確かにソースを見てみても愚直にstatic foreachなどの機能を使って必要なコンストラクタや関数オーバーロードを生成するのがメインとなっています。

- 実装
    - https://github.com/pbackus/sumtype/blob/master/src/sumtype.d#L267

また猛烈なunittestの山を見るに、ほぼ気合で書いている状況が連想されますね…

単体テストは人を強くする…！


# 所感・まとめ

当然ライブラリはとても便利に使っているのですが、プログラミング言語が溢れ代数的データ型などが組み込みの言語も珍しくないご時世ですので、それに倣った便利機能が使えるのはとても良いですね。

また、「簡単に作れそうだと感じた」「実際に作れた」「Cとほぼ同じ結果」という点は注目に値しそうです。
そして本当にC++等で同じようなものが作れないのか？作るのが難しいのか？というのも気になるところです。

D言語を主に使う身としては、何が簡単そうだと感じさせるのか、具体的なところを今後掘り下げていきたいですね！

メタプログラミングの良さも再確認できましたし、使っていて非常に面白いライブラリの1つです！

というわけで、これを機にぜひ使ってみてください！