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

インタフェース

Zenではインタフェースを使用して、複数の構造体型が共通して持つ特性を定義します。インタフェースを利用することで、異なる構造体に対して同一の操作が適用できるようになるため、より抽象的にプログラムを書くことが可能になります。

Zenのインタフェースの特徴は、実行時にコストがかからないことです。Zenコンパイラは、コンパイル時にインタフェースが満たされているかどうかを検査します。この特徴は静的ポリモーフィズムまたはゼロコスト抽象化と呼ばれるものです。Zenでのプログラミングは、実行時にコストをかけないことを重要視しているため、インタフェースによる静的な抽象化が第一の選択肢です。

全ての抽象化が静的に確定するわけではないため、実行時にコストを払うことで動的ポリモーフィズムを実現するvtableも用意されています。詳しくは6章 vtableを参照して下さい。

インタフェースの定義

インタフェースはinterfaceキーワードから始めて、宣言ブロック ({}) が続きます。宣言ブロック内には、フィールドの宣言メソッドの宣言とを書くことができます。インタフェースに宣言したフィールドやメソッドは、インタフェースが持つ制約の役割を果たします。ある構造体が、あるインタフェースを実装する、ということは、この制約を満たすことに他なりません。

具体例で見ていきましょう。

1回の取得データがu32のセンサを制御する構造体があるとしましょう。そのセンサは温度センサかもしれませんし、輝度センサかもしません。いずれにしても、u32でデータが取得できる、という特性を抽象化し、実際のセンサ種別に関わらず、同じ方法で扱えるようにしたいです。そこで、dataメソッドを持つセンサのインタフェースを、次のように定義します。

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

const Sensor = interface {
    fn data() u32;
};

fn data() u32は関数の宣言です。これは、Sensorインタフェースを実装するためには、構造体がfn data() u32のシグネチャに一致するメソッド (関数でないことに注意して下さい) を持つ必要があることを意味します。

つまりSensorインターフェースの制約を満たし、実装された構造体は次のように呼び出されることが保証されます。

    data : u32 = someSenser.data();

インタフェースの実装

それではSensorインタフェースを実装する構造体を定義してみましょう。Zenではインタフェースを実装するために特別な構文は不要です。ただ、インタフェースを満たすように構造体を定義するだけです。

examples/ch06-polymorphism/src/sensor_simplest.zen:8:13

const SensorA = struct {
    latest_data: u32 = 42,
    fn data(self: *SensorA) u32 {
        return self.latest_data;
    }
};

先ほど、インタフェースを実装する構造体は、fn data() u32のシグネチャに一致するメソッドを実装しなければならない、と書きました。SensorAは、自身のインスタンスを第一引数として受け取るdataメソッドを持っています。この2つのシグネチャは別物のように見えますが、インタフェースの定義では、メソッドの第一引数を省略しているのです。dataメソッドがどの型を第一引数で受け取るか、はインタフェースを実装する構造体ごとに異なるため、インタフェース定義の時点で型が書けないためです。

インタフェースの利用

インタフェースを使う時、コンパイラは、構造体型をインタフェース型に変換できるかどうか検査します。この時、Zenのインタフェースは、構造体に対する制約として機能します。ある構造体型があるインタフェース型に変換可能かどうか、はコンパイラが検証します。

次の例では、SensorA構造体のインスタンスを、Sensorインタフェースに変換してから、dataメソッドを呼び出しています。ここで、SensorA構造体のインスタンスがSensorインタフェースに変換可能なのは、SensorASensorインタフェースの制約を満たすためです。

examples/ch06-polymorphism/src/sensor_simplest.zen:15:19

test "cast to interface" {
    const a = SensorA {};
    var sensor: Sensor = a;
    ok(sensor.data() == 42);
}

例えば、SensorA構造体のdataメソッドを、u64を返すように変更してみましょう。

const SensorA = struct {
    fn data(self: SensorA) u64 {
        return 42;
    }
};

これはもちろんコンパイルエラーになります。特にnoteとして示されている内容から、SensorA.dataSensor.dataのシグネチャと一致しない、SensorASensorで制約されている、ということが読み取れます。

