Open16

Zig やる

おーみーおーみー

https://ziglang.org/learn/overview/ を読むとどういう言語なのかだいたいわかる。勘違いしていたのだがZigにはガベージコレクションがない。Rustと比較して言語機能が単純なので読みやすいとか、標準ライブラリでヒープアロケータ (呼び方合ってるか?) を使うものは全部アロケータを引数に取るので差し替えるのが容易、そのへんもろもろによりリッチじゃない環境でも動かしやすいという利点があるらしい。Rustをbetter C++としてZigはbetter Cみたいな見方するのがよさそう。

https://ziglang.org/documentation/0.9.1/ を読んでいく。0.10でないのはnixpkgsがまだ上がっていないから。

Language serverとして zls というのがもうあるのが高得点。

おーみーおーみー
  • 拡張子は .zig
  • ソースコードはUTF-8 encoded
  • zig build-exe hello.zig で実行ファイルをコンパイルできる
const std = @import("std");

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();
    try stdout.print("Hello, {s}!\n", .{"world"});
}
  • @import は組み込み関数 (builtin function)
  • const で定数 (なのか?)
  • 実行ファイルを作るには pub fn main 関数が必要
    • そうでないように実行ファイルを作ることもできはするっぽい
    • もちろんライブラリにも必要ない
  • !void は Error Union Type
  • Error Union TypeはError Set Typeもしくは他のデータ型
    • Error Union Typeは <error set type> ! <any data type> という文法
      • 左を省略すると推論されるっぽい?

インポートが関数になっているのはなかなか珍しいんじゃなかろうか。エラーがunionになってるっぽいのはちょっと良さがある。

  • "Hello, world!\n" という文字列はコンパイル時に組み立てられる
  • .{"world"} は匿名構造体リテラル (anonymous struct literal)
  • try <expr> 式は <expr> がエラーになった場合そのエラーで実行中の関数 (今なら main) からreturnする

エラー対処はわりとモダンに感じる。

おーみーおーみー

Writerの print はファイルなどに使うことを考えてエラーを返すが、std.debug.print を使うとエラーを無視することもできる。

const print = @import("std").debug.print;

pub fn main() void {
    print("Hello, world!\n", .{});
}

メソッドって書いてないのやや気になる。

おーみーおーみー

コメントは // で、マルチラインコメント /**/ はない。これによって文脈情報を持つことなく各行をトークナイズできるという。

ドキュメンテーションコメントは ///。書ける場所が限定されており、違反するとコンパイルエラーになる。

パッケージのドキュメンテーションなどはトップレベルドキュメンテーションコメント //! に書ける。

おーみーおーみー
  • 整数型は i8 u32 みたいな
  • Cの void に相当するのは anyopaque
    • type-reased pointerってなんですか
  • void は unit 型に相当しそう
    • 0 bit typeと説明される
  • noreturn 型は break continue return unreachable while (true) {} の型
    • これらって式なの?
    • TSの never とかHaskellの Void とかRustの ! とかそう
  • 型の型 type がある
    • ジェネリクスとかこれで実現してるらしい
おーみーおーみー
  • 文字列リテラルはバイト列への定数ポインタ (これであってるのか?)
  • 文字列リテラルの型には長さが含まれており、また文字列リテラルはヌル終端
    • 「can be coerced to both Slices and Null-Terminated Pointers」がよくわかってないぜ
  • Unicode code point literalsというものがある
    • Unicodeのコードポイントが入っている
    • comptime_int
    • characterとは呼ばない
      • Unicodeにcharacterの厳密な定義というのがない

Unicode code point literals、あまりに正しい

おーみーおーみー
  • 識別子に値を割り当てるには const x = 1123; のようにする
  • 再代入可能な変数は var
  • 変数は初期化する必要がある
  • 値がない状態の変数は undefined を代入する
    • undefined が使われる状況はバグであり、使用される前に上書きされるはず

letval でよくない?

おーみーおーみー
const std = @import("std");

test "expect addOne adds one to 41" {
    try std.testing.expect(addOne(41) == 42);
}

fn addOne(number: i32) i32 {
    return number + 1;
}

