Zig やる
Zig をやろうという気になってきた。https://www.sigbus.info/compilerbook とかやったら面白いのではないかと思っている。
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>
という文法- 左を省略すると推論されるっぽい?
- Error Union 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
が使われる状況はバグであり、使用される前に上書きされるはず
-
let
か val
でよくない?
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 b
…a
がnull
のときb
を返し、そうでなければa
を返す- JSの
??
- JSの
-
a.?
…a orelse unreachable
-
unreachable
ってどういう効果だろう
-
-
a catch b
…a
はError Union。a
がエラーのときb
を返し、そうでなければa
をunwrapした値を返す -
a catch |err| b
…↑と同じ。err
はbのスコープで有効なエラー値 -
a.*
…ポインタa
を参照する -
&a
…a
のアドレス
ドキュメンテーションの順番とは前後することになるけど 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
でループをインライン化できる- コンパイル時にいろいろしたいときとか