オフィスアワーがそろそろ始まるよ!()

タグ付き共用体

タグ付き共用体は、列挙型と共用体を組み合わせたデータ構造です。タグ付き共用体により、共用体が有している値のタグを検査してから、そのタグに紐づくヴァリアントを操作することができます。このことにより、複雑なデータ構造を少ないコードで安全に扱うことが可能になります。タグ付き共用体は、関数型言語では代数的データ型と呼ばれるものです。

タグ付き共用体の定義

タグ付き共用体は、const 型名 = union(enum / 列挙型) { ヴァリアント, ... };で定義します。

examples/ch03-user-defined-types/src/tagged_union.zen:4:8

const Number = union(enum) {
    U64: u64,
    I64: i64,
    F64: f64,
};

前節で見た共用体の定義とよく似ています。異なる点は(enum)の部分のみです。しかし、このことにより、列挙型と共用体、両方の特性を持った型として扱えます。

タグ付き共用体の変数は、共用体と同じ方法で定義します。インスタンスの値へのアクセス方法も、共用体と同じです。

examples/ch03-user-defined-types/src/tagged_union.zen:10:13

test "simple tagged union" {
    var value = Number { .U64 = 0 };
    ok(value.U64 == 0);
}

タグ付き共用体では、インスタンスを列挙型として扱うことができます。

examples/ch03-user-defined-types/src/tagged_union.zen:15:18

test "tagged union as enum" {
    var value = Number { .U64 = 0 };
    ok(value == Number.U64);
}

その名の通り、タグ付き共用体は、次のような列挙型と共用体が一体になったものです。

const Number = enum {
    U64,
    I64,
    F64,
};

const Number = union {
    U64: u64,
    I64: i64,
    F64: f64,
};

タグ (列挙型) を明示的に定義した上で、タグ付き共用体を定義することも可能です。

examples/ch03-user-defined-types/src/tagged_union_separate.zen:4:14

const NumberTag = enum {
    U64,
    I64,
    F64,
};

const Number = union(NumberTag) {
    U64: u64,
    I64: i64,
    F64: f64,
};

NumberTagという列挙型を定義し、タグ付き共用体を定義する際に、union(NumberTag)とします。ここで、列挙型とタグ付き共用体のヴァリアントは過不足なく一致しなければなりません。

このタグ付き共用体のインスタンスは、NumberTag列挙型としてタグを比較したり、Number共用体として内部の値を利用することができます。

examples/ch03-user-defined-types/src/tagged_union_separate.zen:16:20

test "separated tagged union" {
    const value = Number { .U64 = 0 };
    ok(value.U64 == 0);
    ok(value == NumberTag.U64);
}

列挙型とタグ付き共用体の型変換

タグ付き共用体から列挙型へは、暗黙の型変換が行われます。列挙型からタグ付き共用体へは、共用体のヴァリアントがvoid型などコンパイル時計算可能な場合のみ、暗黙の型変換が行われます。

examples/ch03-user-defined-types/src/tagged_union_cast.zen:16:24

test "implicit casting between tagged union and enum" {
    const u = Types { .U64 = 0 };
    // `Types` => `TypesTag`
    ok(u == TypesTag.U64);

    const v: TypesTag = .VOID;
    // `TypesTag` => `Types`
    ok(v == Types.VOID);
}

タグ型とタグ整数値の設定

union(enum)でタグ付き共用体を定義する際、タグ型とタグ整数値を設定できます。

examples/ch03-user-defined-types/src/tagged_union.zen:20:31

const Units = union(enum(u64)) {
    KILO: u32 = 1_000,
    MEGA: u32 = 1_000_000,
    GIGA: u64 = 1_000_000_000,
    TERA: u64 = 1_000_000_000_000,
};

test "setting tag value of a tagged union" {
    const kilo = Units { .KILO = 0 };
    ok(@enumToInt(kilo) == 1_000);
    ok(@enumToInt(Units.TERA) == 1_000_000_000_000);
}

switch

タグ付き共用体はswitchでそれぞれのヴァリアントを場合分けすることができます。

examples/ch03-user-defined-types/src/tagged_union.zen:33:39

test "switch tagged union" {
    const kilo = Units { .KILO = 1 };
    switch (kilo) {
        .KILO => std.debug.warn("unit is KILO, value is {}\n", kilo.KILO),
        else => std.debug.warn("unit is not KILO\n"),
    }
}

