はじめに
注意
2021/06/05 にリリースされた dmd v2.097.0 にて sumtype
は std.sumtype
として標準ライブラリに取り込まれました。
今後はライブラリとしての参照を追加する必要はありません。
記事の大部分はそのまま残しておきますが、最新版のコンパイラを利用していれば import std.sumtype
として利用してください。
当面はライブラリのほうも同じように保守されるようなので、そのまま使うことも可能です。
今回は、個人的に最近よく使うようになってきた sumtype
というライブラリの紹介と使い方メモです。
@nogc
や @safe
などにも対応し、シンプルに便利でメタプログラミングの面白さが溢れるライブラリだと思いますので、ぜひ使ってみてください!
-
sumtype
パッケージのページ -
std.sumtype
の公式ドキュメント
概要
できることは至ってシンプルで、「『指定した型のうちいずれか』を表す型」を安全に構築し、扱えるようにします。
安全に扱うための match
関数が特徴で、他の言語で言うところの match
式と似たようなことができます。
下手な説明より見た方が早い説があるので、ちょっと補足しながらコードを眺めたいと思います。
dubなどの構築は済んでいる前提で、ざっくりプロジェクトを初期化するところからです。
dub init sandbox-sumtype -n
cd sandbox-sumtype
dub add sumtype // 最新版のコンパイラを使っていれば、このステップは不要です
と、これでプロジェクトはできるので後はお好みのエディタで開いてコードを書いていきましょう。
まずはよくある例ということで、以下のようなサンプルです。
- 矩形と円形を定義、それのいずれかが入るShape型を定義
- Shape型の引数を受け取って面積を計算する
area
関数を定義-
match
を使って分岐を掛けたうえで面積を計算する
-
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();
}
50
28.2743
ライブラリ機能
提供される機能は基本3つしかないのですぐに覚えられます。
SumType
可変長のテンプレート引数を指定して、「そのいずれかである型」を生成するためのテンプレートです。
サンプルの通り、 alias S = SumType!(string, int);
といった使い方をします。
また可変長の引数を持ったテンプレートですので、扱う型はお好きなだけ指定して大丈夫です!(フラグ)
なお、あんまりハマらないと思いますが、使う上での注意点として「引数に同じ型は含められない」というのがあります。
最小限の例を見れば分かるのですが、以下のような状況です。
alias S = SumType!(string, string);
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
というテンプレートがあるので、これを使って重複を除去しておくといい感じだと思います。
これに加えて std.variant
の This
という型を使って自分の型を使う再帰した型が作れるのですが、ちょっと複雑で長くなるのとあまり使い慣れていないので割愛します。
match
match
は SumType
で生成した型を受け取るテンプレート関数です。
扱う型毎にハンドラーを指定して final switch
で分岐するような関数です。
ハンドリングされない型があるとコンパイルエラーになるため、処理漏れがないのが良いところです。
alias S = SumType!(string, int);
S data = "text";
data.match!(
(string s) { ... },
(int n) { ... },
);
また、一部のみ処理して他は特に気にしない場合、「引数の型」を省略したラムダ式でハンドラーを指定することで一律処理できます。
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
と違い、ハンドラーで処理されない型が来ると例外を発生させるバージョンです。
こちらはハンドラーの種類が不足していても大丈夫で、スクリプト的に使う場合は楽そうですね。
alias S = SumType!(string, int);
S data = 10;
// 実行時にMatchExceptionが発生
data.tryMatch!(
(string s) { writeln("string: ", s); },
);
簡単な仕組み(タグ付きunionとは)
上記の例を何も考えずにやろうとすると、 union
などを使って以下のようになると思います。
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
最近よく見るラノベ風に意訳すると、「本気でやったわけではないのだが、気づいたら std::visit
を破っていた」という感じのタイトルです。
Cの union
が最も効率の良い結果を生成しており、C++がそれにどこまで近いのか、Dで書いたものがどれくらい近いのか、というのを整理されています。
要旨は「C++ほど頑張らなくてもC並に最適化された結果が得られ、しかもC++より効率が良い」という点なのですが、
作者は「sumtype
は最初からこれを気にして作ったものではなく、書けそうだったから書いてみただけで、実験したらこの結果だった」という点を強調しています。
比較の過程で生成されるアセンブリに対する細かい話もありますが、最終的に「これぞゼロコスト抽象化ではないだろうか」という感じで記事は締めくくられています。
補足
@nogc
や pure
などへの対応で色々ありますが、確かにソースを見てみても愚直にstatic foreachなどの機能を使って必要なコンストラクタや関数オーバーロードを生成するのがメインとなっています。
また猛烈なunittestの山を見るに、ほぼ気合で書いている状況が連想されますね…
単体テストは人を強くする…!
所感・まとめ
当然ライブラリはとても便利に使っているのですが、プログラミング言語が溢れ代数的データ型などが組み込みの言語も珍しくないご時世ですので、それに倣った便利機能が使えるのはとても良いですね。
また、「簡単に作れそうだと感じた」「実際に作れた」「Cとほぼ同じ結果」という点は注目に値しそうです。
そして本当にC++等で同じようなものが作れないのか?作るのが難しいのか?というのも気になるところです。
D言語を主に使う身としては、何が簡単そうだと感じさせるのか、具体的なところを今後掘り下げていきたいですね!
メタプログラミングの良さも再確認できましたし、使っていて非常に面白いライブラリの1つです!
というわけで、これを機にぜひ使ってみてください!