인라인 스위치 프롱

인라인 스위치 프롱 (Inline Switch Prongs)

스위치 프롱을 inline으로 표시하면, 그 프롱이 가질 수 있는 각 값마다 프롱의 본문을 따로 생성해요. 이렇게 하면 해당 프롱에서 잡은(captured) 값이 comptime 값이 됩니다. 런타임 값이 아니라 컴파일 시점에 정해진 값으로 코드를 분석하고 싶을 때 쓰는 기능이죠.

출처: Zig Documentation

본문

기본 예제: inline 프롱으로 컴파일 시점 값 다루기

isFieldOptional 함수는 어떤 struct의 특정 필드가 optional 타입인지 확인하는데, field_index는 런타임 값이에요. 그런데 프롱을 inline으로 만들면 각 인덱스별로 idx가 컴파일 시점에 알려진 값으로 처리되므로, @typeInfo(field_types[idx])처럼 타입 정보를 다루는 코드를 쓸 수 있어요.

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

fn isFieldOptional(comptime T: type, field_index: usize) !bool {
    const field_types = @typeInfo(T).@"struct".field_types;
    return switch (field_index) {
        // This prong is analyzed twice with `idx` being a
        // comptime-known value each time.
        inline 0, 1 => |idx| @typeInfo(field_types[idx]) == .optional,
        else => return error.IndexOutOfBounds,
    };
}

const Struct1 = struct { a: u32, b: ?u32 };

test "using @typeInfo with runtime values" {
    var index: usize = 0;
    try expect(!try isFieldOptional(Struct1, index));
    index += 1;
    try expect(try isFieldOptional(Struct1, index));
    index += 1;
    try expectError(error.IndexOutOfBounds, isFieldOptional(Struct1, index));
}

Struct1에 대한 isFieldOptional 호출은 아래 함수와 같은 모양으로 풀려서(unrolled) 생성돼요. 즉 inline 프롱이 값마다 몸통을 복제하는 격이죠.

// Calls to `isFieldOptional` on `Struct1` get unrolled to an equivalent
// of this function:
fn isFieldOptionalUnrolled(field_index: usize) !bool {
    return switch (field_index) {
        0 => false,
        1 => true,
        else => return error.IndexOutOfBounds,
    };
}
$ zig test test_inline_switch.zig
1/1 test_inline_switch.test.using @typeInfo with runtime values...OK
All 1 tests passed.

inline 키워드와 범위(ranges)의 조합

inline 키워드는 범위와 함께 쓸 수도 있어요. 아래처럼 0...field_types.len - 1 범위를 inline으로 처리하면, 범위 안의 각 값마다 프롱 본문이 생성되면서 각 idx가 컴파일 시점 값이 됩니다.

fn isFieldOptional(comptime T: type, field_index: usize) !bool {
    const field_types = @typeInfo(T).@"struct".field_types;
    return switch (field_index) {
        inline 0...field_types.len - 1 => |idx| @typeInfo(field_types[idx]) == .optional,
        else => return error.IndexOutOfBounds,
    };
}

inline else 프롱: inline for 루프의 타입 안전한 대안

inline else 프롱은 inline for 루프의 타입 안전한(type safe) 대안으로 쓸 수 있어요. inline for는 함수가 일련의 if 문으로 생성된 뒤 옵티마이저가 switch로 바꿔주기를 기대하는 반면, inline else는 처음부터 원하는 switch 모양으로 명시적으로 생성되고 컴파일러가 모든 경우가 처리됐는지 검사할 수 있어요.

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

const SliceTypeA = extern struct {
    len: usize,
    ptr: [*]u32,
};
const SliceTypeB = extern struct {
    ptr: [*]SliceTypeA,
    len: usize,
};
const AnySlice = union(enum(u8)) {
    a: SliceTypeA,
    b: SliceTypeB,
    c: []const u8,
    d: []AnySlice,
};

fn withFor(any: AnySlice) usize {
    const Tag = @typeInfo(AnySlice).@"union".tag_type.?;
    const info = @typeInfo(Tag).@"enum";
    inline for (info.field_names, info.field_values) |field_name, field_value| {
        // With `inline for` the function gets generated as
        // a series of `if` statements relying on the optimizer
        // to convert it to a switch.
        if (field_value == @backingInt(any)) {
            return @field(any, field_name).len;
        }
    }
    // When using `inline for` the compiler doesn't know that every
    // possible case has been handled requiring an explicit `unreachable`.
    unreachable;
}