std.debug.warnの出力結果は次の通りです。

unit is KILO, value is 1

パターンマッチ

switchでヴァリアントを場合分けした際、共用体の値を取り出すことができます。上のコードを下のように修正します。

examples/ch03-user-defined-types/src/tagged_union.zen:41:47

test "pattern match" {
    const kilo = Units { .KILO = 1 };
    switch (kilo) {
        .KILO => |v| std.debug.warn("unit is KILO, value is {}\n", v),
        else => std.debug.warn("unit is not KILO\n"),
    }
}

.KILO => |v|の部分で、kilo.KILOの値をvに格納します。このコードの実行結果は、修正前と同じです。

unit is KILO, value is 1

共用体インスタンスの値を変更したい場合、ポインタとして参照を取得します。

examples/ch03-user-defined-types/src/tagged_union.zen:49:57

test "retreave value as pointer" {
    var kilo = Units { .KILO = 1 };
    switch (kilo) {
        .KILO => |*v| v.* = 2,
        else => std.debug.warn("unit is not KILO\n"),
    }

    ok(kilo.KILO == 2);
}

.KILO => |*v|の部分で、kilo.KILOの値の参照を取得します。

メソッド定義

タグ付き共用体 (共用体) にも関数やメソッドを定義できます。

関数は共用体名前空間内の関数として扱われ、共用体型.関数名で呼び出すことができます。

examples/ch03-user-defined-types/src/tagged_union_method.zen:4:25

const Number = union(enum) {
    U64: u64,
    I64: i64,
    F64: f64,

    fn createUnsinged(v: u64) Number {
        return Number { .U64 = v };
    }
};

test "functions in a tagged union" {
    var number = Number.createUnsinged(5);
    ok(number.U64 == 5);
}

メソッドは第一引数に共用体インスタンス (またはそのポインタ) を受け取る関数です。メソッドは、インスタンス名.メソッド名で呼び出すことができます。

examples/ch03-user-defined-types/src/tagged_union_method.zen:4:30

const Number = union(enum) {
    // ...
    fn isZero(num: Number) bool {
        return switch (num) {
            .U64 => |n| n == 0,
            .I64 => |n| n == 0,
            .F64 => |n| n == 0.0,
        };
    }
};

test "methods in a tagged union" {
    var number = Number.createUnsinged(5);
    ok(number.isZero() == false);
}

複雑なデータ構造を持つタグ付き共用体

タグ付き共用体の中には、異なる型のヴァリアントを持つことができます。もちろん、ヴァリアントは構造体のようなユーザー定義型でもかまいません。

例えば、次のように、自分で定義したMyStructをヴァリアントとして持つタグ付き共用体を定義できます。

examples/ch03-user-defined-types/src/tagged_union_struct_variant.zen:5:16

const MyStruct = struct {
    x: u64 = 0,
    y: u64 = 0,
};

const Objects = union(enum) {
    Null: void,
    Boolean: bool,
    Number: f64,
    String: []u64,
    Struct: MyStruct,
};

switchで各ヴァリアントを処理できます。例えば、各ヴァリアントの値を表示する関数は次の通りです。

examples/ch03-user-defined-types/src/tagged_union_struct_variant.zen:18:26

fn printVariant(object: Objects) void {
    return switch (object) {
        .Null => std.debug.warn("Value: Null\n"),
        .Boolean => |b| std.debug.warn("Value: {}\n", b),
        .Number => |n| std.debug.warn("Value: {}\n", n),
        .String => |s| std.debug.warn("Value: {}\n", s),
        .Struct => |s| std.debug.warn("Value: {}\n", s),
    };
}

examples/ch03-user-defined-types/src/tagged_union_struct_variant.zen:30:35

test "complex tagged union" {
    const null_obj = Objects.Null;
    const struct_obj = Objects { .Struct = MyStruct {} };
    printVariant(null_obj);
    printVariant(struct_obj);
}

上のコードのstd.debug.warnの出力結果は、以下のとおりです。

Value: Null
Value: MyStruct{ .x = 0, .y = 0 }

☰ 人の生きた証は永遠に残るよう ☰
Copyright © 2018-2019 connectFree Corporation. All rights reserved.
Zen, the Zen three-circles logo and The Zen Programming Language are trademarks of connectFree corporation in Japan and other countries.