튜플(Tuples)

튜플(Tuples)

필드 이름을 하나씩 붙이지 않고도 값들을 한 덩어리로 묶고 싶을 때가 있어요. 그럴 때 Zig의 튜플(tuple)을 쓰면 돼요. 이번에는 튜플이 뭔지, 그 특징이 뭔지, 그리고 여러 값을 한 번에 돌려받을 때 어떻게 쓰는지 하나씩 살펴볼게요.

출처: Zig Documentation

본문

Zig에서는 필드 이름을 지정하지 않고도 익명 구조체(anonymous struct)를 만들 수 있어요. 이렇게 만든 것을 우리는 튜플(tuple) 이라고 불러요. 빈 튜플은 .{ }처럼 생겼는데, 이 모양을 Hello World 예제 중 하나에서 이미 본 적이 있을 거예요.

튜플의 필드는 0부터 시작하는 숫자로 이름이 암시적으로 매겨져요. 필드 이름이 정수라서, @""로 감싸지 않는 한 . 문법으로는 접근할 수 없어요. @"" 안에 들어 있는 이름은 항상 식별자(identifier)로 인식된답니다.

배열과 마찬가지로 튜플도 .len 필드를 갖고 있고, 인덱스로 접근할 수 있으며(단, 인덱스는 comptime에 알려져 있어야 해요), ++ 연산자와도 함께 쓸 수 있어요. 또 inline for로 순회하는 것도 가능하죠. 코드로 확인해 볼게요.

const std = @import("std");
const expect = std.testing.expect;
const expectEqual = std.testing.expectEqual;

test "tuple" {
    const values = .{
        @as(u32, 1234),
        @as(f64, 12.34),
        true,
        "hi",
    } ++ .{ false, false };
    try expectEqual(1234, values[0]);
    try expectEqual(false, values[4]);
    inline for (values, 0..) |v, i| {
        if (i != 2) continue;
        try expect(v);
    }
    try expectEqual(6, values.len);
    try expectEqual('h', values.@"3"[0]);
}
$ zig test test_tuples.zig
1/1 test_tuples.test.tuple...OK
All 1 tests passed.

여기서 눈여겨볼 점이 몇 가지 있어요. .{ ... }에 담긴 값들이 서로 다른 타입(u32, f64, bool, []const u8)이라도 한 튜플로 묶였죠. 그리고 ++로 뒤에 튜플을 이어 붙였으니 values.len이 6이 되는 거예요. 마지막의 values.@"3"[0]은 숫자 이름 3@""로 감싸 접근한 뒤, 그 문자열의 0번째 문자인 'h'를 꺼낸 겁니다.

튜플 구조 분해(Destructuring Tuples)

튜플은 구조 분해(destructuring)를 할 수 있어요. 구조 분해는 블록에서 여러 값을 한 번에 돌려받고 싶을 때 특히 유용하답니다.

const print = @import("std").debug.print;

pub fn main() void {
    const digits = [_]i8 { 3, 8, 9, 0, 7, 4, 1 };

    const min, const max = blk: {
        var min: i8 = 127;
        var max: i8 = -128;

        for (digits) |digit| {
            if (digit < min) min = digit;
            if (digit > max) max = digit;
        }

        break :blk .{ min, max };
    };

    print("min = {}\n", .{ min });
    print("max = {}\n", .{ max });
}
$ zig build-exe destructuring_block.zig
$ ./destructuring_block
min = 0
max = 9

blk: 라벨이 붙은 블록 안에서 최솟값과 최댓값을 구한 뒤, break :blk .{ min, max }로 두 값을 튜플 하나에 담아 돌려줬어요. 그걸 const min, const max = ...처럼 좌변에서 구조 분해로 받아낸 거죠. 이렇게 하면 함수를 만들지 않고도 블록 하나로 여러 값을 돌려받을 수 있어요.

함수나 내장 함수가 튜플 형태로 여러 값을 반환하는 경우에도 구조 분해는 편리해요.

const print = @import("std").debug.print;

fn divmod(numerator: u32, denominator: u32) struct { u32, u32 } {
    return .{ numerator / denominator, numerator % denominator };
}

pub fn main() void {
    const div, const mod = divmod(10, 3);

    print("10 / 3 = {}\n", .{div});
    print("10 % 3 = {}\n", .{mod});
}
$ zig build-exe destructuring_return_value.zig
$ ./destructuring_return_value
10 / 3 = 3
10 % 3 = 1

divmod의 반환 타입이 struct { u32, u32 }인데, 필드 이름 없이 타입만 나열했죠. 이것도 튜플이에요. divmod(10, 3)이 튜플 .{ 3, 1 }을 돌려주면, 그것을 const div, const mod = ...로 각각 나눠 받는 겁니다. 여러 값을 돌려받아야 할 때 함수 하나로 깔끔하게 처리되는 게 보이시죠?

더 알아보기