Open14

ziglearnを読む Chapter1

r_mutaxr_mutax

低レイヤに使える新しいプログラミング言語と噂のzigを勉強してみようと思ったので、ziglearn.orgを読んでいく。

あとから読み返しやすいようにしたいので、ほとんど日本語訳みたいになると思います。

r_mutaxr_mutax

ダウンロード

Zigのダウンロードページからダウンロードしました。
とりあえずmasterのWindows向けbinaryを入れてみます。
zipをダウンロードしたら、展開したら使えます。

そのままだとPATHが通っていないので、PATHを通します。
こちらのページにユーザレベル / システムレベルでのパスの通し方が書いてあります。
Windowsの場合はPowershellのスクリプトを利用します。

無事導入が終わったら、zig versionのコマンドを実行します。
versionが表示されたら無事zigが使えるようになっています。

r_mutaxr_mutax

代入

変数の宣言は (const | var) identifier[: type] = valueでできます。

  • const をつけると 定数 で宣言でき、再代入できません。
  • var をつけると 変数 で宣言でき、変更可能です。
  • :type は型注釈です。初期化の値で推測できるときは省略可能です。
  • 未使用のローカル変数があるとコンパイルエラーになります。無駄な変数が残らなくてよいですね。_に代入することで、いらない変数を捨てる?ことができます。(後でちゃんと見る)
  • 変数や定数は必ず値が必要です。Cと違って、不定値が存在しないのでよいですね! ただし、型注釈がある場合は、任意の型になるundefinedを与える事ができます。
const expect = @import("std").testing.expect;

test "zig test" {
    const c: i32 = 5;
    // c = 5;       //  定数への再代入でコンパイルエラーになる
    _ = c;          //  _ に代入することで、未使用のデータを捨てられる?

    var v: u32 = 5000;
    _ = v;

    // 型注釈の省略
    const ic = @as(i32, 5);
    _ = ic;

    // 変数は値を必ず持たないといけない。
    // `undefined`にしてもよい。
    const a: i32 = undefined;
    const b: u32 = undefined;
    _ = a;
    _ = b;
}
r_mutaxr_mutax

配列

  • 配列は[N]T で示せます。N は配列の要素数、T は配列の子要素の型を示します。
  • 初期化式で配列のサイズが推測できる場合は、N_に置き換えて要素数を省略でる。
  • 配列の要素数はlenでアクセスできる。
const expect = @import("std").testing.expect;

test "zig test" {
    const a = [5]u8{ 1, 2, 3, 4, 5};
    const b = [_]u8{ 'h', 'e', 'l', 'l', 'o'};
    _ = b;
    
    // array.len で配列の要素数を取れる
    try expect(a.len == 5);
}
r_mutaxr_mutax

そういえばインラインテストもあるみたい。
expectは標準の関数で、falseが渡されるとテストを失敗させる。

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

test "zig test" {
    try expect(false);
}
r_mutaxr_mutax

if

Zigのif文はboolしか受け取れない。モダンな言語らしく? if式も使えるみたい。

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

test "if statement" {
    const a = true;
    var x: u16 = 0;

    // if文にはbool値しかわたせない。
    // if (!x) {
    //     try expect(false);
    // }

    if(a) {
        x = 10;
    } else {
        x = 20;
    }

    try expect(x == 10);

    // if式も使える。
    var y: u16 = if(a) 1 else 2;
    try expect(y == 1);
}
r_mutaxr_mutax

While文

while (cond) : (expr)みたいに書く。coond には継続条件を、exprにはループ更新時に動かす式をかける。C/C++でいうfor文みたいになってる?
breakとかcontinueとかはよくある感じなので大丈夫ですね。

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

test "while" {
    var i: u8 = 2;
    while (i < 10){
        i *= 2;
    }
    try expect(i == 16);

    i = 1;
    var sum: u8 = 0;
    // 継続式もかける。
    while (i <= 10) : (i += 1){
        sum += i;
    }
    try expect(sum == 55);

    i = 1;
    sum = 0;
    while(i < 10) : (i += 1){
        // breakも使える
        if(i == 3) break;
        sum += i;
    }
    try expect(sum == 3);

    i = 1;
    sum = 0;
    while(i < 10) : (i += 1){
        // continueも使える
        if (i % 2 == 0) continue;
        sum += i;
    }
    try expect(sum == 25);
}

r_mutaxr_mutax

For

for ( array ) | data, index | { } みたいに書く。range based forみたいな使い方?

  • | data, index | の部分はforで回しているところをキャプチャしている。各要素とindexを変数に取れる。
  • キャプチャは必ず取らなくてもいい。indexは省略できる。dataは省略できなくて、_を書いて捨てる必要がある。
const expect = @import("std").testing.expect;

test "For statement" {
   const string = [_]u8 {'a', 'b', 'c'};


    // forは配列をイテレートする。range based forみたいなもの?
    // もう少し読み進めると違うタイプが紹介される
    // | data, index | -> dataが配列の要素、indexに添字をキャプチャできる
    for (string) |character, index| {
        _ = character;
        _ = index;
    }

    // indexは省略できる
    for (string) |character| {
        _ = character;
    }

    // dataの方は省略できない。省略したいなら、'_'で捨てる必要がある。
    for(string) |_, index | {
        _ = index;
    }

    // dataもindexも両方捨てることもできる
    for(string) |_| {}
}
r_mutaxr_mutax

Functions

