Zigの良い点と良くない点を交互に書いていく
はじめに
最近Zig 0.14.0の安定版がリリースされてより一層使いやすくなったZigですが、まだまだ未成熟なところや言語設計として分かりづらい・難しい点があるので良いところと良くないところを合わせて書いてみようと思いました。
良い点1
中括弧が便利すぎる
例えばこの様な使い方ができます。
- Deferの例
std.http.Client.init(allocator) catch |err| {
std.debug.print("HTTPクライアントの初期化エラー: {s}\n", . {@errorName(err)});
return err;
};
// std.http.Client.fetchなどの関数を使用してhttp接続をした後
defer{
std.http.Client.deinit(testClient);
std.debug.print("解放完了", .{});
}
また、この様にスコープを抜ける時に複数の処理を指定できる。
- Mutexの例
sample_mutex: std.Thread.Mutex = .{},
// Mutexを初期化した後
{
self.sample_mutex.lock();
defer self.sample_mutex.unlock();
// ここで処理を書く
// ここでスコープから抜けるのでdeferが解放される。
}
{
self.sample_mutex.lock();
defer self.sample_mutex.unlock();
// ここで処理を書く
// ここでスコープから抜けるのでdeferが解放される。
}
上記の様にMutexなど最短でロックを解除しなくてはならないシナリオで{}のブロックを単体で使用することによってdeferが発生するタイミングを細かく調節出来ます。ZigやCなどメモリ管理を自分で行う言語特有の概念だと思いますが、メモリを解放するタイミングをミスすればセグメントフォルトが発生しますしMutexなどもタイミングが合わなければデッドロックによるハングを引き起こします。しかし、この様にスコープを細かく調整出来る機能は素晴らしいと思いました。特にZigはDeferによるメモリ解放をすることでCよりも安全にメモリ管理が出来るところが良い点です。
良くない点1
独特の概念が多い
- constとvarの扱い
慣れれば良いという感じもしますが最初は誰だって初心者なので間違える場面もあります。
例えば自分がC#からやってきて感じた違いはconstとvarキーワードの考え方の違いです。
C#ではconstは定数値でvarは型推論ですがZIGでは違います。constはメモリアドレスが変更できない変数に使用します。メモリアドレス先のポインターの値は変更可能です。varはメモリアドレスが変更できる変数に使用します。これを知らないでZIGでこのキーワードを使うと、varを使用してる場所で何故かconstに変更する必要があるという旨のエラーが発生します。
-
.{}
の存在
基本的にZigでは空の構造体などには.{}を使用します。
代表的な例だと
std.debug.print("テスト\n", .{});
debug.printはこの様な構造になっています。
fn print(comptime fmt: []const u8, args: anytype) void
だからなんだと思うかもしれませんが最初の方は忘れがちです。特に最初に.が付くのはZigぐらいだと思います。
- nullとundefinedの違い
Zigではnullは?Tのような値が存在しない状態を表す時に使います。まあ、これは他の言語でも似たようなもんだと思います。しかし、undefinedは初期化されていない変数の状態を表すキーワードになります。
var buffer: [32 * 1024]u8 = undefined;
var response = try client.open(.GET, uri, .{
.server_header_buffer = &buffer,
});
defer response.deinit();
上記のような変数を初期化せず後で上書きされることを前提としている時にundefinedを使用します。
- 型の複雑性
zigの型は非常に表現豊かです。良いところもある半面if文などを使用した時に同じ型だと思ってたのにエラーで弾かれるときがあります。例えば文字列を表現するu8の種類を例にあげます。
- u8 - 単純な8ビット符号なし整数(バイト)
- []u8 - u8型の可変(mutable)スライス
- []const u8 - u8型の不変(immutable)スライス(文字列としてよく使用される)
- [N]u8 - N個のu8を持つ固定長配列(例:[10]u8は10バイトの配列)
- *[N]u8 - 固定長配列へのポインタ
- *[]u8 - 可変スライスへのポインタ
- *const []u8 - 可変スライスへの不変ポインタ
- *[]const u8 - 不変スライスへのポインタ
- *const []const u8 - 不変スライスへの不変ポインタ
- [][]u8 - u8型の二次元スライス
- [][N]u8 - 各要素がN個のu8からなる配列のスライス
- [N][]u8 - N個のu8スライスからなる配列
- [:0]u8 - ヌル終端のバイトスライス(C文字列との互換性のため)
- [:0]const u8 - 不変のヌル終端バイトスライス
たくさんありますが最も多いのは[]u8と[]const u8の違いでの構文エラーです。
この場合最も簡単な型変換方法は
// urlが url: []const u8だった場合
const url_copy = try gpa.dupe(u8, url);
とすることで型変換が出来ます。
- build.zigの存在
zigではビルドフローやライブラリの追加などをbuild.zigに記述します。
zigだけで全て完結するのでCのMakeFileを書いたりしなくてよいのは良い点と言えます。しかし、build.zigだけで使用する関数や概念などがあり、学習難易度が高いです。
後で書きますがライブラリを追加するのに最初は手がかかるかもしれません。
良い点2
シンプルな言語設計
Zigに似た言語というか思いっきりインスパイアされた言語としてC言語がありますがC言語の良さを持ってきて痒いところに手が届く様にした言語だと思っています。よくBetter then Cという言葉がありますが個人的にはCにはCの良さZigにはzigの良さがあると思います。それでも、Cのマクロやヘッダーや関数宣言、関数のスコープ範囲など他にも様々な面倒くさい要素があります。Zigはそこら辺のCの面倒くさい部分を現代風の言語に直し、便利な機能をつけた言語だと思っています。この説明だとZigのライバルというか人気度は全然違うけど似たような言語にRustがあります。しかし、RustはZigよりも独自の構文や機能を持っており最近では、個人的には複雑化してると思っています。そしてRustには派生マクロなど完全に隠蔽されたマクロをライブラリ仕様で頻繁に使っています。Zigは隠蔽されたものは一切なくメモリ操作すら全て手動です。これが正解だとは全然思いません。ですが、低レイヤー向けのアプリケーションや構造理解においてマクロや隠蔽されたコードはデメリットになります。もちろん、メモリ操作を全て手動で行うZigにもデメリットは存在します。ZigがRustに100%勝ってると言える点はシンプルな言語設計だと思います。シンプルだけど強力な言語設計であり、開発者もユニークで良い言語だと思います。本題とはズレてしまいますが、Zigの公式Youtubeでは言語設計の解説など非常に勉強になるコンテンツをアップしてくれているので興味があったら覗いてみてください。
良くない点2
スタンダードライブラリの使い方が分かりづらい
Zigの公式ドキュメントにはスタンダードライブラリの使い方が書いてありません。なのでコミュニティーが作ったZig Guideを見る必要があるのですが、ここに書いてないのは全て自分で他の言語のドキュメントなどを見てZigに変換するかGithubで他の人のコードを参考にしないといけません。例えば、std.httpなどの解説はありません。Zigを触る人は中級者以上しかいない想定なのかもしれませんが、言語のコミュニティーを拡大するためにも、プログラミング初心者の人でも分かるようにドキュメントを整備して欲しいです。実際、こちら側が勝手にライバル視してるRustはドキュメントがしっかりし過ぎていてあれ以外の入門書など買わなくても全く問題ないレベルです。あそこまでとは言いませんが、ドキュメントが見やすいともっと人が増えると思うので頑張って欲しいです。
良い点3
痒いところに手が届く言語設計
- エラーを書くのが楽
zigではエラーを書く時に必要なのがこれだけです。
return error.WebSocketNotConnected;
これは非常に便利です。例えばdebug.printと組み合わせると
std.debug.print("Error websocket not connected : {s}\n", .{@errorName(err)});
return error.WebSocketNotConnected;
この様に書けます。WebSocketNotConnectedのようなException関数のオーバーライドを作成しなくても良いのは楽です。
- ポインターの参照外しを変数の関数呼び出しから行える
queue_ptr.*.mutex.Lock();
Cとかと比べるとより分かりやすいかなと思います。
- Cの場合
queue_ptr->mutex.lock()
それか
(*queue_ptr).mutex.lock()
- キャプチャパターン
オプショナルのアンラップと変数へのバインドを同時に行える
例
if (message) |m| {
self.handleWebSocketMessage(allocator, m) catch |err| {
std.debug.print("EventLoop: Error handling message: {s}\n", .{@errorName(err)});
// Free message data if handleWebSocketMessage didn't
allocator.free(m.data);
};
}
この仕様はcatchなどでも使用できる
browser.run() catch |err| {
std.debug.print("Error: {s}\n", .{@errorName(err)});
return err;
};
分かりやすく非常に便利。ただし ||内の変数名が同じスコープ範囲で重なるとエラーになるので変数名には気をつける必要があります。
- structの柔軟性
ZigではStructの中に関数を書けますし変数も書けます。
const Self = @This();
上記の@This();を使うことで再帰的に関数を参照できます。Zigのスタンダードライブラリでもよくstructを使用してその中に関数定義をしています。
良くない点3
外部ライブラリを追加するのに苦労する
zigにはbuild.zig.zonという外部ライブラリを定義するファイルがあります。ライブラリ作成者がGithubでReleaseをちゃんと作成していればzig fetchでダウンロードしてbuild.zig内に複数行追加するだけで良いです。
const hoge_dep = b.dependency("Hoge1", .{});
const hoge_module = hoge_dep.module("Hoge1");
//Under
//const exe = b.addExecutable(.{
// .name = "hoge",
// .root_source_file = b.path("src/main.zig"),
// .target = target,
// .optimize = optimize,
// });
exe.root_module.addImport("Hoge1", hoge_module);
しかしReleaseを作っていない場合、Git SubModuleで環境にコピーしてからライブラリのエントリーポイントとなるファイルを探してそこにパスを通したモジュールを作成し競合しないようにモジュールをインポートしなくてはなりません。これは、かなり面倒くさいです。CよりはマシですがRustと比べられるようなものじゃありません。外部ライブラリのバージョンチェック機能も無いです。正式リリースまでにはパッケージ機能が追加されるようですがいつになることやら...
終わりに
長々と話してきましたが、Zigは本当に良い言語です。色々と冗長なコードを書かなくちゃいけない時もありますがメモリを自分で操作したりポインターの操作をしたりしているときはプログラミングしてる感じが凄くして楽しいです。プログラミングをある程度やっていてまだZigを触ったことがない人はぜひこの機会に触ってみて下さい。
Discussion