Zig는 효율적인 시스템 프로그래밍을 위해 독특한 설계 철학을 따릅니다. 다음은 주요 특징들을 심층적으로 분석합니다.
재귀적 데이터 구조 및 태그된 유니온
Zig에서 재귀적 데이터 구조를 정의할 때는 포인터(*Expr)가 필수적입니다. 이는 OCaml의 variant와 유사한 태그된 유니온(union(enum))을 통해 다양한 타입의 표현식을 안전하게 처리합니다. Expr 유니온은 binary와 int 두 가지 형태를 가지며, Binary 구조체는 op, lhs, rhs 필드를 포함하여 이진 연산 표현식을 구성합니다.
zig
pub const Expr = union(enum) {
binary: Binary,
int: usize,
pub const Binary = struct {
op: BinaryOp,
lhs: *Expr,
rhs: *Expr,
};
pub const BinaryOp = enum {
"+",
};
};
명시적 메모리 관리
Zig는 메모리 할당 및 해제를 매우 명시적으로 다룹니다. std.heap.page_allocator를 기반으로 std.heap.ArenaAllocator를 사용하여 효율적인 메모리 관리를 수행합니다. ArenaAllocator는 한 번에 많은 객체를 할당하고, 프로그램 종료 시 defer 키워드를 통해 한 번에 모든 할당된 메모리를 해제하는 RAII(Resource Acquisition Is Initialization)와 유사한 패턴을 제공합니다. 이는 C++의 RAII와 유사하며, 복잡한 메모리 해제 로직을 간소화합니다.
zig
const page_alloc = std.heap.page_allocator;
var arena = std.heap.ArenaAllocator.init(page_alloc);
defer arena.deinit();
const alloc = arena.allocator();
또한, clone 함수는 anytype과 @TypeOf를 활용하여 주어진 값의 타입을 동적으로 추론하고, 해당 타입 크기만큼 메모리를 할당하여 값을 복사하는 제네릭 기능을 제공합니다. 이는 Zig의 강력한 컴파일 타임 타입 시스템을 보여주는 예시입니다.
컴파일 타임 제네릭 및 트리 순회 (exprWalker)
exprWalker 함수는 Zig의 컴파일 타임 제네릭 기능을 활용하여 context와 error 타입을 파라미터로 받아 새로운 타입을 반환합니다. 이 반환된 타입은 context 필드와 Walker 타입의 함수 포인터를 포함합니다. Walker 함수 포인터는 실제 트리 순회 로직을 담당하며, true를 반환하면 순회를 중단하고, false를 반환하면 하위 노드로 재귀적으로 순회를 계속합니다. 이는 클로저가 없는 Zig에서 외부 상태(context)를 캡처하고 특정 로직을 주입하는 효과적인 방법입니다.
zig
pub fn exprWalker(T: type, E: type) type {
return struct {
context: T,
walker: Walker,
const Self = @This();
pub const Walker = *const fn (*Self, *Expr) E!bool;
pub fn run(self: *Self, node: *Expr) E!void {
// ... 순회 로직 ...
}
};
}
sumup 함수는 exprWalker를 사용하여 표현식 트리의 정수 노드들을 합산하는 구체적인 예시입니다. walk 함수 내에서 context 필드에 합계를 누적하며, switch 문을 통해 int 타입 노드를 찾아 처리합니다. 이는 OCaml의 패턴 매칭보다는 단순하지만, 충분히 유용하게 활용될 수 있습니다.