前回のエントリで std.variant の変遷(?)についてなんとなく追えました。
今回は実際に触ってみて、理解を深めようと思います。

$ dmd --version
DMD64 D Compiler v2.091.1

Copyright (C) 1999-2020 by The D Language Foundation, All Rights Reserved written by Walter Bright

Variant

Variant型には色んな値を入れることができます。TypescriptのAnyみたいなやつです。

import std.variant;
import std.stdio;

void main() {
    Variant a;
    a = "hello";
    writeln(a, " ", a.type, " ", a.get!(string));

    a = 3.14;
    writeln(a, " ", a.type, " ", a.get!(double));

    a = 100;
    writeln(a, " ", a.type, " ", a.get!(int));

    a = [1, 2, 3];
    writeln(a, " ", a.type, " ", a.get!(int[]));
    
    a = ["a":"A", "b":"B", "c":"C"];
    writeln(a, " ", a.type, " ", a.get!(string[string]));
}
$ rdmd main.d 
hello immutable(char)[] hello
3.14 double 3.14
100 int 100
[1, 2, 3] int[] [1, 2, 3]
["c":"C", "a":"A", "b":"B"] immutable(char)[][immutable(char)[]] ["c":"C", "a":"A", "b":"B"]

type() で型を確認でき、 get(T)() で値を取得できます。
getができなかった場合、VariantExceptionが発生します。


import std.variant;
import std.stdio;

void main() {
    Variant b;
    writeln(b.hasValue, " ", b.type);

    b = 1000;
    writeln(b.hasValue, " ", b.type);

    b = null;
    writeln(b.hasValue, " ", b.type);

    b = Variant();
    writeln(b.hasValue, " ", b.type);
}
$ rdmd main.d 
false void
true int
true typeof(null)
false void

hasValue()は値を持つか検査します。初期化直後はfalse、値を設定後はtrueになります。
nullを入れてもtrueなのですね。


import std.variant;
import std.stdio;

void main() {
    Variant c = 10;
    
    c += 5;
    writeln(c, " ", c.type);

    c += 0.1;
    writeln(c, " ", c.type);

    c = c.coerce!(string) ~ "a";
    writeln(c, " ", c.type);

    c = 123.45f;
    writeln(c.type, " ", c.convertsTo!(int), " ", c.convertsTo!(double));
}
$ rdmd main.d 
15 int
15.1 double
15.1a immutable(char)[]
float false true

Variantは演算することもできます。明示的に変換する場合、coerce(T)()を使います。
convertsTo(T)()で変換可能か検査することができます。


Algebraic

Algebraicは型の種類を指定できるVariantです。
たとえば auto a = Algebraic!(int, double)(0) は int または double のみが入る型です。
Typescriptだと a: int | double = 0 みたいに書いたりしますよね。たしか。

import std.variant;
import std.stdio;

alias Number = Algebraic!(int, double);
void main() {
    Number d = 1;
    writeln(d, " ", d.type, " ", d.get!(int));

    d -= 0.00001;
    writeln(d, " ", d.type, " ", d.get!(double));

    // d = "5";
    // stringはコンパイルエラーになる
}
$ rdmd main.d 
1 int
0.99999 double

上記のようにaliasで別名をつけたりすると分かりやすいかもしれません。
Variant同様に演算などもできますが、指定外の値を入れようとするとコンパイルエラーになります。


import std.variant;
import std.stdio;

alias Any = Algebraic!(long, double, string, bool, typeof(null));
void main() {
    Any[] any = [ Any("a"), Any(3.14), Any(true), Any(10_000_000L), Any(null) ];

    foreach (Any a; any) {
        auto ret = a.visit!(
            (long v) => "A",
            (double v) => "B",
            (string v) => "C",
            (_) => "other" // その他の型を1つにまとめられる
        );
        writeln(a, " ", a.type, " ", ret);
    }
}
$ rdmd main.d 
a immutable(char)[] C
3.14 double B
true bool other
10000000 long A
null typeof(null) other

visit(T)(handlers...)は型にマッチする処理を行い、結果を返します。
Algebraicに指定してある型を網羅してないとコンパイルエラーになります。 (特定の型のみ処理したい場合は、最後に型を指定しないlambda式を書くと良いです。いずれにもマッチしない型の場合にはこれが処理されます。)
また、網羅してなくてもコンパイルエラーにならないtryVisit(T)(handlers...)もあります。(※ただし、実際に網羅外の値が来た場合にはVariantExceptionが発生します。)

なんとなく便利そうなこのvisitなのですが、Variantには使えずAlgebraicでないとダメみたいです。残念。

参考