ジェネリクス

あるプログラマーが次のような関数を書いたとします。

proc swapTwoInt(a, b: var int) =
  var tmp = a
  a = b
  b = tmp

proc reverseIntArray(arr: var openArray[int]) =
  var x = 0
  var y = arr.high
  while x < y:
    swapTwoInt(arr[x], arr[y])
    dec(y)
    inc(x)

この関数を使うと整数を型とする配列を逆順に並べることができます:

var intArr = [1, 2, 3, 4, 5]
reverseIntArray(intArr)
echo intArr # [5, 4, 3, 2, 1]

では、同様の処理を文字列 var msg = "miN morf olleH" に対して適用させたい場合はどうでしょう? swapreverseIntArray をよく見ると型 int に依存しない実装になっていますので int の部分を char に置換したコードを活用できそうです。

proc swapTwoChar(a, b: var char) =
  var tmp = a
  a = b
  b = tmp

proc reverseMsg(a: var openArray[char]) =
  var x = 0
  var y = a.high
  while x < y:
    swapTwoChar(a[x], a[y])
    dec(y)
    inc(x)

var msg = "miN morf olleH"
reverseMsg(msg)
echo msg # Hello from Nim

そうすると今度は次のタプル型を要素とする配列をソートさせたくなります。

type
  # Person = tuple[name: string, age: int]
  Person = tuple
    name: string
    age: int

これらの作業を延々と繰り返していくと各々の型に対応する似たようなコードが量産されてしまします。Nim はジェネリクスによってこうしたことを避けることができます。 ジェネリクスを使うことで、関数の引数の型をパラメータ化し、次のように型に関して抽象的なプログラムを行うことができます。

# sample.nim
proc swap[T](a, b: var T) =
  var tmp = a
  a = b
  b = tmp

proc reverse[T](arr: var openArray[T]) =
  var x = 0
  var y = arr.high
  while x < y:
    swap(arr[x], arr[y])
    dec(y)
    inc(x)

var intArr = [1, 2, 3, 4, 5]
reverse(intArr) # reverse[int](intArr) でも良い
echo intArr # [5, 4, 3, 2, 1]

var msg = "miN morf olleH"
reverse(msg) # reverse[char](msg) でも良い
echo msg # Hello from Nim

この場合、Nim はコンパイルの際に型を推定しコードを展開します。実際、コンパイル時に生成される C のコード (nimcache/sample.c) を見ると、 intchar 各々の場合に対応した reverse および swap のコードが生成されており、各々の関数が適切に呼び出される様子が確認できます。コンパイラは賢いですね。

TIP

実は、swap はすでに Nim に組み込まれており定義する必要はありません。また reverse は標準ライブラリ algorithm モジュールをインポートすることで利用できます。

ジェネリクスはオブジェクトに対しても適用できます。次の例はこちらの例を拡張して型 T 上の行列と行列の積を定義しています。

type
  #define M by N matrix over type T
  Matrix[T; M, N: static[int]] =
    array[1..M, array[1..N, T]]

var A: Matrix[int, 3, 2] = [[1, 0],
                            [0, 1],
                            [1, 0]]

var B: Matrix[int, 2, 3] = [[1, 0, 1],
                            [0, 1, 0]]

proc `*`[T, I, K, J](a: Matrix[T, I, K], b: Matrix[T, K, J]): Matrix[T, I, J] =
  var c: Matrix[T, I, J]
  for i in 1..I:
    for j in 1..J:
      for k in 1..K:
        c[i][j] += a[i][k] * b[k][j]
  result = c

var C: Matrix[int, 3, 3]

C = A * B

echo C # [[1, 0, 1], [0, 1, 0], [1, 0, 1]]

さらにジェネリクスはイテレータでも使うことができます。コードはこちらを参考にしました。

iterator reverseItems[T](x: openArray[T]): T =
  for i in countdown(x.high, x.low):
    yield x[i]

for c in reverseItems("foo"):
  echo c # o, o, f の順に出力される

var arr = [0, 1, 2]
for c in reverseItems(arr):
  echo c # 2, 1, 0 の順に出力される