関数はfn funcname(ident: type) type {}で書く。
モダンな言語らしく戻り値の型は後ろに置く。
戻り値がある関数は、その戻り値を破棄できない。強制的に[[nodiscurd]]みたいになっている。
Zigでは関数名はCamelCaseでつけるのが慣例みたい?

Unlike variables which are snake_case, functions are camelCase.
ziglearn.org - Functions

ziglearn.orgの説明文には、 再帰関数はコンパイラが使用するスタックサイズを把握できない ので安全ではない、との記載があった。
今後、安全な再帰関数の作り方は取り上げるとも。

When recursion happens, the compiler is no longer able to work out the maximum stack size. This may result in unsafe behaviour - a stack overflow. Details on how to achieve safe recursion will be covered in future.
ziglearn.org - Functions

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

// 関数はcamelCaseで書く慣習らしい
fn add(x: u32, y: u32) u32 {
    // 関数の引数はimmutableなので変更するとコンパイルエラー
    // x = 5;
    return x + y;
}

test "function" {
    const y = add(3, 5);

    // 関数の戻り値は破棄できない。
    // 破棄したい場合は'_' に代入する。
    _ = add(3, 5);

    try expect(@TypeOf(y) == u32);
    try expect(y == 8);
}
r_mutaxr_mutax

defer

deferをつけると、ブロックを抜けるまで文の評価が保留される。
golangはあまり詳しくないけど、同じものがあったかな?
(昔A Tour of Goをちょっと眺めたときに見かけた)

deferが積み上げられたときは、逆順に評価される。

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

test "defer" {
    var x: f32 = 5;
    {

        // 同一のブロック内で複数のdeferがあった場合、逆順に処理をする。
        defer x += 2;
        defer x /= 2;
        
        // deferをつけた式は、ブロックを抜けるまで評価されない
        try expect(x == 5);
    }

    // ここでは(5 + 2) / 2 = 3.5 ではなく、
    // (5 / 2) + 2 = 4.5として評価される
    try expect(x == 4.5);
}
r_mutaxr_mutax

Error

Zigには例外がなく、エラーだけ。エラー型はenumみたいに宣言して使う。ちなみにenumもあるみたい。

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

// エラーは宣言してconst変数に入れて使う(名前を付ける)
const FileOpenError = error {
    AccessDenied,
    OutOfMemory,
    FileNotFound,
};

// エラーセットはスーパーセットにキャストできる。
// ↓の例でいうと、FileOpenError.OutOfMemoryを抽出した
// AllocationErrorをerrに代入する際にキャストされている。
const AllocationError = error{OutOfMemory};

test "coerce error from a subset to a superset" {
    const err: FileOpenError = AllocationError.OutOfMemory;
    try expect(err == FileOpenError.OutOfMemory);
    try expect(@TypeOf(err) == FileOpenError);
}
r_mutaxr_mutax

Error Union

ZigにはErrorと通常の型を連結したErrorUnion型がサポートされている。
Error と! を連結することで、ErrorUnion型になり、連結したエラー、通常の型のどちらも代入できるようになる。
std::optional<>みたいに使える? Cとかだとint getData(int* data)みたいにして戻り値でエラー(正常/異常)を受け取っていたから便利ですね。

A catch B構文は、AがエラーならBに、そうでないならAとして評価される。

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

const TestError = error {
    Error_1,
};

test "error union" {
    // ! で連結することでError Unionにできる。
    const not_err : TestError ! u16 = 16;
    const err : TestError ! u16 = TestError.Error_1;

    // A catch B 構文は、AがエラーならBとして評価される。
    // AがエラーじゃないならAとして評価される。
    const not_catch_err = not_err catch 0;
    const catch_err = err catch 10;

    try expect(not_catch_err == 16);
    try expect(catch_err == 10);
}

Error Unionを返す関数に対して、catch |err| とすることでエラーをキャプチャできる。
tryx catch |err| return err| の短縮形で、エラーとなっちゃいけなところでエラーを補足する。
他の言語でよくあるtry-catch構文とは違って、Zigのtrycatchは関係ないらしい。

fn returnErrorUnion() error {Ooops} ! void {
    return error.Ooops;
}

test "return error" {
    // Error Unionを返す関数で |err| でエラーの値を受け取れる。
    returnErrorUnion() catch |err| {

        // try は x catch |err| return err の短縮形
        // tryとcatchはよくあるtry-catch構文とは違う。
        try expect(err == error.Ooops);
    };
}
r_mutaxr_mutax

Errdefer

通常のdeferと違いerrdeferもある。
errdeferで修飾された式は、ブロックがエラーをリターンする場合に評価される。
エラーのときだけメモリを開放するとか、色々使えそう。

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

var data: u32 = 0;

fn getData(x: bool) error{Oops} ! u32 {
    // 関数からエラーで帰るときに評価される
    errdefer data = 1;

    // 引数がfalseならエラーを返す
    if(x){
        return 5;
    } else {
        return error.Oops;
    }
}

test "errdefer" {
    // 正常終了なのでerrdeferは評価されず、
    // data == 0
    const nmlptn = getData(true) catch 0;
    try expect(data == 0);
    try expect(nmlptn == 5);

    // エラーを返したので、errdeferが評価され、
    // data == 1
    const errptn = getData(false) catch 0;
    try expect(data == 1);
    try expect(errptn == 0);
}
r_mutaxr_mutax

Errorセットは、合成することができる。
属性ごとにエラー用意しといて、包括的なエラーを作って~とかできそう。

const FileError = error { NotFound, WriteError };
const MemoryError = error { AccessDenied, CannotAllocate };
const AllError = FileError || MemoryError;