Zig vs Go: 제네릭
Source: Dev.to
번역할 텍스트가 제공되지 않았습니다. 번역을 원하는 본문을 알려주시면 한국어로 번역해 드리겠습니다.
Introduction
Go는 1.18 버전에서 제네릭을 도입했으며, 이를 통해 함수와 구조체를 타입으로 매개변수화할 수 있습니다. Zig는 오래전부터 comptime 키워드를 통해 컴파일 타임 제네릭을 지원해 왔으며, 이는 런타임 오버헤드가 없습니다.
함수 제네릭
Go
func doubleNumber[T constraints.Integer | constraints.Float](a T) T {
return a * T(2)
}
doubledFloat := doubleNumber[float32](5.3)
fmt.Println(doubledFloat)
// type parameter inferred
doubledInteger := doubleNumber(5)
fmt.Println(doubledInteger)
Zig
fn doubleNumber(comptime T: type, a: T) T {
return a * 2;
}
const doubledFloat = doubleNumber(f32, 5.3);
std.debug.print("{}\n", .{doubledFloat});
const doubledInteger = doubleNumber(i32, 5);
std.debug.print("{}\n", .{doubledInteger});
Go에서는 제네릭이 인터페이스에 의해 제한되며, 컴파일러가 컴파일 시점에 이를 검사합니다. Zig는 제공된 타입에 대해 함수를 인스턴스화할 때 컴파일‑타임 단계에서 오류를 평가합니다.
구조체 제네릭
Go
type Stack[T any, Q any] struct {
Items []T
AlterItems []Q
Index int
}
intStack := Stack[int8, float32]{
Items: []int8{1, 2, 3, 4, 5},
AlterItems: []float32{1.1, 2.1, 3.1, 4.7, 5.3},
Index: 4,
}
fmt.Println(intStack.Items[intStack.Index], intStack.AlterItems[intStack.Index])
strStack := Stack[string, bool]{
Items: []string{"hello", "gophers"},
AlterItems: []bool{false, true},
Index: 0,
}
fmt.Println(strStack.Items[strStack.Index], strStack.AlterItems[strStack.Index])
Zig
fn Stack(comptime T: type, comptime Q: type) type {
return struct {
items: []const T,
alterItems: []const Q,
index: usize,
};
}
const intArr = [_]i32{ 1, 2, 3, 4, 5 };
const floatArr = [_]f32{ 1.1, 2.1, 3.1, 4.7, 5.3 };
const intStack = Stack(i32, f32){
.items = intArr[0..],
.alterItems = floatArr[0..],
.index = 4,
};
std.debug.print("{d} {d}\n", .{
intStack.items[intStack.index],
intStack.alterItems[intStack.index],
});
const strArr = [_][]const u8{ "hello", "ziguanas" };
const boolArr = [_]bool{ false, true };
const strStack = Stack([]const u8, bool){
.items = &strArr,
.alterItems = &boolArr,
.index = 1,
};
std.debug.print("{s} {}\n", .{
strStack.items[strStack.index],
strStack.alterItems[strStack.index],
});
제약 vs. 인터페이스
Go에서는 제약(constraints)을 인터페이스로 표현합니다. 제네릭 함수가 구조체를 매개변수로 받을 때, 해당 구조체는 함수 내부에서 사용되는 메서드를 구현하고 있어야 합니다.
type IdTrackable interface {
GetID() string
}
type Article struct {
ID string
Name string
Category string
}
func (a Article) GetID() string { return a.ID }
type SKU struct {
ID string
ArticleID string
Available bool
}
func (s SKU) GetID() string { return s.ID }
func findById[T IdTrackable](items []T, id string) (*T, error) {
for _, item := range items {
if item.GetID() == id {
return &item, nil
}
}
return nil, errors.New("not found")
}
Example: Find by ID
Go 사용법
articles := []Article{
{ID: "a1", Name: "Laptop", Category: "Electronics"},
{ID: "a2", Name: "Book", Category: "Education"},
}
article, err := findById(articles, "a1")
if err == nil {
fmt.Printf("Found article: %+v\n", *article)
}
Zig 구현
const Article = struct {
id: []const u8,
name: []const u8,
category: []const u8,
};
const SKU = struct {
id: []const u8,
articleId: []const u8,
available: bool,
};
fn findById(comptime T: type, items: []const T, idx: []const u8) !T {
for (items) |item| {
if (std.mem.eql(u8, @field(item, "id"), idx)) {
return item;
}
}
return error.NotFound;
}
const articles = [_]Article{
.{ .id = "a1", .name = "Laptop", .category = "Electronics" },
.{ .id = "a2", .name = "Book", .category = "Education" },
};
const art = try findById(Article, &articles, "a2");
std.debug.print("Found article: {}\n", .{art});
Zig에서는 제네릭 함수가 각 고유 타입마다 컴파일 타임에 인스턴스화되어, 런타임 비용 없이 별도의 특수화된 버전을 생성합니다.
결론
Zig의 컴파일 타임 메타프로그래밍은 강력하고 오버헤드가 없는 제네릭 접근 방식을 제공하며, Go의 제네릭은 인터페이스에 의해 제한된 런타임 없이 타입 파라미터에 의존합니다. 두 언어 모두 타입 안전하고 재사용 가능한 코드를 가능하게 하지만, Zig는 현재 Go가 지원하는 범위를 넘어서는 보다 폭넓은 메타프로그래밍 기능을 제공합니다.