オフィスアワーがそろそろ始まるよ!()

ジェネリクス

Zen言語は型を明示的に記述するコンパイル型言語です。そのため、様々な場所に型を記述しなければなりません。特に、関数の引数や戻り値の型については、型を省略することができません。次のようなu32の値を2倍にする関数があるとします。

fn double(x: u32) u32 {
    return x * 2;
}

この関数は、u32の引数と戻り値にのみ、利用することができます。では、f64の値を加算したい場合はどうすれば良いでしょうか?新しく次のような関数を用意しなければならないのでしょうか?

fn doubleF64(a: f64) f64 {
    return a * 2;
}

もちろん、このようなことをする必要はありません。Zenでは特定の型に依存しない関数を記述することが可能です。そのような特定の型に依存しないようなプログラミング手法をジェネリクスと呼びます。

ここでは、Zenのジェネリクスについて学びます。

ジェネリック関数

特定の型に依存しない関数をジェネリック関数と呼びます。Zenでジェネリック関数を作る方法は、大きく分けて2つあります。varパラメータを使う方法と、comptime typeを渡す方法です。

varパラメータ

まず、varパラメータを使う方法を説明します。次のコードでは、引数aの型はvarになっています。

examples/ch06-polymorphism/src/generics.zen:4:6

fn doubleByVarParam(a: var) @typeOf(a) {
    return a * 2;
}

このように書くと、引数aの型は、コンパイル時に推論されます。また、戻り値の型を@typeOf(a)とすることで、戻り値の型はaと同じ型になります。

上のコードの使い方を見てみましょう。ここでのポイントは、doubleByVarParamを2回呼び出していますが、それぞれの呼び出しで戻り値の型が異なっていることです。関数からの戻り値型が、引数の型と等しくなっています。

examples/ch06-polymorphism/src/generics.zen:8:16

test "use generic doule by parameter's type" {
    const resultU32 = doubleByVarParam(u32(1));
    ok(@typeOf(resultU32) == u32);
    ok(resultU32 == 2);

    const resultF64 = doubleByVarParam(f64(1));
    ok(@typeOf(resultF64) == f64);
    ok(resultF64 == 2.0);
}

comptime type

次に、comptime typeを渡す方法での実装を紹介します。この方法では、関数の引数として型を渡します。次のコードの第一引数 (comptime T: type) は、型を渡しています。typeは何らかの型であることを意味する型です。

この宣言以降、Tは型名として利用可能です。型名は必ずしもTである必要はありませんが、型 (Type) の頭文字であるTを使うことが多いです。

examples/ch06-polymorphism/src/generics.zen:18:20

fn doubleByComptimeType(comptime T: type, a: T) T {
    return a * 2;
}

この関数の使い方をお見せします。u32f64として使用する場合、次のようになります。doubleByComptimeType関数を2回呼び出していますが、それぞれの戻り値型は異なっています。

examples/ch06-polymorphism/src/generics.zen:22:32

test "use generic double" {
    // u32
    const resultU32 = doubleByComptimeType(u32, 1);
    ok(@typeOf(resultU32) == u32);
    ok(resultU32 == 2);

    // f64
    const resultF64 = doubleByComptimeType(f64, 1);
    ok(@typeOf(resultF64) == f64);
    ok(resultF64 == 2.0);
}

varパラメータ v.s. comptime type

ここまでで2つのジェネリクス実現方法を説明しました。では、なぜ2つの方法があるのでしょうか?上の例では、varパラメータを使う方法のほうが、引数が1つで済んで良いように思えます。

その答えは、型を推論できない場合comptime typeを使う、というものです。一例として、任意の整数型から別の整数型に変換する組込み関数@intCastを見てみましょう。

@intCastの関数シグネチャは次の通りです。第一引数にターゲットとする整数型、第二引数に任意の整数型 (関数シグネチャとしては任意の型ですが、整数型以外はコンパイルエラーになります) を受け取ります。

@intCast(comptime DestType: type, int: var) DestType

任意の整数型を受け取るために第二引数はvarパラメータを使用していますが、第二引数の型からターゲットの整数型を推論することはできません。例えば、u32からu64に変換したい場合、u64に変換したいことがu32の型情報からだけでは、コンパイラには知りようがありません。そこでコンパイラにターゲットとする整数型を教えてあげるために、comptime typeを使用します。

このように関数の引数型からだけでは、型が決定できないような場合、comptime typeを使ったジェネリクスを使用します。

ジェネリック構造体

関数だけではなく、ジェネリックな構造体を定義することができます。ジェネリックな構造体を定義する際は、無名構造体の型を戻り値とする関数を定義します。例えば、任意の型を格納できるスタックは、次のように定義可能です。

examples/ch06-polymorphism/src/generics.zen:48:72

fn Stack(comptime T : type) type {
    return struct {
        items: []T,
        top: usize,
        // ...
    };
}

関数の戻り値型がtypeになっています。これは、関数の戻り値がであることを意味しています (これまで見てきた関数はある型のを返していました) 。Stack関数の戻り値型は、保持するデータ型 (T) によって型が変わります。そのため具体的な型名を戻り値型として記述することができません。

そこで具体的な型名の代わりに、何らかの型を返すことを意味するtypeを戻り値型として指定し、実際の型は関数呼び出し時点で決定します。

Stack関数から返している構造体には名前がついていません。このような構造体を無名構造体と呼びます。無名構造体は、今回の例のように具体的な型名をプログラマが命名できない場合に便利です。

では、Stackの利用例を見てみましょう。Stack関数の戻り値は、構造体の型です。そのため、そのまま構造体の初期化を続けて記述することができます。

examples/ch06-polymorphism/src/generics.zen:34:46

test "generic stack test" {
    // `Stack`の返す無名構造体インスタンスを作成する
    var stack_u32 = Stack(u32) {
        .items = &([_]u32{ 0 } ** 10),
        .top = 0,
    };

    // `Stack`の返す無名構造体インスタンスを作成する
    var stack_f64 = Stack(f64) {
        .items = &([_]f64{ 0.0 } ** 10),
        .top = 0,
    };
}

もちろん、一度、Stack関数が返す構造体型に名前を与えることも可能です。一般的には、一度命名する方が好ましいと言えるでしょう。

    const StackU32 = Stack(u32);

関数 / メソッドを持つジェネリック構造体

ジェネリック構造体に関数およびメソッドを持たせることも可能です。ジェネリック構造体は、無名構造体を扱うため、構造体名がわかりません。そこで、@This組込み関数を利用して、構造体型名をSelfとするのが慣習です。

examples/ch06-polymorphism/src/generics.zen:48:72

fn Stack(comptime T : type) type {
    return struct {
        const Self = @This();

        items: []T,
        top: usize,

        fn init(buf: []T) Self {
            return Self {
                .items = buf,
                .top = 0,
            };
        }

        fn push(self: *Self, item: T) void {
            self.items[self.top] = item;
            self.top += 1;
        }

        fn pop(self: *Self) T {
            self.top -= 1;
            return self.items[self.top];
        }
    };
}

Selfを型名として使用していることと、型Tが随所に現れる以外は、通常の構造体と変わりありません。

☰ 人の生きた証は永遠に残るよう ☰
Copyright © 2018-2019 connectFree Corporation. All rights reserved.
Zen, the Zen three-circles logo and The Zen Programming Language are trademarks of connectFree corporation in Japan and other countries.