제네릭
Vais 제네릭 및 트레이트 심화 튜토리얼
이 튜토리얼은 Vais의 제네릭 프로그래밍과 트레이트 시스템을 심층적으로 다룹니다. 타입 추론, 모노모피제이션, 그리고 실전 활용법을 배웁니다.
목차
제네릭 개념
제네릭이란?
제네릭은 타입을 매개변수화하여 여러 타입에 대해 동작하는 코드를 작성할 수 있게 합니다.
제네릭 없이:
# i64용 함수
F identity_i64(x: i64) -> i64 = x
# f64용 함수
F identity_f64(x: f64) -> f64 = x
# str용 함수
F identity_str(x: str) -> str = x
# 각 타입마다 중복 코드!
제네릭 사용:
# 하나의 함수로 모든 타입 처리
F identity<T>(x: T) -> T = x
F main() -> i64 {
a := identity(42) # T = i64
b := identity(3.14) # T = f64
c := identity("hello") # T = str
0
}
Vais 제네릭의 특징
- Monomorphization: 컴파일 시 각 사용된 타입에 대해 코드 생성
- Zero-cost Abstraction: 런타임 오버헤드 없음
- 타입 추론: 대부분의 경우 타입 명시 불필요
- 정적 디스패치: 컴파일 타임에 모든 타입 결정
제네릭 함수
기본 제네릭 함수
단일 타입 매개변수:
# 가장 간단한 제네릭 함수
F identity<T>(x: T) -> T = x
# 여러 번 사용
F first<T>(a: T, b: T) -> T = a
F second<T>(a: T, b: T) -> T = b
# 블록 형태
F duplicate<T>(x: T) -> T {
puts("Duplicating value")
x
}
F main() -> i64 {
# 타입 추론 - 명시 불필요
x := identity(100)
y := first(10, 20)
z := duplicate(42)
print_i64(x + y + z) # 152
0
}
다중 타입 매개변수
여러 타입 매개변수를 사용:
# 두 개의 타입 매개변수
F pair<A, B>(a: A, b: B) -> A = a
# 타입이 다른 값들 처리
F first_of_pair<A, B>(a: A, b: B) -> A = a
F second_of_pair<A, B>(a: A, b: B) -> B = b
# 삼중 타입
F choose_first<A, B, C>(a: A, b: B, c: C) -> A = a
F main() -> i64 {
# A=i64, B=f64
x := pair(10, 3.14)
# A=i64, B=str
y := first_of_pair(42, "hello")
# A=i64, B=f64, C=str
z := choose_first(100, 2.5, "world")
print_i64(x + y + z) # 152
0
}
제네릭 함수의 타입 추론
Vais는 대부분의 경우 타입을 자동으로 추론합니다:
F swap<A, B>(a: A, b: B) -> (B, A) {
(b, a)
}
F main() -> i64 {
# 타입 추론: A=i64, B=i64
(x, y) := swap(10, 20)
# 타입 추론: A=i64, B=str
(num, text) := swap(42, "answer")
print_i64(x) # 20
0
}
제네릭과 Self-Recursion
제네릭 함수에서 @ 사용:
# 제네릭 재귀 함수
F repeat<T>(x: T, count: i64) -> T {
I count <= 0 {
x
} E {
@(x, count - 1)
}
}
F main() -> i64 {
result := repeat(42, 5)
print_i64(result) # 42
0
}
제네릭 구조체
기본 제네릭 구조체
# 단일 타입 매개변수
S Box<T> {
value: T
}
# 생성 및 사용
F main() -> i64 {
# Box<i64>
int_box := Box { value: 42 }
# Box<f64>
float_box := Box { value: 3.14 }
print_i64(int_box.value) # 42
0
}
다중 타입 매개변수 구조체
# Pair - 두 개의 값을 담는 구조체
S Pair<T> {
first: T,
second: T
}
# Container - 서로 다른 타입
S Container<K, V> {
key: K,
value: V
}
# Triple - 세 개의 타입
S Triple<A, B, C> {
first: A,
second: B,
third: C
}
F main() -> i64 {
# Pair<i64>
pair := Pair { first: 10, second: 20 }
# Container<i64, str>
container := Container { key: 1, value: "data" }
# Triple<i64, f64, str>
triple := Triple { first: 42, second: 3.14, third: "hello" }
print_i64(pair.first) # 10
0
}
제네릭 구조체 메서드
X 키워드로 제네릭 구조체에 메서드 추가:
S Pair<T> {
first: T,
second: T
}
# Pair에 메서드 구현
X Pair {
# self는 Pair<T> 타입
F sum(&self) -> i64 {
self.first + self.second
}
F swap(&self) -> Pair {
Pair { first: self.second, second: self.first }
}
F first_value(&self) -> T {
self.first
}
}
F main() -> i64 {
p := Pair { first: 10, second: 20 }
# 메서드 호출
total := p.sum() # 30
swapped := p.swap() # Pair { first: 20, second: 10 }
first := p.first_value() # 10
print_i64(total)
print_i64(swapped.first)
print_i64(first)
0
}
중첩 제네릭 구조체
S Box<T> {
value: T
}
S Pair<T> {
first: T,
second: T
}
F main() -> i64 {
# Box<i64>
simple := Box { value: 42 }
# Pair<i64>
pair := Pair { first: 10, second: 20 }
# Box<Pair<i64>> - 중첩 제네릭
boxed_pair := Box {
value: Pair { first: 1, second: 2 }
}
# 접근
inner_pair := boxed_pair.value
first_val := inner_pair.first
print_i64(first_val) # 1
0
}
제네릭 Enum
기본 제네릭 Enum
# Option<T> - 값이 있거나 없음
E Option<T> {
None,
Some(T)
}
# Result<T, E> - 성공 또는 에러
E Result<T, E> {
Ok(T),
Err(E)
}
F main() -> i64 {
# Option<i64>
opt_int := Some(42)
opt_none := None
# Result<i64, str>
result_ok := Ok(100)
result_err := Err("Error message")
0
}
제네릭 Enum 패턴 매칭
E Option<T> {
None,
Some(T)
}
F unwrap_or<T>(opt: Option<T>, default: T) -> T {
M opt {
Some(value) => value,
None => default
}
}
F main() -> i64 {
opt1 := Some(42)
opt2 := None
v1 := unwrap_or(opt1, 0) # 42
v2 := unwrap_or(opt2, 99) # 99
print_i64(v1)
print_i64(v2)
0
}
다중 variant 제네릭 Enum
E Either<L, R> {
Left(L),
Right(R)
}
F process_either<L, R>(either: Either<L, R>) -> i64 {
M either {
Left(l) => {
puts("Got Left")
0
},
Right(r) => {
puts("Got Right")
1
}
}
}
F main() -> i64 {
# Either<i64, str>
left := Left(42)
right := Right("hello")
r1 := process_either(left) # 0
r2 := process_either(right) # 1
print_i64(r1 + r2) # 1
0
}
트레이트 정의와 구현
트레이트 정의
W 키워드로 트레이트(인터페이스) 정의:
# Printable 트레이트
W Printable {
F print(&self) -> i64
}
# Comparable 트레이트
W Comparable {
F compare(&self, other: &Self) -> i64
}
# 여러 메서드를 가진 트레이트
W Drawable {
F draw(&self) -> i64
F erase(&self) -> i64
F move(&self, x: i64, y: i64) -> i64
}
트레이트 구현
X 키워드로 구조체에 트레이트 구현:
W Printable {
F print(&self) -> i64
}
S Point {
x: i64,
y: i64
}
# Point에 Printable 구현
X Point: Printable {
F print(&self) -> i64 {
puts("Point(")
print_i64(self.x)
puts(", ")
print_i64(self.y)
puts(")")
putchar(10)
0
}
}
F main() -> i64 {
p := Point { x: 10, y: 20 }
p.print() # Point(10, 20)
0
}
여러 트레이트 구현
W Printable {
F print(&self) -> i64
}
W Resettable {
F reset(&self) -> i64
}
S Counter {
value: i64
}
# Printable 구현
X Counter: Printable {
F print(&self) -> i64 {
puts("Counter: ")
print_i64(self.value)
putchar(10)
0
}
}
# Resettable 구현
X Counter: Resettable {
F reset(&self) -> i64 {
self.value = 0
0
}
}
F main() -> i64 {
c := Counter { value: 42 }
c.print() # Counter: 42
c.reset()
c.print() # Counter: 0
0
}
트레이트 없는 메서드 (Impl)
트레이트 없이 직접 메서드 추가:
S Rectangle {
width: i64,
height: i64
}
# 트레이트 없이 메서드 추가
X Rectangle {
F area(&self) -> i64 {
self.width * self.height
}
F perimeter(&self) -> i64 {
(self.width + self.height) * 2
}
F is_square(&self) -> i64 {
I self.width == self.height { 1 } E { 0 }
}
}
F main() -> i64 {
rect := Rectangle { width: 10, height: 20 }
area := rect.area() # 200
perimeter := rect.perimeter() # 60
is_sq := rect.is_square() # 0
print_i64(area)
print_i64(perimeter)
0
}
제네릭 바운드
트레이트 바운드 개념
제네릭 타입에 제약 조건을 추가:
# T는 Printable을 구현해야 함
F print_twice<T: Printable>(value: &T) -> i64 {
value.print()
value.print()
0
}
Where 절 (개념적)
복잡한 바운드 표현:
# 여러 바운드
F process<T>(value: T) -> i64
where
T: Printable,
T: Comparable
{
value.print()
0
}
다중 바운드 예제
W Display {
F display(&self) -> i64
}
W Clone {
F clone(&self) -> Self
}
S Point {
x: i64,
y: i64
}
X Point: Display {
F display(&self) -> i64 {
puts("(")
print_i64(self.x)
puts(",")
print_i64(self.y)
puts(")")
0
}
}
X Point: Clone {
F clone(&self) -> Point {
Point { x: self.x, y: self.y }
}
}
F show_and_clone<T>(value: &T) -> T
where
T: Display,
T: Clone
{
value.display()
value.clone()
}
F main() -> i64 {
p := Point { x: 10, y: 20 }
p2 := show_and_clone(&p)
p2.display() # (10,20)
0
}
표준 라이브러리 제네릭
Vec - 동적 배열
U std/vec
F main() -> i64 {
# Vec<i64> 생성
v := Vec.with_capacity(10)
# 요소 추가
v.push(10)
v.push(20)
v.push(30)
# 길이 확인
len := v.len() # 3
# 요소 접근
first := v.get(0) # 10
second := v.get(1) # 20
# 요소 제거
last := v.pop() # 30
print_i64(len)
print_i64(first)
print_i64(last)
v.drop() # 메모리 해제
0
}
Option - 선택적 값
U std/option
F safe_divide(a: i64, b: i64) -> Option<i64> {
I b == 0 {
None
} E {
Some(a / b)
}
}
F main() -> i64 {
result := safe_divide(10, 2)
# 패턴 매칭
M result {
Some(value) => {
puts("Result: ")
print_i64(value) # 5
},
None => {
puts("Division by zero!")
}
}
# unwrap_or 메서드
value := result.unwrap_or(0)
print_i64(value) # 5
# is_some / is_none
has_value := result.is_some() # 1
0
}
Result<T, E> - 에러 처리
E Result<T, E> {
Ok(T),
Err(E)
}
F parse_positive(x: i64) -> Result<i64, str> {
I x < 0 {
Err("Negative number")
} E I x == 0 {
Err("Zero")
} E {
Ok(x)
}
}
F main() -> i64 {
result1 := parse_positive(42)
result2 := parse_positive(-5)
M result1 {
Ok(value) => {
puts("Success: ")
print_i64(value) # 42
},
Err(msg) => {
puts("Error: ")
puts(msg)
}
}
M result2 {
Ok(value) => {
puts("Success: ")
print_i64(value)
},
Err(msg) => {
puts("Error: ")
puts(msg) # "Negative number"
}
}
0
}
HashMap<K, V> - 해시맵
U std/hashmap
F main() -> i64 {
# HashMap<i64, i64>
map := HashMap.with_capacity(10)
# 삽입
map.insert(1, 100)
map.insert(2, 200)
map.insert(3, 300)
# 조회
value := map.get(2) # 200
# 존재 확인
exists := map.contains(1) # 1
# 크기
size := map.len() # 3
print_i64(value)
print_i64(size)
map.drop()
0
}
고급 패턴
제네릭 팩토리 패턴
S Box<T> {
value: T
}
X Box<T> {
# 정적 메서드 - 생성자
F new(value: T) -> Box<T> {
Box { value: value }
}
F get(&self) -> T {
self.value
}
F set(&self, new_value: T) -> i64 {
self.value = new_value
0
}
}
F main() -> i64 {
# 팩토리 메서드 사용
box1 := Box::new(42)
box2 := Box::new(3.14)
val1 := box1.get() # 42
box1.set(100)
val2 := box1.get() # 100
print_i64(val1)
print_i64(val2)
0
}
제네릭 빌더 패턴
S Builder<T> {
value: T,
count: i64
}
X Builder<T> {
F new(initial: T) -> Builder<T> {
Builder { value: initial, count: 0 }
}
F with_count(&self, n: i64) -> Builder<T> {
self.count = n
self
}
F build(&self) -> T {
self.value
}
}
F main() -> i64 {
builder := Builder::new(42)
builder.with_count(10)
result := builder.build()
print_i64(result) # 42
0
}
제네릭 컨테이너 패턴
# Stack<T>
S Stack<T> {
data: Vec<T>,
top: i64
}
X Stack<T> {
F new() -> Stack<T> {
Stack {
data: Vec.with_capacity(10),
top: 0
}
}
F push(&self, value: T) -> i64 {
self.data.push(value)
self.top = self.top + 1
self.top
}
F pop(&self) -> Option<T> {
I self.top > 0 {
self.top = self.top - 1
value := self.data.pop()
Some(value)
} E {
None
}
}
F is_empty(&self) -> i64 {
I self.top == 0 { 1 } E { 0 }
}
}
F main() -> i64 {
stack := Stack::new()
stack.push(10)
stack.push(20)
stack.push(30)
M stack.pop() {
Some(v) => print_i64(v), # 30
None => puts("Empty")
}
M stack.pop() {
Some(v) => print_i64(v), # 20
None => puts("Empty")
}
0
}
타입 추론
타입 추론 규칙
Vais는 강력한 타입 추론을 제공합니다:
F identity<T>(x: T) -> T = x
F main() -> i64 {
# 리터럴로부터 추론
a := identity(42) # T = i64
b := identity(3.14) # T = f64
c := identity("text") # T = str
# 변수로부터 추론
x := 100
d := identity(x) # T = i64 (x의 타입)
# 반환 타입으로부터 추론
result: i64 = identity(50) # T = i64
0
}
컨텍스트 기반 추론
S Pair<T> {
first: T,
second: T
}
F make_pair<T>(a: T, b: T) -> Pair<T> {
Pair { first: a, second: b }
}
F main() -> i64 {
# 반환 타입으로부터 추론
p1: Pair<i64> = make_pair(10, 20) # T = i64
# 인자로부터 추론
p2 := make_pair(1, 2) # T = i64
print_i64(p1.first) # 10
print_i64(p2.first) # 1
0
}
추론 실패와 명시적 타입
추론이 불가능한 경우:
# 이 함수는 T를 추론할 정보가 없음
F create_default<T>() -> T {
# 기본값 반환
0
}
F main() -> i64 {
# 에러: T를 추론할 수 없음
# x := create_default()
# 해결: 타입 명시
x: i64 = create_default() # T = i64
print_i64(x)
0
}
실전 예제
예제 1: 제네릭 연결 리스트
E List<T> {
Nil,
Cons(T, i64) # (value, next pointer)
}
X List<T> {
F new() -> List<T> {
Nil
}
F prepend(&self, value: T) -> List<T> {
# self를 next로 하는 새 노드 생성
Cons(value, 0) # 간소화
}
F is_empty(&self) -> i64 {
M self {
Nil => 1,
Cons(_, _) => 0
}
}
F len(&self) -> i64 {
M self {
Nil => 0,
Cons(_, next_ptr) => {
# 재귀적으로 길이 계산
1
}
}
}
}
F main() -> i64 {
list := List::new()
node1 := list.prepend(10)
node2 := node1.prepend(20)
node3 := node2.prepend(30)
is_empty := node3.is_empty() # 0
len := node3.len() # 1
print_i64(is_empty)
print_i64(len)
0
}
예제 2: 제네릭 Tree
E Tree<T> {
Empty,
Node(T, i64, i64) # (value, left, right)
}
X Tree<T> {
F empty() -> Tree<T> {
Empty
}
F leaf(value: T) -> Tree<T> {
Node(value, 0, 0)
}
F height(&self) -> i64 {
M self {
Empty => 0,
Node(_, _, _) => 1
}
}
F is_leaf(&self) -> i64 {
M self {
Empty => 0,
Node(_, left, right) => {
I left == 0 && right == 0 { 1 } E { 0 }
}
}
}
}
F main() -> i64 {
empty := Tree::empty()
leaf := Tree::leaf(42)
h1 := empty.height() # 0
h2 := leaf.height() # 1
is_l := leaf.is_leaf() # 1
print_i64(h1)
print_i64(h2)
print_i64(is_l)
0
}
예제 3: 제네릭 Iterator
W Iterator<T> {
F next(&self) -> Option<T>
F has_next(&self) -> i64
}
S RangeIterator {
current: i64,
end: i64
}
X RangeIterator: Iterator<i64> {
F next(&self) -> Option<i64> {
I self.current < self.end {
value := self.current
self.current = self.current + 1
Some(value)
} E {
None
}
}
F has_next(&self) -> i64 {
I self.current < self.end { 1 } E { 0 }
}
}
F sum_iterator<T>(iter: &Iterator<T>) -> i64 {
sum := 0
L iter.has_next() {
M iter.next() {
Some(value) => {
sum = sum + value
},
None => B
}
}
sum
}
F main() -> i64 {
iter := RangeIterator { current: 0, end: 10 }
# 0+1+2+...+9 = 45
total := sum_iterator(&iter)
print_i64(total) # 45
0
}
예제 4: 제네릭 캐시
U std/hashmap
U std/option
S Cache<K, V> {
map: HashMap<K, V>,
capacity: i64
}
X Cache<K, V> {
F new(capacity: i64) -> Cache<K, V> {
Cache {
map: HashMap.with_capacity(capacity),
capacity: capacity
}
}
F get(&self, key: K) -> Option<V> {
has := self.map.contains(key)
I has {
value := self.map.get(key)
Some(value)
} E {
None
}
}
F put(&self, key: K, value: V) -> i64 {
# 용량 체크
I self.map.len() >= self.capacity {
puts("Cache full!")
0
} E {
self.map.insert(key, value)
1
}
}
F clear(&self) -> i64 {
self.map.clear()
0
}
}
F main() -> i64 {
cache := Cache::new(3)
# 데이터 저장
cache.put(1, 100)
cache.put(2, 200)
cache.put(3, 300)
# 조회
M cache.get(2) {
Some(value) => {
puts("Found: ")
print_i64(value) # 200
},
None => {
puts("Not found")
}
}
# 없는 키
M cache.get(99) {
Some(value) => {
puts("Found: ")
print_i64(value)
},
None => {
puts("Not found") # 이쪽 실행
}
}
cache.clear()
0
}
성능 고려사항
Monomorphization의 장단점
장점:
- 런타임 오버헤드 없음 (virtual dispatch 불필요)
- 인라이닝 최적화 가능
- 타입별 최적화 가능
단점:
- 코드 크기 증가 (각 타입마다 별도 코드)
- 컴파일 시간 증가
제네릭 사용 팁
- 자주 사용하는 타입만 제네릭으로:
# 좋음: 재사용성 높음
F swap<T>(a: T, b: T) -> (T, T) {
(b, a)
}
# 피하기: 한 번만 사용
F process_int(x: i64) -> i64 = x * 2
# 제네릭 불필요
- 타입 수 제한:
# 많은 타입에 사용 -> 큰 바이너리
F generic<A, B, C, D, E>(...) -> ... { ... }
# 필요한 만큼만
F simple<T>(x: T) -> T { x }
- 인라인 가능한 작은 함수:
# 인라인되기 좋음
F identity<T>(x: T) -> T = x
# 큰 함수는 신중히
F complex<T>(x: T) -> T {
# 100줄 코드...
}
요약
핵심 개념
- 제네릭 함수:
F name<T>(x: T) -> T - 제네릭 구조체:
S Name<T> { field: T } - 제네릭 Enum:
E Name<T> { Variant(T) } - 트레이트 정의:
W Trait { F method(&self) } - 트레이트 구현:
X Type: Trait { F method(&self) { ... } } - 제네릭 바운드:
<T: Trait>
베스트 프랙티스
- ✅ 타입 추론 활용
- ✅ 재사용성 높은 코드에 제네릭 사용
- ✅ 트레이트로 공통 인터페이스 정의
- ✅ 표준 라이브러리 제네릭 타입 활용 (Vec, Option, Result)
- ❌ 과도한 제네릭화 피하기
- ❌ 불필요한 트레이트 바운드 피하기
다음 단계
- 고급 트레이트 패턴
- 제네릭과 Async 결합
- 커스텀 컬렉션 구현
- 제네릭 라이브러리 설계
참고 자료
- 기본 튜토리얼:
TUTORIAL.md - Async 튜토리얼:
async_tutorial.md - 언어 스펙:
LANGUAGE_SPEC.md - 표준 라이브러리:
STDLIB.md - 예제 코드:
examples/generic_struct_test.vaisexamples/generic_bounds_test.vaisexamples/trait_test.vaisexamples/option_test.vais
Happy generic coding with Vais!