Tagged union

Tagged union (태그된 유니언)

enum 태그 타입으로 유니언을 선언할 수 있어요. 이렇게 하면 유니언이 태그된(tagged) 유니언이 되는데, 이게 되면 switch 표현식과 함께 쓸 수 있게 됩니다. 스위치에서 태그된 유니언을 분기할 때 태그 값은 추가 캡처로 얻을 수 있어요. 태그된 유니언은 자신의 태그 타입으로 강제(coerce)되기도 하는데, 이 부분은 Type Coercion: Unions and Enums에서 다룹니다.

출처: Zig Documentation

본문

먼저 태그된 유니언을 선언하고, 그걸 switch에서 써 보는 기본적인 모습부터 볼게요. 태그 타입을 enum으로 정해 주면 유니언은 그 enum의 값으로 '지금 어떤 필드가 활성화된 상태인지'를 기억합니다.

// test_tagged_union.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

const ComplexTypeTag = enum {
    ok,
    not_ok,
};
const ComplexType = union(ComplexTypeTag) {
    ok: u8,
    not_ok: void,
};

test "switch on tagged union" {
    const c = ComplexType{ .ok = 42 };
    try expectEqual(ComplexTypeTag.ok, @as(ComplexTypeTag, c));

    switch (c) {
        .ok => |value| try expectEqual(42, value),
        .not_ok => unreachable,
    }

    switch (c) {
        .ok => |_, tag| {
            // Because we're in the '.ok' prong, 'tag' is compile-time known to be '.ok':
            comptime std.debug.assert(tag == .ok);
        },
        .not_ok => unreachable,
    }
}

test "get tag type" {
    try expectEqual(ComplexTypeTag, std.meta.Tag(ComplexType));
}
$ zig test test_tagged_union.zig
1/2 test_tagged_union.test.switch on tagged union...OK
2/2 test_tagged_union.test.get tag type...OK
All 2 tests passed.

첫 번째 switch에서 .ok 분기 안의 |value|처럼 캡처로 값을 직접 받았죠. 두 번째 switch에서는 |_, tag|처럼 태그만 따로 캡처할 수도 있어요. 이때 .ok 분기 안에 있으니 tag는 컴파일 타임에 .ok임이 보장됩니다. std.meta.Tag(ComplexType)로 유니언의 태그 타입을 꺼내 쓸 수도 있습니다.

스위치 표현에서 태그된 유니언의 페이로드를 수정하고 싶다면, 변수 이름 앞에 *를 붙여서 그 값을 포인터로 받으면 돼요.

// test_switch_modify_tagged_union.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

const ComplexTypeTag = enum {
    ok,
    not_ok,
};
const ComplexType = union(ComplexTypeTag) {
    ok: u8,
    not_ok: void,
};

test "modify tagged union in switch" {
    var c = ComplexType{ .ok = 42 };

    switch (c) {
        ComplexTypeTag.ok => |*value| value.* += 1,
        ComplexTypeTag.not_ok => unreachable,
    }

    try expectEqual(43, c.ok);
}
$ zig test test_switch_modify_tagged_union.zig
1/1 test_switch_modify_tagged_union.test.modify tagged union in switch...OK
All 1 tests passed.

|*value|로 받은 뒤 value.* += 1처럼 역참조해서 값을 고쳤죠. 그 결과 c.ok가 43이 된 걸 확인합니다.

이번엔 유니언이 enum 태그 타입을 스스로 추론하게 할 수도 있어요. 또 struct나 enum처럼 유니언에도 메서드를 정의할 수 있습니다.

// test_union_method.zig
const std = @import("std");
const expect = std.testing.expect;

const Variant = union(enum) {
    int: i32,
    boolean: bool,

    // void can be omitted when inferring enum tag type.
    none,

    fn truthy(self: Variant) bool {
        return switch (self) {
            Variant.int => |x_int| x_int != 0,
            Variant.boolean => |x_bool| x_bool,
            Variant.none => false,
        };
    }
};

test "union method" {
    var v1: Variant = .{ .int = 1 };
    var v2: Variant = .{ .boolean = false };
    var v3: Variant = .none;

    try expect(v1.truthy());
    try expect(!v2.truthy());
    try expect(!v3.truthy());
}
$ zig test test_union_method.zig
1/1 test_union_method.test.union method...OK
All 1 tests passed.

union(enum)이라고 쓰면 태그 타입을 직접 나열하지 않아도 되고, none처럼 타입이 void인 필드는 아예 타입 표기를 생략할 수 있어요. truthy 메서드는 스스로를 switch로 분기해서 각 필드의 진위를 돌려줍니다.

enum 태그 타입을 추론하는 유니언은, 추론된 태그에 **서수값(ordinal value)**을 직접 지정할 수도 있어요. 이때는 태그가 명시적인 정수 타입을 지정해야 합니다. 그리고 활성 필드에 해당하는 서수값을 꺼내고 싶으면 @backingInt를 사용하면 됩니다.

// test_tagged_union_with_tag_values.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

const Tagged = union(enum(u32)) {
    int: i64 = 123,
    boolean: bool = 67,
};

test "tag values" {
    const int: Tagged = .{ .int = -40 };
    try expectEqual(123, @backingInt(int));

    const boolean: Tagged = .{ .boolean = false };
    try expectEqual(67, @backingInt(boolean));
}
$ zig test test_tagged_union_with_tag_values.zig
1/1 test_tagged_union_with_tag_values.test.tag values...OK
All 1 tests passed.

int에는 123, boolean에는 67이라는 서수값을 박아 뒀죠. 그런 뒤 @backingInt(int)로 활성 필드의 서수값을 꺼내 원래 지정했던 값과 같은지 확인합니다.

마지막으로, @tagName을 쓰면 필드 이름을 나타내는 comptime [:0]const u8 값을 얻을 수 있습니다.

// test_tagName.zig
const std = @import("std");
const expectEqualSlices = std.testing.expectEqualSlices;

const Small2 = union(enum) {
    a: i32,
    b: bool,
    c: u8,
};
test "@tagName" {
    try expectEqualSlices(u8, "a", @tagName(Small2.a));
}
$ zig test test_tagName.zig
1/1 [email protected]
All 1 tests passed.

@tagName(Small2.a)가 문자열 "a"와 일치하는지 확인하고 있어요. 이렇게 하면 필요한 순간에 태그의 이름을 문자열로 꺼내 쓸 수 있습니다.

더 알아보기