fn withSwitch(any: AnySlice) usize {
    return switch (any) {
        // With `inline else` the function is explicitly generated
        // as the desired switch and the compiler can check that
        // every possible case is handled.
        inline else => |slice| slice.len,
    };
}

test "inline for and inline else similarity" {
    const any = AnySlice{ .c = "hello" };
    try expectEqual(5, withFor(any));
    try expectEqual(5, withSwitch(any));
}
$ zig test test_inline_else.zig
1/1 test_inline_else.test.inline for and inline else similarity...OK
All 1 tests passed.

union을 switch하는 경우: enum 태그 값도 comptime으로 얻기

union을 switch하는 인라인 프롱을 쓰면, payload는 런타임에만 알려져도 union의 enum 태그 값은 컴파일 시점에 얻을 수 있는 추가 캡처 변수를 쓸 수 있어요. 아래 예제에서 tag는 컴파일 시점에 알려진 값이라 tag == .b 같은 분기가 타입 안전하게 동작해요.

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

const U = union(enum) {
    a: u32,
    b: f32,
};

fn getNum(u: U) u32 {
    switch (u) {
        // Here `num` is a runtime-known value that is either
        // `u.a` or `u.b` and `tag` is `u`'s comptime-known tag value.
        inline else => |num, tag| {
            if (tag == .b) {
                return @intFromFloat(num);
            }
            return num;
        },
    }
}

test "test" {
    const u = U{ .b = 42 };
    try expectEqual(42, getNum(u));
}
$ zig test test_inline_switch_union_tag.zig
1/1 test_inline_switch_union_tag.test.test...OK
All 1 tests passed.

참고: while 루프 기초

이번 장 마지막으로, inline 프롱과 자주 함께 공부하게 되는 while 루프의 기본 형태를 같이 보면 좋아요. while 루프는 어떤 조건이 더 이상 참이 아닐 때까지 표현식을 반복해서 실행하는 데 써요.

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

test "while basic" {
    var i: usize = 0;
    while (i < 10) {
        i += 1;
    }
    try expectEqual(10, i);
}
$ zig test test_while.zig
1/1 test_while.test.while basic...OK
All 1 tests passed.

조건이 거짓이 되기 전에 루프를 빠져나오려면 break를 써요.

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

test "while break" {
    var i: usize = 0;
    while (true) {
        if (i == 10)
            break;
        i += 1;
    }
    try expectEqual(10, i);
}
$ zig test test_while_break.zig
1/1 test_while_break.test.while break...OK
All 1 tests passed.

루프의 시작 부분으로 되돌아가려면 continue를 써요.

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

test "while continue" {
    var i: usize = 0;
    while (true) {
        i += 1;
        if (i < 10)
            continue;
        break;
    }
    try expectEqual(10, i);
}
$ zig test test_while_continue.zig
1/1 test_while_continue.test.while continue...OK
All 1 tests passed.

while 루프는 continue 표현식(continue expression)도 지원해요. 루프가 계속될 때 이 표현식이 실행되며, continue 키워드는 이 표현식을 존중합니다.

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

test "while loop continue expression" {
    var i: usize = 0;
    while (i < 10) : (i += 1) {}
    try expectEqual(10, i);
}

test "while loop continue expression, more complicated" {
    var i: usize = 1;
    var j: usize = 1;
    while (i * j < 2000) : ({
        i *= 2;
        j *= 3;
    }) {
        const my_ij = i * j;
        try expect(my_ij < 2000);
    }
}
$ zig test test_while_continue_expression.zig
1/2 test_while_continue_expression.test.while loop continue expression...OK
2/2 test_while_continue_expression.test.while loop continue expression, more complicated...OK
All 2 tests passed.

while 루프는 **표현식(expression)**이에요. 그 표현식의 결과값은 while 루프의 else 절 결과인데, 이 else 절은 while 루프의 조건이 거짓으로 판정될 때 실행됩니다.

breakreturn처럼 값을 받을 수 있어요. 그 값이 while 표현식의 결과가 됩니다. while 루프에서 break로 나가면 else 분기는 평가되지 않아요.

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

test "while else" {
    try expect(rangeHasNumber(0, 10, 5));
    try expect(!rangeHasNumber(0, 10, 15));
}

fn rangeHasNumber(begin: usize, end: usize, number: usize) bool {
    var i = begin;
    return while (i < end) : (i += 1) {
        if (i == number) {
            break true;
        }
    } else false;
}
$ zig test test_while_else.zig
1/1 test_while_else.test.while else...OK
All 1 tests passed.

더 알아보기