인라인 스위치 프롱
인라인 스위치 프롱 (Inline Switch Prongs)
스위치 프롱을 inline으로 표시하면, 그 프롱이 가질 수 있는 각 값마다 프롱의 본문을 따로 생성해요. 이렇게 하면 해당 프롱에서 잡은(captured) 값이 comptime 값이 됩니다. 런타임 값이 아니라 컴파일 시점에 정해진 값으로 코드를 분석하고 싶을 때 쓰는 기능이죠.
본문
기본 예제: 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 루프의 조건이 거짓으로 판정될 때 실행됩니다.
break는 return처럼 값을 받을 수 있어요. 그 값이 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.