テストを書いたファイルは zig test hogehoge.zig で実行できる。

おーみーおーみー
  • 変数のシャドーイングは外側のスコープに対するものも含め不可
  • 識別子の法則は普通
    • @"hoge hoge" で任意文字列を取れる
  • container level variables (トップレベル、struct union enum 内で宣言される変数のこと)
    • 静的ライフタイムを持つ (プログラムの全期間で生存する)
    • 順序がない
    • 初期化はコンパイル時
    • const なら値もコンパイル時に決定
    • var なら値は実行時に決定

初期化がコンパイル時本当か?std.io.getStdOut().writer() はさすがにランタイムでは?読み間違い?マジでコンパイル時にできる?

おーみーおーみー

静的ライフタイムを持つローカル変数とかいうものも作れてしまうらしい。マジ?

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

test "static local variable" {
    try expect(static_local() == 1235);
    try expect(static_local() == 1236);
}

test "local variable" {
    try expect(local() == 1235);
    try expect(local() == 1235);
}

fn static_local() i32 {
    const St = struct {
        var x: i32 = 1234;
    };
    St.x += 1;

    return St.x;
}

fn local() i32 {
    var x: i32 = 1234;
    x += 1;

    return x;
}

shared mutable state的にこれ大丈夫なんだろうか

おーみーおーみー

大丈夫じゃないかもだから threadlocal なんてものがあるのかもしれないな。

const std = @import("std");
const assert = std.debug.assert;
const spawn = std.Thread.spawn;

threadlocal var x: i32 = 1234;

test "thread local storage" {
    const thread1 = try spawn(.{}, threadLocal, .{});
    const thread2 = try spawn(.{}, threadLocal, .{});
    threadLocal();

    thread1.join();
    thread2.join();
}

fn threadLocal() void {
    assert(x == 1234);
    x += 1;
    assert(x == 1235);
}
おーみーおーみー
  • localな const の初期化値がcomptime-knownの場合変数もcomptime-knownになる
  • comptime var とすると初期化値がcomptime-knownであることを要求し、さらにその変数の読み書きは実行時ではなくコンパイル時に行われる
おーみーおーみー
  • 演算子オーバーロードなし
  • 算術演算子はまあ省略
  • a orelse banull のとき b を返し、そうでなければ a を返す
    • JSの ??
  • a.?a orelse unreachable
    • unreachable ってどういう効果だろう
  • a catch ba はError Union。a がエラーのとき b を返し、そうでなければ a をunwrapした値を返す
  • a catch |err| b…↑と同じ。err はbのスコープで有効なエラー値
  • a.*…ポインタ a を参照する
  • &aa のアドレス
おーみーおーみー

ドキュメンテーションの順番とは前後することになるけど while 見てみる。

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

test "while" {
    var i: usize = 0;
    while (i < 10) {
        i += 1;
    }

    try expect(i == 10);
}

test "while break" {
    var i: usize = 0;
    while (true) {
        if (i == 10) break;
        i += 1;
    }

    try expect(i == 10);
}

test "while continue" {
    var i: usize = 0;
    while (true) {
        i += 1;
        if (i < 10) continue;
        break;
    }

    try expect(i == 10);
}

test "while with continue expression" {
    var i: usize = 0;
    while (i < 10) : (i += 1) {}

    try expect(i == 10);
}

test "while is expression" {
    try expect(rangeHasNumber(0, 10, 5));
    try expect(!rangeHasNumber(0, 10, 15));
}

fn rangeHasNumber(begin: usize, end: usize, number: usize) bool {
    var i = begin;
    return while (i < end) : (i += 1) {
        if (i == number) break true;
    } else false;
}

Cでいう for(; i<10; i++)while で達成されているのが面白い。そして式だし。

おーみーおーみー

Just like if expressions,

その if、この後に解説があるんですよ

  • while (optional()) |value| {} でオプショナルな値を使ってループできる
  • while (error()) |value| {} else |err| {} でエラーを使ってループできる
  • inline while でループをインライン化できる
    • コンパイル時にいろいろしたいときとか