error[E02046]: expected 'u32', found 'u64'
    fn data(self: *SensorA) u64 {
                            ~
note[E00023]: 'SensorA.data' does not match the signature for 'Sensor.data'
    fn data() u32;
              ~
note[E00024]: 'SensorA' is being constrained by 'Sensor' here
    var sensor: Sensor = a;
                         ~

実用的には、インタフェースが構造体への制約として機能することを利用して、入り口となるゲートウェイ関数を通してインタフェースを利用します。すなわち、次のようなインタフェース型を引数にとる関数を用意し、そこでインタフェースのフィールドやメソッドを利用します。

examples/ch06-polymorphism/src/sensor_simplest.zen:21:23

fn data(sensor: Sensor) u32 {
    return sensor.data();
}

比較のために、もう1つセンサの構造体SensorBを用意します。

examples/ch06-polymorphism/src/sensor_simplest.zen:8:23

const SensorA = struct {
    latest_data: u32 = 42,
    fn data(self: *SensorA) u32 {
        return self.latest_data;
    }
};

const SensorB = struct {
    latest_data: u32 = 52,
    fn data(self: *SensorB) u32 {
        return self.latest_data;
    }
};

fn data(sensor: Sensor) u32 {
    return sensor.data();
}

ここまでの準備が完了すると、fn data(sensor: Sensor) u32関数を通して、SensorAインスタンスとSensorBインスタンスを共通の方法で扱うことができます。

test "use interface through a gateway" {
    var a = SensorA {};
    var b = SensorB {};
    // `a`と`b`は同じように扱える
    ok(data(&a) == 42);
    ok(data(&b) == 52);
}

インタフェースの振る舞いへの制約

インタフェースにより、ある構造体が制約を満たしているかどうか、を検査しながらより抽象的なコードを書けるようになります。ただし、ここで規定している制約は、構造上のもの、つまりある構造体が特定のフィールドもしくはメソッドを持っているかどうか、だけです。では、インタフェースの振る舞いに制約を持たせたい場合は、どうすれば良いのでしょうか。センサの例では、dataメソッドで読み出したデータは0以外でなければならない、という制約をインタフェース共通のものにしたい、といったことです。

もちろん、インタフェースのドキュメントとして振る舞いの制約を記述することはできます。しかし、可能な限りプログラマへの負担を減らす形で制約を規定すべきです。

そこで、ゲートウェイとなる関数内で振る舞いの制約を規定します。手段は、assertを使う方法や、別途エラー型を用意する方法があります。

fn data(sensor: Sensor) u32 {
    res = sensor.data();
    assert(res != 0);
    return res;
}

センサインタフェースの拡張

ここまでで作ったセンサインタフェースをもう少し実用的に拡張してみましょう。次のようなセンサインタフェースを実装して下さい。

  • センサが初期化完了しているかどうかを表す内部状態readyを持つ
  • センサを初期化するための関数probeを持つ
  • probeが成功して初期化が完了するとreadytrueになる
  • センサデータをu32で取得する関数dataを持つ

実装例を示します。

examples/ch06-polymorphism/src/sensor.zen:1:61

const std = @import("std");
const ok = std.testing.ok;

const Sensor = interface {
    // センサが初期化済みであれば`true`になる
    ready: bool,

    /// センサを初期化する。初期化が完了すると`ready`を`true`にする
    fn probe() void;
    /// センサからデータを取得する
    fn data() u32;
    /// `ms`で指定されたミリ秒間、スリープする
    fn sleep(ms: u32) void;
};

const SensorError = error {
    FailToInitialize,
    NotInitialized,
};

fn probe(sensor: Sensor) SensorError!void {
    sensor.probe();
    if (sensor.ready == false) {
        return SensorError.FailToInitialize;
    }
}

fn data(sensor: Sensor) SensorError!u32 {
    if (sensor.ready == false) {
        return SensorError.NotInitialized;
    }
    return sensor.data();
}

fn sleep(sensor: Sensor, ms: u32) void {
    sensor.sleep(ms);
}

const ASensor = struct {
    ready: bool = false,

    fn probe(self: *ASensor) void {
        self.ready = true;
    }

    fn data(self: ASensor) u32 {
        return 1;
    }

    fn sleep(self: ASensor, ms: u32) void {
        // sleep
    }
};

test "sensor" {
    var sensor = ASensor {};
    try probe(&sensor);
    const result = try data(&sensor);
    ok(result == 1);
    sleep(&sensor, 100);
}

☰ 人の生きた証は永遠に残るよう ☰
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.