これは何?
Zig を学ぼうと 公式文書 (0.91時点) を読んでいるんだけど、読みながら思ったことを記していく。
続編は Zig の文書読んで所感を記す #2 へ。
その前に
Zig への言及が最近多いなぁ、でもシンプルな言語だって言うしまあどうでもいいかなぁ、ぐらいの気持ちでいたんだけど、ZigはCMakeの代替となるか を読んで、俄然興味が湧いてきて、じゃあ読んでみるか、と思った。
数値
i32
とか u16
のような名前で型が提供されている。
整数は 128bit まである。そればかりか、 3bit とか 53bit のような中途半端な幅の整数も使える模様。面白い。
さらに。何に使うのかわかってないけど、 i0
u0
のようなゼロビットの整数もある。
ちなみに0ビット整数には 0
が代入できる。
u1
は、 0
または 1
。 i1
は、 0
または -1
が代入可能。
浮動小数点数は、16bit と 128bit もある。すばらしい。そればかりか、80bit もある。8087 の呪いだね。
あと。0で始まる整数が 10進数になる。すばらしい。8進数は 0o123
のように書く。
2022年8月23日加筆
当初「go のリテラルと同じく、リテラル自体は型を持たない。」と書いていたが、これは不正確。
zig のリテラルは、 comptime_int
という型を持つ。その型は、go の untyped な整数定数とよく似た振る舞いをするけど、 go が untyped な整数定数を他の情報なしに int だと推論(?)する
fromUntyped := 1 // 右辺は untyped だが、左辺は推論(?)の結果、int になる。
のと異なり、zig は comptime_int
を isize
や c_int
に推論したりはしない。
const fromComptimeInt1 = 1; // 右辺も左辺も comptime_int
var fromComptimeInt2 = 1; // comptime_int を var になれる型に推論できないのでエラー
文字列
"abc"
のようなものは、文字列。
文字列は null-terminator がついているバイトの配列へのポインタ。C言語と似ている。文字コードは utf-8 だと決めているわけじゃないけどまあ普通そうだよね、ぐらいの感じっぽい。
文字列用の特別な型は無い模様。可変長の文字列は絶対必要なので、普通こうやるよ、みたいなのをライブラリの形で提供していると信じているけど、まだ存在は確認していない。
文字型
'a'
のようなものは、文字ではなく整数。Unicode のコードポイントの値になる。
文字列は utf-8 のバイト列で 'a'
は Unicode のコードポイントの値なので、互いの関係はやや複雑になる。
変数
const
const y = x + 2;
のような const は、 C++ の const と似ていて、再代入できないという意味の模様。go の const とはぜんぜん違う。
型と初期化
const y: i32 = 123;
のように、型推論ができない場合はコロンに続けて型を書く。
初期化は必須で、初期化がどうしても不要の場合は
var x: i32 = undefined;
のようにする。いい考えだと思う。
コンパイル時変数
コンパイル時に値がわかっているものを、C++ では constexpr
で書くけど、Zig では comptime
と書く。
驚くべきことに、comptime
であっても値を変えることができる。
comptime var x: i32 = 1;
try stdout.print("x is {}\n", .{x});
x += 10;
try stdout.print("x is {}\n", .{x});
x *= 123;
try stdout.print("x is {}\n", .{x});
上記のコードの *=
なんかはコンパイル時に終えるということなんだと思う。
static 変数
C/C++ にあって、go にない、関数内 static 変数が Zig にはある。
C++ にあって、go にない クラス内 static 変数も Zig にはある。
演算と演算子
オーバーフロー
オーバーフロー時の振る舞いによって演算子が違う。
const std = @import("std");
fn satuMul(x: u8, y: u8) u8 {
return x *| y;
}
fn wrapMul(x: u8, y: u8) u8 {
return x *% y;
}
fn simpleMul(x: u8, y: u8) u8 {
return x * y;
}
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("satuMul(34,56) is {}\n", .{satuMul(34, 56)});
try stdout.print("wrapMul(34,56) is {}\n", .{wrapMul(34, 56)});
try stdout.print("simpleMul(34,56) is {}\n", .{simpleMul(34, 56)}); // panic: integer overflow
}
便利。こういうのがいいとわりと昔から思っていた。
通常の演算子だとオーバーフロー時に panic になるんだけど、この panic をキャッチというかリカバーというかできるかどうかはまだわかっていない。
条件演算子
C の cond ? if_true : if_false
に相当する演算子は、演算子のリストにはなかった。
なかったけど、if
が値を返すので if (cond) if_true else if_false
と書ける。
悪くない。
配列の加算と乗算
a
, b
が配列で、 n
が整数のとき、
a ++ b
で配列の連接、 a ** n
で配列の繰り返しを作ることができる。
面白いのは、これができるのは両辺がコンパイル時にわかっている値の場合に限るという点。
見覚えのない演算子
C の(p が ポインタとして) p ? *p : fallback
に相当する演算子 p orelse fallback
というのがある。
あと、 a catch b
というのもある。これは a
が「エラーまたは値」のときに使うもので、a
がエラーなら b
、a
がエラーじゃない値ならその値、というもの。面白い。
配列とスライス
配列の文法はこんな感じ。
const a0 = [2]i32{ 1111, 2222 }; // 個数指定
const a1 = [_]i32{ 11, 22, 33, 44 }; // 個数指定省略は下線
C/C++/go などと違って、数があっていないとエラー。自動的にゼロで埋めたりはしない。
それとは別に Sentinel-Terminated Arrays という物がある。日本語訳は番兵終端配列かな。
const a2 = [_:0xff]u8{ 99, 88, 77 };
try stdout.print("{any} {any}\n", .{ a2, a2[3] }); //=> { 99, 88, 77 } 255
try stdout.print("{any}\n", .{a2.len}); //=> 3
try stdout.print("{any}\n", .{a2 ++ a2}); //=> { 99, 88, 77, 99, 88, 77 }
C の文字列のように、文字数+1 のメモリ領域を使い、末尾に終端記号が入る。
++
でつなげると、終端記号以外をつないで末尾に終端をつける。すばらしい。
.len
が返す値は利用メモリ観点ではなく要素数観点での長さなので、要注意かも。
配列の一部を取り出すとスライスになる。
var a = [_]i32{ 00, 11, 22, 33, 44, 55, 66, 77 };
var s = a[2..5];
try stdout.print("s is {} {} {}\n", .{ s[0], s[1], s[2] }); //=> s is 22 33 44
ruby の range を作る演算子と同じ綴の ..
だけど、ruby と
a=[00, 11, 22, 33, 44, 55, 66, 77]
p a[2..5] #=> [22, 33, 44, 55]
異なり、終端は含まない。
私としては、下表の三種類が全部あるといいなと思ってるんだけど、今の所 「先頭」「末尾の次」方式しか見当たらない。
意味 | ruby | groovy | Zig |
---|---|---|---|
「先頭」「末尾の次」 | a[b...e] |
a[b..<e] |
a[b..e] |
「先頭」「末尾」 | a[b..e] |
a[b..e] |
n/a |
「先頭」「長さ」 | a[b,len] |
a[b,len] |
n/a |
まあ、python にも go にもないのでそうかなとは思うけれど。
構造体
下表のとおり、Zig は Java 等と同等のことができる感じ。
構造体の要素 | go | Java・C++ | Zig |
---|---|---|---|
普通のメンバ変数 | ✅ | ✅ | ✅ |
クラス変数 | - | ✅ | ✅ |
コンパイル時定数 | - | ✅ | ✅ |
普通のメソッド | ✅ | ✅ | ✅ |
クラスメソッド | - | ✅ | ✅ |
別の構造体 | - | ✅ | ✅ |
順に見ていこう。
普通のメンバ変数
const Point = struct {
x: f32 = 12, // カンマで終える
y: f32 = 34,
};
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
var pt = Point{ .y = 56 };
try stdout.print("{any}\n", .{pt});
}
上記の通り、必要なら初期値も書ける。初期値がない場合、初期化時に値の省略ができない。
初期化時は .y=56
のように、メンバ名の前にピリオドを付ける。C99 っぽい。
あんまりわかってないけど
var pt: Point = .{ .y = 56 };
のように書いてもいいっぽい。
クラス変数とコンパイル時定数
const Foo = struct {
var x: f32 = 12; // セミコロンで終える
const y: f32 = 34;
};
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("{any} {any}\n", .{ Foo.x, Foo.y });
}
var
をつけるとクラス変数。
comptime
とは書けないけど、たぶん const
とつけるとコンパイル時定数になる。
普通のメソッドとクラスメソッド
const Point = struct {
x: f32,
y: f32,
pub fn len2(p: Point) f32 {
return p.x * p.x + p.y * p.y;
}
};
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
const pt = Point{ .x = 3, .y = 4 };
const a = pt.len2();
const b = Point.len2(pt);
try stdout.print("{any} {any}\n", .{ a, b });
}
普通のメソッドとクラスメソッドは区別されない。
第一引数を関数名の前に書くこともできるよ、という対応。Python っぽい。
別の構造体
構造体を単なる名前空間として使ってもいいということだと思う。
const std = @import("std");
const Foo = struct {
const Bar = struct {
x: i32,
};
const Baz = struct {
x: Bar,
};
};
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
const bar = Foo.Bar{ .x = 123 };
const baz = Foo.Baz{ .x = Foo.Bar{ .x = 234 } };
try stdout.print("bar={} baz={}\n", .{ bar, baz });
}
enum
C++ の enum class
っぽい。
C++ と違って、中に型・定数・メソッドなんかを書けるし、C++ と違って print
で名前を見ることができる。
const std = @import("std");
const Hoge = enum {
zero,
one,
two,
three,
const Foo = struct {};
};
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
const x = Hoge.two;
try stdout.print("x is {}\n", .{x}); //=> x is Hoge.two
}
Non-exhaustive enum
日本語にすると、非網羅的 enum かな。
普通の enum は、名前がつけられてない値から enum に変換するとエラー。
Non-exhaustive だと、名前のない値になることができる。
const Exhaustive = enum(u32) { one = 1 };
const NonExhaustive = enum(u32) { one = 1, _ };
const e1 = @intToEnum(Exhaustive, 1);
const e2 = @intToEnum(Exhaustive, 2); // enum 'Exhaustive' has no tag matching integer value 2
const n1 = @intToEnum(NonExhaustive, 1);
const n2 = @intToEnum(NonExhaustive, 2); // okay.
union
C言語で様々なトラブルの原因になってきた union だけど、Zig の union は安全っぽい。
const uni = union {
i: i32,
f: f64,
};
var u = uni{ .f = 3 };
u.f = 4;
u.i = 5; // panic: access of inactive union field
i: i32
の方に値を入れたくなったら、下記のように
const uni = union {
i: i32,
f: f64,
};
var u = uni{ .f = 3 };
u.f = 4;
u = uni{ .i = 5 }; // it is okay.
union そのもので上書きすればよい。
しかしこのままだと、何が入っているのかを知る合理的な方法がない。
何が入っているか知りたい場合は Tagged union を使う。
const std = @import("std");
const uniTag = enum { i, f };
const uni = union(uniTag) {
i: i32,
f: f64,
};
const stdout = std.io.getStdOut().writer();
fn show(u: uni) !void {
switch (u) {
uniTag.i => |v| try stdout.print("u is {any}\n", .{v}),
uniTag.f => |v| try stdout.print("u is {any}\n", .{v}),
}
}
pub fn main() !void {
try show(uni{ .f = 3.14 }); // u is 3.14e+00
try show(uni{ .i = 722 }); // u is 722
}
タグの定義と union の定義をバラバラにしたくない場合は下記のように
const uni = union(enum) {
i: i32,
f: f64,
};
fn show(u: uni) !void {
switch (u) {
uni.i => |v| try stdout.print("u is {any}\n", .{v}),
uni.f => |v| try stdout.print("u is {any}\n", .{v}),
}
}
すればよい。
ブロック
ブロックは値を返すことができる。すばらしい。
fn hoge(a: f64) !void {
const b = foo: { // b = a*a+1/(a*a);
const x = a * a;
break :foo x + 1 / x;
};
try stdout.print("a={}\n", .{b});
}
値を返すためには、ラベルを付けなければならない。
ブロック内にブロックがある場合にどっちだかわからなくなるからだと思う。
ruby と違って break が必要っぽい。
switch
わりと見慣れない感じの文法になっている。
そして、union や enum と絡まっていろいろある。
まずは単なる分岐
単なる分岐としてはこんな感じ。
const s = switch (a) {
1...3 => "1...3",
6, 7 => "6, 7",
else => "else",
};
1...3
となっていて、ピリオド 3つ。スライスのときはピリオド2つだった。
そしてスライスと異なり、両端含む。
両端含むかどうかは ruby と逆。間違えそう。
私としては、たとえば a..<b
だと末尾含まずで、 a..=b
だと末尾含む、とかがいいなぁと思う。
=>
の左側にマッチ条件。右側に式。
マッチ条件は、コンマ区切りまたは ...
で範囲指定。ピリオド2個の ..
はエラー。なんでだろ。
あと、条件に重複があるとエラー。
enum との絡み
Exhaustive な(つまり、_
が無い) enum で switch
する場合、網羅していないとコンパイルエラーになる。警告ではなくエラー。
else
を書いたら許してくれるので問題ない。
const Hoge = enum { foo, bar, baz, qux };
const s = switch (a) {
.foo => "FOO",
.bar, .baz => "BXX",
else => "otherwise", // else が無いとエラー
};
union との絡み
tag つき union の値を取り出すのに使える。
const Hoge = union(enum) {
i: i32,
u: u32,
f: f64,
fn show(self: Hoge) !void {
switch (self) {
.i => |v| try stdout.print("i: {}\n", .{v}),
.u => |v| try stdout.print("u: {}\n", .{v}),
.f => |v| try stdout.print("f: {}\n", .{v}),
}
}
};
こちらも網羅的でないとエラーになる。
.i, .f=> |v| 略
のように書けるかと思ったら「capture group with incompatible types」というエラーだった。残念。
while
else など
なんと。while
には else をつけることができる。私が今思いつく範囲では、 python と zig だけ。
しかも while
も値を返す。便利。
まあ下記の例ではあんまり便利じゃないけど。
fn whileVal(n: u32) u32 { var i: u32 = 2;
return while (i < 100) {
if (n % i == 0) {
break i;
}
i += 1;
} else 0;
}
pub fn main() !void {
const x = whileVal(35); // x=5
const y = whileVal(101); // y=0
// 以下略
}
あと。コードは載せないけどラベルをつけることができて、多重 whlie の break / continue で困ることがない。
optional 型との絡み
while
の中に optional を返す式(?)を入れると、null
になるまで繰り返すという意味になる。
それだけならまあそりゃそうだってことだけど、その optional の値の中身を |v|
のような感じで受けることができる。
const foo = struct {
i: u32,
e: u32,
fn next(self: *foo) ?u32 {
self.i += 1;
return if (self.e <= self.i) null else self.i;
}
};
pub fn main() !void {
var f = foo{ .i = 10, .e = 20 };
var sum: u32 = 0;
while (f.next()) |v| {
sum += v;
}
// 以下略
}
エラーと while
while
の条件節にエラーかもしれない値を返す式を書いて、while
の末尾に else |err|
のようにすると「エラーが出るまでループ」になる。
const FooError = error{foobar};
const foo = struct {
i: u32,
e: u32,
fn next(self: *foo) !u32 {
defer self.i += 1;
return if (self.e <= self.i) FooError.foobar else self.i;
}
};
pub fn main() !void {
var f = foo{ .i = 9, .e = 22 };
var sum: u32 = 0;
while (f.next()) |v| {
sum += v;
} else |err| {
try doSomething(err);
}
// 以下略
}
inline while
inline while
と書くと、ループアンロールされるらしい。
そうすると、可能であればコンパイル時評価されるらしい。
まだ気持ちがわからないので、脳の片隅に留め置く程度で先に進む。
for
0 から初めて N-1 になるまでの N 回の繰り返し、みたいなのは想定されていないように見える。
配列やスライスの要素をなめるのに使う。
const c = [_]u32{ 11, 22, 33, 44 };
var sum: u32 = 0;
for (c) |v| {
sum += v;
}
else が使えたり値を返せたりするところは同じ。
C 言語の for( int i=0 ; i<N ; ++i ){略}
、ruby の N.times{ |i| 略 }
に相当するものを普通 while
で書くのかなと今のところ思っているんだけど、どうなんだろ。
if
while
と似ている。
- 単に条件を書く
- optional を条件節に入れて
|v|
で受ける - エラーかもしれない式を条件節に入れて
|v|
とelse |err|
で値とエラーを受ける
の三通りの使い方がある。
defer と errdefer
go と同じく defer
がある。
go と異なり、スコープを抜けたら実行する。どっちが便利だかわからないけど、両方使うことになる私は混乱すると思う。
go と異なり、errdefer
なるものがある。これは、エラー時にのみ実行される defer。
- メモリ確保してなんかする。
- 成功したらそのメモリ活用したオブジェクトを返す。
- 失敗したら、メモリ解放してエラーを返す。
というありがちなストーリーで「失敗したときだけメモリ解放」を実現するためにあるんだと思う。
unreachable
ビルドモードによっては、という注釈付きで、panic になる。
というか。
ビルドモードなるものがあるんだね。
noreturn
呼んだら帰ってこない関数の返礼値型、かな。
関数定義
こんな感じで定義する。
fn hoge(a: u8, b: u8) u8 {
return a + b / 2;
}
同じ型が続いても省略できない。そこは go とは違う。
修飾子(?)をつけることができるんだけど、場所が先頭とは限らない。
修飾方法 | 意味(たぶん) |
---|---|
fn の前に export
|
C 言語互換でリンカから見えるようにする |
fn の前に pub
|
他の zig ファイルから @import できるようにする |
引数リストと返戻値の間に callconv(略)
|
色々。下表。 |
callconv(略)
シリーズは
略の部分 | 意味(たぶん) |
---|---|
.Inline |
強制インライン。インライン展開できなければコンパイルエラー。 |
WINAPI |
Windows の WINAPI になるんだと思う。 |
.Naked |
関数呼び出しプロローグとかがなくなる。アセンブラ書く場合以外は使わない。 |
他にも .C
とか .Async
とか色々あるっぽいけどよくわからない。
C・Java・go のいずれとも異なり、引数は定数。書き換えられない。
書き換えられないことにより、値渡しか参照渡しなのかはユーザーの知るところではないというやや思いがけない対応が可能となる。
気分的には常に C++ の const 参照、みたいな感じなのかな。
クロージャ
クロージャというか、ラムダとういか、関数リテラルというか、そういうものをシンプルに定義する方法がみあたらない。
キャプチャ無しでよければ、構造体の中に関数が書けるのでそれが使えるけど、構造体定義してからその関数を参照する、なのでちょっとだるい。
ブロックが値を返すんだからそれを使え、ということなんだろうと思う。
ローカル変数をキャプチャしたクロージャをリターンする、とかは難しいのかな。型演算をうまくつかったらできるのかな。
型を返す関数・型を受け取る関数
zig にとって型は値の一種なので、型を返す関数や型を引数とする関数を普通に書くことができる。
const foo = struct {
a: u32 = 123,
b: u32,
};
const bar = struct {
a: foo = foo{ .a = 444, .b = 555 },
b: u32,
};
fn hoge(x: bool) type {
if (x) {
return foo;
}
return bar;
}
const numType = enum {
signedInt,
unsignedInt,
fn get(comptime t: type) numType {
return if (@as(t, 0) -% 1 < 0) .signedInt else .unsignedInt;
}
};
ただ、コンパイル時に確定しなければならないという制限がある。
逆に言うと、それしか制限がない。と理解している。
エラー
エラーは、整数に名前をつけたものらしい。そしてその整数は今の所 符号なし16bit整数。
エラーユニオン
関数の返戻値などで !u8
のようになっているのがエラーユニオン。
!u8
は、u8
または エラーという意味。
!u8
のような型の値から u8
を得るには catch
を使う。
const x = foo() catch ifError;
foo()
がエラーを返した場合は catch
以下が評価される。そうでなければエラーではない値を得ることができる。
catch
内で return すると関数を終えることができるんだけど、この catch |err| return err
のシンタックスシュガーが try
。
const x = foo() catch |err| return err;
// ↑と↓は同じ
const x = try foo();
なるほどそういう意味だったのか。よくできている。
go が毎回
x, err := foo()
if err != nil {
return err
}
と書かせるのに対して、 zig は try
で済ませている。素晴らしい。
あと。go と違ってエラーを無視すると
fn hoge() !u32 {
return 0;
}
pub fn main() !void {
hoge(); // error is ignored. consider using `try`, `catch`, or `if`
// 以下略
}
コンパイルエラーになる。
errdefer
+ try
+ 「無視するとコンパイルエラー」 という組み合わせで、例外と同じぐらいの安全さになっているように思う。
それでいて例外よりも挙動がわかりやすい。
エラーの種類の指定
C++ の黒歴史である例外仕様 の香りがするので不安だけど、エラーの種類を指定できる。
こんな
const FBB = error{ Foo, Bar, Baz };
const BQQ = error{ Baz, Qux, Quux };
const FBBQQ = FBB || BQQ;
fn fbb() FBB!u32 {
return error.Foo;
}
fn bqq() BQQ!u32 {
return error.Quux;
}
fn fbbqq(b: bool) FBBQQ!u32 {
return if (b) {
fbb();
} else {
bqq();
};
}
感じ。
FBB
で定義されている Baz
と BQQ
で定義されている Baz
は同じ値である点がちょっと思いがけない。
両方とも error.Baz
で指定するので、書いてみるとまあ同じだよねとなるけれど。定義を見ると別にしか見えない。
エラーの推定
エラーの種類を省略すると推定してくれる。
推定してくれるんだけど、その関数は普通の関数ではなく generics 関数になってしまう。
generics 関数は普通の関数ではないので関数ポインタにできないとかそういう制限がある。
const F = error{Foo};
// こちらを呼ぶと「error: cannot resolve inferred error set」でコンパイルエラー
fn factG(i: u32) !u32 {
if (i < 0) {
return error.Foo;
}
if (i < 2) {
return i;
}
return (try factG(i - 1)) * i;
}
// こちらは OK。
fn factS(i: u32) F!u32 {
if (i < 0) {
return error.Foo;
}
if (i < 2) {
return i;
}
return (try factS(i - 1)) * i;
}
Optional
オプショナルがある。
if
の類と orelse
で値を得る。
fn hoge(i: u32) ?u32 {
// 略
}
fn bar() !void {
const opta = hoge(0);
const b = opta orelse 1;
if (opta) |a| {
try doSomething(a, b);
}
}
その他に opta.?
で値を得ることができるが、特段の事情がない限り使わないほうがいい。これさえ使わなければ、値がないのに中身を見に行って死ぬ、ということが文法的にできなくなる。
Optional ポインタ
Zig にはポインタがあるが、null ポインタがない。らしい。
代わりに Optional ポインタがある。
たとえば。C の関数が uint8_t*
を返すなら、 zig としては *u8
ではなく ?*u8
で受ける。
C の関数が NULL
を返しているのであれば、 zig は空の Optional としてうけるので、間接参照外しに到達できない。安全。
C の関数が非 NULL
を返しているのであれば、 zig は 空ではない Optional として受けて間接参照外しできるようになる。
型変換
暗黙の型変換
整数から整数、浮動小数点数から浮動小数点数の場合、情報が失われない型変換は暗黙のうちに行われる。
情報が失われる型変換はエラーになる。
ただ、整数を f32
f64
なんかに入れるのはエラー。桁落ちしないのであればエラーにしなくていいと思う。
var vu16: u16 = 1;
var vu32: u32 = 1;
var vi32: i32 = 1;
var vi64: i64 = 1;
var vu64: u64 = 1;
var vf32: f32 = 1;
var vf64: f64 = 1;
vu64 = vu32; // ok
vu64 = vi32; // エラー
vi64 = vu32; // ok
vf64 = vf32; // ok
vf64 = vi16; // エラー
そのほかに
-
u32
等から?u32
等への変換 -
u32
等からsomeErrorType!u32
等への変換 - 配列からスライスへの変換
など、色々ある。
ここは慣れるのに時間がかかりそう。
zig 二項演算子の暗黙の型変換のルールはまだ発見できてないのでよくわからない。
ただ、わりと複雑だと思う。
コンパイル時に値がわかっている場合、
const ci8: i8 = 3;
const cu8: u8 = 2;
const ci32: i32 = 20;
const cu32: u32 = 20;
const a = ci8 + cu8; // okay. a is i8
const b = cu8 + ci8; // okay. b is u8
const c = cu8 + ci32; // okay. c is i32
const d = cu32 + ci8; // okay. d is u32
const e = cu8 + cu32; // okay. e is u32
const f = cu8 + cu32 * cu32; // okay. f is u32
const g = ci8 + cu32 * cu32; // error. g is wanted to be an i8, but it overflows.
const h = cu32 * cu32 + ci8; // okay. g is u32
その値によって計算できたりできなかったりする。
コンパイル時にはわからない値の場合
fn hoge(vi8: i8, vu8: u8, vi32: i32, vu32: u32) !void {
var a = vi8 + vu8; // error
var b = vu8 + vi8; // error
var c = vu8 + vi32; // okay. c is i32
var d = vu32 + vi8; // error
var e = vu8 + vu32; // okay. e is u32
var f = vu8 + vu32 * vu32; // okay. f is u32
var g = vi8 + vu32 * vu32; // error
var h = vu32 * vu32 + vi8; // error
// 以下略
}
その型の範囲をつかう。
エラーを見る限り
- 符号の有無が同じなら、大きい方の型になる
- 符号の有無が異なる場合、左辺の方になる
というルールかなと思う。どうだろう。
明示的型変換
@
で始まる関数(のようなもの?)の形で、たくさん用意されている。
いくつか紹介すると。
zig | 説明 |
---|---|
@bitCast |
C++20 の std::bit_cast だと思う |
@floatCast |
f64 から f32 とか |
@intCast |
u32 から i32 とか |
@intToPtr |
整数をポインタにする。危険。 |
@ptrCast |
別の型へのポインタにする。危険。 |
@ptrToInt |
ポインタを整数にする。 |
こんな感じ。
便利そう。
comptime と generics
Zig は、静的型付け言語(だよね?)としては大変珍しく、u32
のような型やユーザー定義型を、 123
などと同じように変数に代入したり、関数に渡したり、関数の返戻値にしたりできる。
なので、generics というか template は、単に型を返す関数ということになる。
@hasField
(構造体などがフィールドを持っているかどうか調べる)なんかもある。
インラインアセンブラ
ある。
まあ使わないと思う。
使わないんじゃないかな。
Atomic
ある。文書に書いてないけど、ある。
suspend-resume と async-await
文書読んでサンプル見てサンプル書いたんだけど、まだ気持ちがわからない。
const std = @import("std");
const stdout = std.io.getStdOut().writer();
var log: u64 = 0;
fn amain() u32 {
log = log * 10 + 1;
var frame = async func();
log = log * 10 + 6;
const r: u32 = await frame;
log = log * 10 + 7;
return r;
}
fn func() u32 {
const wait = 100 * 1000 * 1000;
std.time.sleep(wait);
var r: u32 = 0;
suspend {
std.time.sleep(wait);
log = log * 10 + 2;
r = r * 10 + 2;
resume @frame();
log = log * 10 + 5;
r = r * 10 + 5;
}
log = log * 10 + 3;
r = r * 10 + 3;
std.time.sleep(wait);
log = log * 10 + 4;
r = r * 10 + 4;
return r;
}
pub fn main() !void {
const r = nosuspend amain();
try stdout.print("r={} log={}\n", .{ r, log });
}
これを実行すると
$ zig run x.zig
r=234 log=1234567
こうなる。よくわからない。
suspend
内の resume @frame();
後は、 return
の後に実行される模様。
async
で呼んだら、 await
まで突き進みそうな気がしていたが、そうでもないらしい。
async
で呼ばれる関数内で自分でスレッドつくって自分で join
しろということかな。
長くなったので
長くなったので、そろそろこのへんで切り上げて一旦公開にすることにした。
→ 書いた。(Zig の文書読んで所感を記す #2)
続編を書く予定だけど、予定は未定。
2022年8月26日追記
全体的な印象の話を書き忘れていたので、書いておく。
シンプル?
Zig はシンプルという噂だったように思っていたんだけど、全然そんなことない。
多機能で、大掛かり。印象としては、 C++ より大規模。
生々しい
メモリレイアウトとか、こういう CPU 命令に落ちるだろうなとか、そういう感覚を持ちながらコーディングができる感じ。
GC が無いとか、アライメントが指定できるとか、そういうこと。
ジェネリクスと型演算
型が整数と同じように引数になったり返戻値になったりできるので、とても強い。
なんでもできる感じ。
安全
安全に書きたいと思えば、わりと安全になると思う。
go と異なり、カジュアルにエラーを無視したりすることはできないし、C/C++/go と違って、ヌルポインタチェックを忘れて死ぬ、とかいうコードもそうそう書かなさそう。
ユーザー定義型と組み込み型の壁
C++ はこの壁を低くする努力を続けているが、 zig はそういうことは無いみたい。
演算子のオーバーロードがないし、ユーザー定義型をつくるリテラルを定義する文法もない。
f64
をユーザー定義型の有理数に変えたい、とかいうときに困るはずなので残念。
思いがけない言語仕様
- 0 bit 整数
- errdefer
- コンパイル時変数が可変である
- オーバーフロー時の振る舞いが違う3つの乗算演算子
- 静的にサイズが決まっている配列同士の連結で静的にサイズが決まっている配列が作れる
など、見たことのない言語仕様がたくさんある。面白い。