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 = someSensor.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
インタフェースに変換可能なのは、SensorA
がSensor
インタフェースの制約を満たすためです。
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.data
がSensor.data
のシグネチャと一致しない、SensorA
はSensor
で制約されている、ということが読み取れます。
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
が成功して初期化が完了するとready
がtrue
になる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: *mut 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(&mut sensor);
const result = try data(sensor);
ok(result == 1);
sleep(sensor, 100);
}
インタフェースを返すメソッドを持つインタフェースを定義することができます。
次の例のようにインタフェースを返すメソッドを持つインタフェースを定義すると、それを実装する構造体では戻り値としてインタフェース制約を満たす構造体を返すメソッドを定義することができます。
examples/ch06-polymorphism/src/return_interface.zen:5:47
const Interface = interface {
fn getSub() SubInterface;
};
const SubInterface = interface {
fn calc(lhs: u32, rhs: u32) u32;
};
const StructA = struct {
fn getSub(self: StructA) SubStructA {
return SubStructA{};
}
};
const SubStructA = struct {
fn calc(self: SubStructA, lhs: u32, rhs: u32) u32 {
return lhs + rhs;
}
};
const StructB = struct {
fn getSub(self: StructB) SubStructB {
return SubStructB{};
}
};
const SubStructB = struct {
fn calc(self: SubStructB, lhs: u32, rhs: u32) u32 {
return lhs * rhs;
}
};
test "interface has method that returns interface" {
const a: Interface = StructA{};
const b: Interface = StructB{};
const sub_a: SubInterface = a.getSub();
const sub_b: SubInterface = b.getSub();
ok(@TypeOf(sub_a) == SubStructA);
ok(@TypeOf(sub_b) == SubStructB);
ok(sub_a.calc(1, 2) == 3);
ok(sub_b.calc(1, 2) == 2);
}
また、次のようにエラー型とインタフェースをあわせてエラー共用体を返すようにすることもできます。
examples/ch06-polymorphism/src/return_interface.zen:50:100
const SomeError = error {
OtherError,
};
const Interface2 = interface {
fn getSub() SomeError!SubInterface2;
};
const SubInterface2 = interface {
fn calc(lhs: u32, rhs: u32) u32;
};
const StructA2 = struct {
fn getSub(self: StructA2) SomeError!SubStructA2 {
// エラーの発生する可能性のある処理
// ...
return SubStructA2{};
}
};
const SubStructA2 = struct {
fn calc(self: SubStructA2, lhs: u32, rhs: u32) u32 {
return lhs + rhs;
}
};
const StructB2 = struct {
fn getSub(self: StructB2) SomeError!SubStructB2 {
// エラーの発生する処理
// ...
return SubStructB2{};
}
};
const SubStructB2 = struct {
fn calc(self: SubStructB2, lhs: u32, rhs: u32) u32 {
return lhs * rhs;
}
};
test "interface has method that returns interface with error" {
const a: Interface2 = StructA2{};
const b: Interface2 = StructB2{};
const sub_a: SubInterface2 = try a.getSub();
const sub_b: SubInterface2 = try b.getSub();
ok(@TypeOf(sub_a) == SubStructA2);
ok(@TypeOf(sub_b) == SubStructB2);
ok(sub_a.calc(1, 2) == 3);
ok(sub_b.calc(1, 2) == 2);
}
☰ 人の生きた証は永遠に残るよう ☰
Copyright © 2018-2020 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.