みつきんのメモ

組み込みエンジニアです。Interface誌で「My オリジナルLinuxの作り方」連載中

Zen言語の構造体入門

はじめに

Zen入門中の自分用の備忘録。

詳細はここにある。

使用するZenコンパイラのバージョンは0.8.20191124+552247019(2020/03/12時点のパブリックベータ版)。

基本

src/foo.zenとして次のような構造体を定義する。

pub const Foo = struct {
    a : u32 = 0,
    b : u32 = 0,
    c : u32 = 0,
};

次のようにsrc/foo.zenの中にテストを書いて動作を確認する。

const std = @import("std");
const ok = std.testing.ok;
test "struct" {
    const foo = Foo{.a = 10, .b = 20, .c = 30};
    ok(foo.a == 10);
    ok(foo.b == 20);
    ok(foo.c == 30);
}

{.a = 10}のような感じでメンバ名を指定して初期化できる。

変数にアクセスする場合はfoo.aの様に構造体のインスタンス.区切りでメンバを指定する。

実行結果

$ zen test ./src/foo.zen
All tests passed.

メソッド

Zenでは構造体の中に関数のメンバを定義することができる。

先程のFooの例であればfoo.func()ように呼び出せる。

ダメな例

慣れていない場合は次のようにしてしまいそうだが、これはエラーになる。

pub const Foo = struct {
    a : u32 = 0,
    b : u32 = 0,
    c : u32 = 0,
    fn func() u32 {
        return a + b + c;
    }
};

次のようなテストで確認する。

test "method" {
    const foo = Foo{.a = 10, .b = 20, .c = 30};
    ok(foo.func() == 60);
}

実行結果

t$ zen test ./src/foo.zen 
src/foo.zen:21:28: エラー[E09055]: 「0」個の引数が求められていますが、与えられた引数は「1」個です。
    const result = foo.func();
                           ~
