基本的なリスト操作を実装しましょう。
関数型言語では、lengthやmap、reduceのようなリスト操作がよく使われます。既存の関数を使わずに、一連の基本的なリスト操作を実装してみましょう。
実装する操作の正確な数や名前は、既存の名前との衝突を避けるため、トラックによって異なります。ただし、一般的には次のような操作を実装します。
append(2つのリストが与えられたとき、2つ目のリストのすべての要素を1つ目のリストの末尾に追加します)。concatenate(一連のリストが与えられたとき、すべてのリストの要素を1つの平坦なリストにまとめます)。filter(述語とリストが与えられたとき、predicate(item)がTrueになるすべての要素のリストを返します)。length(リストが与えられたとき、その中にある要素の総数を返します)。map(関数とリストが与えられたとき、すべての要素にfunction(item)を適用した結果のリストを返します)。foldl(関数・リスト・初期アキュムレーターが与えられたとき、各要素を左からアキュムレーターに畳み込みます(reduce))。foldr(関数・リスト・初期アキュムレーターが与えられたとき、各要素を右からアキュムレーターに畳み込みます(reduce))。reverse(リストが与えられたとき、元の要素をすべて逆順に並べたリストを返します)。なお、畳み込み関数(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という型は、Tがその型を表すジェネリックな名前であり、コンパイル時に実際の名前に置き換えられることをOdinのコンパイラーに伝えます。
そして、コンパイラーがジェネリック型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を返したでしょう。
2番目の文も最初の文とまったく同じように働きますが、コンパイラーはTをf64と特定します。
ジェネリック型には1文字の名前を付けるのが慣例です(TやEがよく使われます)。
これで、パラメータ多相(別名ジェネリック型)について、List Operations演習に取り組むのに十分な知識が身についたはずです。