Zigでライフゲームを作ってみる

2022/07/26に公開

Zigがずっと気になっていたので、とりあえずライフゲームを作ってみることに。
作っている最中に思ったことなどの備忘録的な記事です。詳しい実装については記しておりません。

ライフゲーム

セルオートマトンと呼ばれるものの一種。
単純なルールで生命の誕生や進化、淘汰などをシミュレーションしたもの。
詳細はwikipediaを参照してください。
私はニコニコ動画のライフゲームの世界という動画で知りました。この動画とても面白いのでおすすめです。
https://www.nicovideo.jp/watch/sm19347846

Zig

https://ziglang.org/
新しい低レイヤー寄りのプログラミング言語。
ここ最近、新しいJavascriptランタイムであるBunが話題となり、そこでZigが使われていたことによりよく目にするようになりました。
実際にどんな言語なのか触っていましたが、低レイヤーの言語をほとんど触らないせいでかなり苦戦しました...

兎にも角にもHello, world

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

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

debug.printは、第一引数に文字列のフォーマット、第二引数に文字列の中で表示したい値の配列を受け取る。ここでは特に表示したい値はないので空の配列.{}を渡している。
.{}.は型の省略を意味しているようです。
例えば、Zigの配列の初期化は次のように表せますが、

// array literal
var array: [4]u8 = [_]u8{ 11, 22, 33, 44 };

変数とともに型も宣言しているので、配列リテラルの方は型を省略できる。

// array literal
var array: [4]u8 = .{ 11, 22, 33, 44 };   

配列の初期化

セルオートマトンは、二次元配列にbool型の値を持たせることで表現する。
Zigで配列の持つ値を全て同じにするには、

// initialize an array to zero
const all_zero = [_]u16{0} ** 10;

ただ、二次元配列では簡単な書き方が見つからなかったので、普通に配列を回すのが良いかも。

乱数

乱数を使うために、Zigの標準ライブラリを見てみましょう。
https://ziglang.org/documentation/0.9.1/std/

検索欄でrandと検索すると、std.randが出てきました。
DefaultPrngという名前でPRNG(擬似乱数生成器)が用意されているので、こちらを使わせていただきましょう。(ドキュメントの内容はあまり充実していないように見えたので、ソースコードのコメントを見た方が欲しい情報が手に入れられそうです。)
https://github.com/ziglang/zig/blob/master/lib/std/rand.zig
タイムスタンプをseedに使いました。
このような感じで乱数を使うことができます。

const std = @import("std");
const prng = std.rand.DefaultPrng;
const time = std.time;
const print = std.debug.print;

pub fn main() void {
    var rand = prng.init(@intCast(u64, time.milliTimestamp()));
    print("{}\n", .{rand.random().int(u32)});
}

配列を関数に渡す

何も考えずに配列を渡したらエラッた。

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

fn printArray(array: []i32) void {
    for (array) |value| {
        print("{}", .{value});
    }
}

pub fn main() void {
    const array = [_]i32{ 3, 1, 4, 1, 5, 9, 2 };
    printArray(array);
}
❯ zig run main.zig
./main.zig:12:16: error: expected type '[]i32', found '[7]i32'
    printArray(array);

仮引数側の配列の型をconstにして、実引数で配列へのポインタを渡せば良いらしい。

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

fn printArray(array: []const i32) void {
    for (array) |value| {
        print("{}", .{value});
    }
}

pub fn main() void {
    const array = [_]i32{ 3, 1, 4, 1, 5, 9, 2 };
    printArray(&array);
}
❯ zig run main.zig
3141592

ここら辺が理解不足でよくわかっていない。

配列のコピー

@memcpyという低レベル組み込み関数があるようですが、これには安全機構がないので基本的には使わずに、次のように書いてくださいとあります。

const array1 = [_]i32{ 3, 1, 4, 1, 5, 9, 2 };
var array2: [array1.len]i32 = undefined;
for (array1) |b, i| array2[i] = b;

オプティマイザは上記の書き方を@memcpyに変えてくれるそうです。
また、標準ライブラリもあります。

const mem = @import("std").mem;
const array1 = [_]i32{ 3, 1, 4, 1, 5, 9, 2 };
var array2: [array1.len]i32 = undefined;

mem.copy(i32, &array2, &array1);

ライフゲームを作る過程で主に詰まったところは以上になります。

感想

普段はPythonでコードを書くことが多いのでなかなか苦戦しました。ただ、Rustにチャレンジしてみた時よりは苦しみが少ないように感じました。C言語を書き慣れている人にとっては全く問題ないかもしれません。
低レイヤーに近い言語はプログラミングしている感があって楽しいですね。
RustやZig、最近ではCarbonなるものが出てきていますが、Web技術だけでなく低レイヤーの方の技術も騒がしくなっているように思うので、これからの進化が楽しみです。

作ったもの

せめて構造体を使うように書き直したい...
debug.printで表示しているのをなんとかしたい...
https://github.com/k41531/lifegame-zig

Discussion