src/foo.zen:5:5: メモ[E00001]: ここで宣言されています。
    fn func() u32 {
    ~
src/foo.zen:6:16: エラー[E07002]: 未宣言の識別子「a」が使用されています。
        return a + b + c;

エラーの原因は2つある。

エラー原因1

まずは1つ目。

src/foo.zen:21:28: エラー[E09055]: 「0」個の引数が求められていますが、与えられた引数は「1」個です。
    const result = foo.func();

これはメソッドがどのインスタンスに対して呼び出されたかを特定するために暗黙に第一引数に自分自身が渡される。 これがないと次のような場合に困る。

const a = Foo{}; 
const b = Foo{};
a.func(); //
b.func(); // どちらもおなじFoo型のfunc

funcメソッドの場合、fn func(self: Foo) u32のようなシグニチャが求められる。 つまり、次のようなことが暗黙的に行われている。

const a = Foo{}; 
const b = Foo{};
Foo.func(a); // 第一引数に自分を渡す。
Foo.func(b); // これでどちらのインスタンスか分かる。

a.func(); //
b.func(); // これらは上のメソッド呼び出しと同じ。

エラー原因2

次に2つ目。

src/foo.zen:6:16: エラー[E07002]: 未宣言の識別子「a」が使用されています。
        return a + b + c;

結局は1つ目と同じ原因で、どのインスタンスのメンバか判断できないためエラーになる。

こちらも、暗黙に渡される第一引数を使用して判別する。

pub const Foo = struct {
    a : u32 = 0,
    b : u32 = 0,
    c : u32 = 0,
    fn func(self: Foo) u32 {
        return self.a + self.b + self.c;
    }
};

次のようにテストする。

test "method" {
    const foo = Foo{.a = 10, .b = 20, .c = 30};
    ok(Foo.func(foo) == 60);
    ok(foo.func() == 60);
}

実行結果

$ zen test ./src/foo.zen 
All tests passed.

ミューテーション

いわゆるsetterの様に自分のメンバを書き換えるメソッドを定義してみる。

ダメな例

安直にすると次のように書いてしまいそうだが、これもエラーになる。

pub const Foo = struct {
    a : u32 = 0,
    fn setA(self: Foo, a: u32) void {
        self.a = a;
    }
};

テストを書いてみる。

test "mutation" {
    var foo = Foo{};
    foo.setA(10);
    ok(foo.a == 10);
}

実行結果

$ zen test ./src/foo.zen 
src/foo.zen:9:18: エラー[E02030]: 定数変数に代入できません。
        self.a = a;
                 ~

変数foovarとして定義されているので定数ではないはずだがこのエラーが発生する。

定数変数という表現が微妙だがイミュータブル(immutable)な変数ということ。

この原因はメソッドの第一引数であるselfがイミュータブルな変数として扱われるためである。

selfに限らず関数の引数として構造体を渡す場合、基本的にイミュータブルな変数として扱われる。

つまり、次のケースも同様にエラーとなる。

fn globalFunc(foo: Foo) void {
    foo.a = 10;
}

test "global mutation" {
    var foo = Foo{};
    globalFunc(foo);
}

実行結果

$ zen test ./src/foo.zen 
src/foo.zen:35:13: エラー[E02030]: 定数変数に代入できません。
    foo.a = 10;
            ~

このような場合、fn setA(self: *Foo, a: u32) voidの様に引数を構造体のポインタにする。

pub const Foo = struct {
    a : u32 = 0,
    fn setA(self: *Foo, a: u32) void {
        self.a = a;
    }
};

test "mutation" {
    var foo = Foo{};
    foo.setA(10);
    ok(foo.a == 10);
}

実行結果。

$ zen test ./src/foo.zen 
All tests passed.

クラス変数的なもの

その型に横断的に存在し、複数のインスタンスに共通な変数を定義することもできる。

pub const Foo = struct {
    a : u32 = 0,
    var bar: u32 = 100;
    const foobar = 50;
 };

test "class member" {
    ok(Foo.bar == 100);
    Foo.bar = 200;
    ok(Foo.bar == 200);
    ok(Foo.foobar == 50);
}

このような場合、インスタンス名ではアクセスできないことに注意が必要。

test "access class member via instance" {
    const foo = Foo{};
    ok(foo.bar == 100);
}

実行結果

$ zen test ./src/foo.zen 
src/foo.zen:48:11: エラー[E07004]: 「Foo」にはメンバー「bar」がありません。
    ok(foo.bar == 100);
          ~

クラス変数的なものはメンバではないということらしい。

クラス関数的なもの

関数も同様に定義できる。もちろんインスタンスが必要なメンバにはアクセスできない。

pub const Foo = struct {
    a : u32 = 0,
    const foobar = 50;

    fn getFoobar() usize {
        return foobar;
    }
};

test "class function" {
    ok(Foo.getFoobar() == 50);
}

こちらもインスタンス名ではアクセスできない。

test "class function via instance" {
    const foo = Foo{};
    ok(foo.getFoobar() == 50);
}

実行結果

$ zen test ./src/foo.zen 
src/foo.zen:67:21: エラー[E09055]: 「0」個の引数が求められていますが、与えられた引数は「1」個です。
    ok(foo.getFoobar() == 50);
                    ~
src/foo.zen:14:5: メモ[E00001]: ここで宣言されています。
    fn getFoobar() usize {
    ~

@This()

メソッドを定義する際に自分自身の型名が必要になるが、これを@This()という組み込み関数で抽象化することができる。

@This()は自分自身の型名を取得することができる。

pub const Foo = struct {
    a : u32 = 0,
    fn hoge(self: @This()) u32 {
        return self.a;
    }
};

test "hoge" {
    const foo = Foo{.a = 60};
    ok(foo.hoge() == 60);
}

テストの実行結果

$ zen test ./src/foo.zen 
All tests passed.

メソッドの定義で毎回@This()を呼び出すのはC言語のマクロの多用の様に可読性が低くなるので、次のようにして予め自身の型名を取得しておくと可読性が上がる。

pub const Foo = struct {
    a : u32 = 0,
    const Self = @This();
    fn fuga(self: Self) u32 {
        return self.a;
    }
};

test "fuga" {
    const foo = Foo{.a = 60};
    ok(foo.fuga() == 60);
}

テストの実行結果

$ zen test ./src/foo.zen 
All tests passed.

まとめ

構造体ではメソッドや型に横断的な変数、定数、関数が定義できる。

意外と写経したものを深く考えずに使ってしまいがちなので、いろいろ試してみた。

Zenではテストを書いて一つずつためしながら実装できるのがうれしい。