Open11

ZigでAtCoderのAPG4bやるやつ

osd-yumosd-yum

AtCoderのAPG4b(https://atcoder.jp/contests/APG4b)は初心者向けなので, Zigの入門に使ってみます.
AtCoderのZigは現在(2024/10/06)ではv0.10.1なので, 現行のv0.13.1(?)とちょっと違うっぽいです.

以下を主に参考にしています.

osd-yumosd-yum

TODO

  • docker 環境を載せる.
  • LSP server をインストールする.
  • Zig についての感想を書く.
osd-yumosd-yum

A問題 (Hello, world!を出力する)

提出

const std = @import("std");

pub fn main() !void {
    const stdout = std.io.getStdOut();
    try stdout.writer().print("Hello, world!\n", .{});
}

解説?

  • const std = @import("std");
    • モジュールを名前付きで参照するみたいな?
    • @付きのものはbuiltin関数らしいです.
  • pub fn main() !void { ... }
    • この中身が実行されるみたいです.
    • !voidはエラーになるかもみたいな感じです?
  • std.io.getStdOut()
    • stdモジュールのioモジュールのstd.io.getStdOut()関数?
    • stdout定数はFile型らしい.
  • stdout.writer().print(..., ...)
    • エラーになるかもしれないから, tryで受けるようです. (エラー伝播?)
    • printは2変数関数であり, print("Hello, {}\n", .{42})のように{}と第二引数を使うとCprintfの様にできます.

感想

  • モジュールの名前っぽいstdがZig言語の定数っぽい扱いされているっぽい気がしますね.
    • @import 関数でファイルを読み込んでいるっぽい?
  • 出力は(今の所)簡単そうに見えますね.
    • フォーマット{}には下のようなものがあるそうです(他にも色々と...).
      • {s}(文字列)
      • {c}(ASCII文字)
      • {any}(全て?, 配列とか)
  • 入力は...苦労しました(後述)
  • 実行時間が0msです.
    • C言語並に速くてC言語より安全でC言語より面倒くさいって感じでしょうか.
osd-yumosd-yum

ex1〜2

pub fn main() !void {
    const stdout = std.io.getStdOut();
    try stdout.writer().print("こんにちは\nAtCoder\n", .{});
}
  • ASCIIではない"こんにちは"もちゃんと出力されている.

ex3

提出

    const n = 100;
    try stdout.writer().print("{}\n", .{n * (n + 1) / 2});
  • format付きの出力ができるのは楽ですね.

ex4

提出

    const seconds_per_year = 365 * 24 * 60 * 60;
    const stdout = std.io.getStdOut();
    for ([_]i32{1, 2, 5, 10}) |y| {
        try stdout.writer().print("{}\n", .{y * seconds_per_year});
    }
  • zig0.10.1ではfor文は括弧の中に配列を書いて, |y|の中の変数がその値になる感じですね.
  • [_]i32{...} は配列を作る記法で, コンパイル時に値が決まるなら [_] でサイズを自動にできるっぽいですね.
osd-yumosd-yum

ex5

提出

// from https://zig.guide/standard-library/readers-and-writers/
fn nextLine(reader: anytype, buffer: []u8) !?[]const u8 {
    var line = (try reader.readUntilDelimiterOrEof(
        buffer,
        '\n',
    )) orelse return null;
    return line;
}
  • https://zig.guide/standard-library/readers-and-writers/ から拝借したコード.
  • readerbuffer を受け取り, line を返している.
    • !?[]const u8 が返り値の型である. めっちゃ驚いている.
    • !null を返すかもしれない.
    • ? はエラーを返すかもしれない.
    • 実行時に決まるわけではないのに const が付いている. const キーワードは何のためにあるのだろうか.
      • const なしでも動く上, 文字列を var で受け取れば変更できるので, const は配列を不変にするためなのだろう.
  • この関数は reader.readUntilDelimiterOrEof という名は体を表しそうな関数で入力を delimiter か EOF まで読み込んでそうである.
    • '\n' を変えたらスペースまで読み込めそうなので, nextSpace 関数も同様に定義できる.
    const stdin = std.io.getStdIn();
    var buf: [1024]u8 = undefined;
    const a = std.fmt.parseInt(usize, (try nextSpace(stdin.reader(), &buf)).?, 10) catch unreachable;
    const b = std.fmt.parseInt(usize, (try nextLine(stdin.reader(), &buf)).?, 10) catch unreachable;

    const stdout = std.io.getStdOut();
    try stdout.writer().print("{}\n", .{a + b});
  • bufu8 の配列を自分で確保する. 初期値が要らない場合は undefined を代入してOK.
  • std.fmt.parseInt(T, s, base) で 文字列 sbase をベースに型 T に変換をするらしい.
  • エラーは後ろに catch 何か でも処理可能?
    • おそらく, try は失敗したら上に伝播させるが, catch はその段階で処理できるのだろう?
  • nextSpacenextLine を使い分けているのは, 入力形式に依存しすぎているので, あんまり綺麗ではない.
    • 全ての文字列を読み込めば, std.mem.tokenize (zig0.13.1ではdeprecatedである.) を使って, " \t\n" で区切れば良さそうである.
  • 後は a + b するだけである.
    • 入力の方が問題よりも難しそうなのである.

コントロールフローを隠さない シンプルな言語の片鱗を見ることができたようです.

ktz_aliasktz_alias

Null終端(\0)限定になりますが、std.zig.Tokenizerでトークンの切り出し行えます。
文字列リテラル読み出すのにはこっちの方が便利かも。

var buf: [1024:0]u8 = undefined;
// (snip)
var tokens = std.zig.Tokenizer.init(&buf);
while (true) {
    const tk = tokens.next();
    if (tk == .eof) break;
    std.debug.print("{s}", .{buf[tk.loc.start..tk.loc.end]});
}
osd-yumosd-yum

ex6

提出

    const stdin = std.io.getStdIn();
    var buf: [1024]u8 = undefined;
    const a = std.fmt.parseInt(usize, (try nextSpace(stdin.reader(), &buf)).?, 10) catch unreachable;
    const op = ((try nextSpace(stdin.reader(), &buf)).?)[0];
    const b = std.fmt.parseInt(usize, (try nextLine(stdin.reader(), &buf)).?, 10) catch unreachable;

    // std.debug.print("{c}\n", .{op});
  • std.debug.print(fmt, args) で標準エラー出力できます.
  • const op = (読み込み)[0];[0] で値として取らないと, おかしなことになります...
    • おそらく, buf のポインタと読み込み関数 の返したポインタが同じ場所を指しているため, parseInt[0] で値として受け取らないといけないのでしょう.
    var ans: usize = undefined;
    const stdout = std.io.getStdOut();
    if (op[0] == '+') {
        ans = a + b;
    } else if (op[0] == '-') {
        ans = a - b;
    } else if (op[0] == '*') {
        ans = a * b;
    } else if (op[0] == '/') {
        if (b == 0) {
            try stdout.writer().print("error\n", .{});
            return;
        }
        ans = a / b;
    } else {
        try stdout.writer().print("error\n", .{});
        return;
    }
    try stdout.writer().print("{}\n", .{ans});
  • 条件分岐は ifswitch でできます.
    • if, switch は式にもできます.
      • Rust っぽいですね.
  • var ans: usize = undefined;
    • var で変数宣言する際に初期値を, コンパイル時に型が分かる必要があるらしいです.
    • var ans = @as(usize, 0); でもコンパイルが通ります... 素直に型宣言(: type)した方が良いでしょう.
  • a / b
    • usize の場合は整数除算が / でできます.
    • i32, u64 などの場合は @divFloor 関数を使う必要があります.
osd-yumosd-yum

ex7

提出

    const a = true;
    const b = false;
    const c = true;
  • bool 型があり, 値は truefalse です.
    if ((!a) and b) {
        try stdout.writer().print("Bo", .{});
    } else if ((!b) or c) {
        try stdout.writer().print("Co", .{});
    }
  • 論理積は and, 論理和は or です.
    • 短絡評価なので, 右の値が評価されないことがあります.
  • 否定は ! です.
  • 優先度は知りません. (ドキュメントにも書いてない? → Precedence)
osd-yumosd-yum

ありがとうございます.
Precedence にたどり着くべきでした...
助かります.