ziglearnを読む Chapter1
低レイヤに使える新しいプログラミング言語と噂のzigを勉強してみようと思ったので、ziglearn.orgを読んでいく。
あとから読み返しやすいようにしたいので、ほとんど日本語訳みたいになると思います。
ダウンロード
Zigのダウンロードページからダウンロードしました。
とりあえずmasterのWindows向けbinaryを入れてみます。
zipをダウンロードしたら、展開したら使えます。
そのままだとPATHが通っていないので、PATHを通します。
こちらのページにユーザレベル / システムレベルでのパスの通し方が書いてあります。
Windowsの場合はPowershellのスクリプトを利用します。
無事導入が終わったら、zig version
のコマンドを実行します。
versionが表示されたら無事zigが使えるようになっています。
代入
変数の宣言は (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;
}
配列
- 配列は
[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);
}
そういえばインラインテストもあるみたい。
expect
は標準の関数で、false
が渡されるとテストを失敗させる。
const expect = @import("std").testing.expect;
test "zig test" {
try expect(false);
}
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);
}
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);
}
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) |_| {}
}
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);
}
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);
}
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);
}
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|
とすることでエラーをキャプチャできる。
try
はx catch |err| return err|
の短縮形で、エラーとなっちゃいけなところでエラーを補足する。
他の言語でよくあるtry-catch
構文とは違って、Zigのtry
とcatch
は関係ないらしい。
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);
};
}
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);
}
Errorセットは、合成することができる。
属性ごとにエラー用意しといて、包括的なエラーを作って~とかできそう。
const FileError = error { NotFound, WriteError };
const MemoryError = error { AccessDenied, CannotAllocate };
const AllError = FileError || MemoryError;