기본적인 배열 연산을 구현해요.
함수형 언어에서는 length, map, reduce 같은 배열 연산을 아주 흔하게 사용해요.
기존 함수를 사용하지 않고, 일련의 기본적인 배열 연산을 직접 구현해요.
구현할 연산의 정확한 개수와 이름은 기존 이름과의 충돌을 피하기 위해 track마다 달라요. 하지만 일반적으로 구현하게 될 연산은 다음과 같아요:
append (주어진 두 배열에서, 두 번째 배열의 모든 항목을 첫 번째 배열의 끝에 추가해요);concatenate (주어진 여러 배열의 모든 항목을 하나로 평탄화한 배열로 합쳐요);filter (주어진 술어와 배열에서, predicate(item)이 True인 모든 항목의 배열을 반환해요);length (주어진 배열의 전체 항목 개수를 반환해요);map (주어진 함수와 배열에서, 모든 항목에 function(item)을 적용한 결과의 배열을 반환해요);foldl (주어진 함수, 배열, 초기 누적자를 받아, 왼쪽부터 각 항목을 누적자에 접어 넣어요(누적해요));foldr (주어진 함수, 배열, 초기 누적자를 받아, 오른쪽부터 각 항목을 누적자에 접어 넣어요(누적해요));reverse (주어진 배열의 모든 원래 항목을 뒤집힌 순서로 담은 배열을 반환해요).참고로, fold 함수(foldl, foldr)에 인자가 전달되는 순서는 중요해요.
이 연습 문제에서는 Odin의 매개변수 다형성(더 흔히 제네릭이라고 불러요)을 사용해야 해요. 이 기능을 처음 본다면, 시작할 수 있도록 간단히 살펴볼게요.
매개변수 다형성은 프로그래밍 언어의 기능으로, 코드에서 사용하는 타입에 대해 덜 구체적으로(더 일반적으로, 그래서 제네릭이라는 이름이 붙었어요) 작성하면서도 타입 안정성은 그대로 유지할 수 있게 해줘요. 당연히 이런 기능은 Odin처럼 강타입 언어에서만 의미가 있어요.
예제부터 시작해볼까요. 배열의 모든 요소를 고정된 값만큼 증가시키고 싶다고 해봐요. 아주 간단한 문제죠.
incr_array_int :: proc(a: []int, by: int) -> []int {
new_array := make([]int, len(a))
for i := 0; i < len(a); i+= 1 {
new_array[i] = a[i] + by
}
return new_array
}
그런데 이제 실수형에도 같은 기능이 필요하다면요?
incr_array_f64 :: proc(a: []f64, by: f64) -> []f64 {
new_array := make([]f64, len(a))
for i := 0; i < len(a); i+= 1 {
new_array[i] = a[i] + by
}
return new_array
}
그러고 나서 부호 없는 정수, 32비트 실수 등등은 어떨까요?
얼마 지나지 않아 똑같은 일을 하지만 타입만 다른 수많은 프로시저가 생겨나요. 로직을 수정해야 할 때가 되면 모든 변형에 대해 빠짐없이 고쳤는지 확인해야 하고, 이는 상당한 유지보수 부담이 돼요. 또 하나 번거로운 점은 Odin이 암시적 프로시저 오버로딩을 지원하지 않기 때문에 각 프로시저에 서로 다른 이름을 붙여야 한다는 거예요(명시적 오버로딩은 여전히 쓸 수 있지만, 그건 다른 연습 문제에서 다룰 이야기예요).
실용적인 언어답게 Odin은 매개변수 다형성이라는 해결책을 내놓아요.
컴파일러가 컴파일 시점에 매개변수의 타입을 알아낼 수만 있다면, 그 타입에 T 같은 제네릭 이름을 붙일 수 있어요.
위의 프로시저를 다시 써볼까요:
incr_array :: proc(a: []$T, by: T) -> []T {
new_array := make([]T, len(a))
for i := 0; i < len(a); i+= 1 {
new_array[i] = a[i] + by
}
return new_array
}
모든 타입 표기(int 또는 f64)를 T로 바꿨고, 첫 번째 T 앞에는 달러 기호($T)가 붙었다는 점을 눈여겨봐요.
$T 타입은 Odin 컴파일러에게 T라는 이름이 그 타입을 가리키는 제네릭 이름이며 컴파일 중에 실제 이름으로 바뀐다는 것을 알려줘요.
그리고 이제 컴파일러가 제네릭 타입 T를 알고 있으므로, 뒤따르는 같은 타입은 선택한 타입 이름(T)으로만 표시하면 돼요.
이제 다음과 같은 코드를 쓸 수 있어요:
a_int := incr_array([]int{1, 2, 3}, 10)
a_f64 := incr_array([]f64{1.0, 2.0, 3.0}, 10.0)
첫 번째 문에서 Odin 컴파일러는 첫 번째 매개변수의 타입인 []int를 제네릭 매개변수의 타입인 []$T와 일치시키고, T = int임을 추론한 다음, 뒤따르는 모든 T를 int로 바꾼 버전을 컴파일해요(위의 특수화된 버전 incr_array_int()와 같아요).
첫 번째 매개변수의 정의에서 달러 기호를 빼면, 컴파일러는 현재 패키지와 임포트 목록에서 T라는 타입을 찾았을 것이고, 십중팔구 컴파일 오류 Error: Undeclared name: T를 냈을 거예요.
두 번째 문은 첫 번째와 똑같이 동작하지만, 컴파일러가 T를 f64로 식별해요.
제네릭 타입에는 한 글자 이름을 붙이는 게 관례예요(T와 E가 자주 쓰여요).
이제 매개변수 다형성, 즉 제네릭 타입에 대해 List Operations 연습 문제를 풀 만큼은 알게 됐어요.