Open11
ZigでAtCoderのAPG4bやるやつ
AtCoderのAPG4b(https://atcoder.jp/contests/APG4b)は初心者向けなので, Zigの入門に使ってみます.
AtCoderのZigは現在(2024/10/06)ではv0.10.1なので, 現行のv0.13.1(?)とちょっと違うっぽいです.
以下を主に参考にしています.
- Zig (https://ziglang.org/)
- Zig Language Reference(https://ziglang.org/documentation/0.10.1/)
- Zigのstdのドキュメント (https://ziglang.org/documentation/0.10.1/std/)
TODO
- docker 環境を載せる.
- LSP server をインストールする.
- Zig についての感想を書く.
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})
のように{}
と第二引数を使うとC
のprintf
の様にできます.
- エラーになるかもしれないから,
感想
- モジュールの名前っぽい
std
がZig言語の定数っぽい扱いされているっぽい気がしますね.- @import 関数でファイルを読み込んでいるっぽい?
- 出力は(今の所)簡単そうに見えますね.
- フォーマット
{}
には下のようなものがあるそうです(他にも色々と...).-
{s}
(文字列) -
{c}
(ASCII文字) -
{any}
(全て?, 配列とか)
-
- フォーマット
- 入力は...苦労しました(後述)
- 実行時間が0msです.
- C言語並に速くてC言語より安全でC言語より面倒くさいって感じでしょうか.
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|の中の変数がその値になる感じですね.
- zig0.13.1では(0..6)みたいな感じできるようです.
-
[_]i32{...}
は配列を作る記法で, コンパイル時に値が決まるなら[_]
でサイズを自動にできるっぽいですね.
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/ から拝借したコード.
-
reader
とbuffer
を受け取り,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});
-
buf
でu8
の配列を自分で確保する. 初期値が要らない場合はundefined
を代入してOK. -
std.fmt.parseInt(T, s, base)
で 文字列s
をbase
をベースに型T
に変換をするらしい.
- エラーは後ろに
catch 何か
でも処理可能?- おそらく, try は失敗したら上に伝播させるが, catch はその段階で処理できるのだろう?
-
nextSpace
とnextLine
を使い分けているのは, 入力形式に依存しすぎているので, あんまり綺麗ではない.- 全ての文字列を読み込めば,
std.mem.tokenize
(zig0.13.1ではdeprecatedである.) を使って," \t\n"
で区切れば良さそうである.
- 全ての文字列を読み込めば,
- 後は
a + b
するだけである.- 入力の方が問題よりも難しそうなのである.
コントロールフローを隠さない
シンプルな言語の片鱗を見ることができたようです.
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]});
}
なるほど. ありがとうございます.
とても便利そうなモジュールですね.
(zig0.10.1)
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});
- 条件分岐は
if
かswitch
でできます.-
if
,switch
は式にもできます.- Rust っぽいですね.
-
-
var ans: usize = undefined;
-
var
で変数宣言する際に初期値を, コンパイル時に型が分かる必要があるらしいです. -
var ans = @as(usize, 0);
でもコンパイルが通ります... 素直に型宣言(: type
)した方が良いでしょう.
-
-
a / b
-
usize
の場合は整数除算が/
でできます. -
i32
,u64
などの場合は@divFloor
関数を使う必要があります.
-
ex7
const a = true;
const b = false;
const c = true;
-
bool
型があり, 値はtrue
かfalse
です.
if ((!a) and b) {
try stdout.writer().print("Bo", .{});
} else if ((!b) or c) {
try stdout.writer().print("Co", .{});
}
- 論理積は
and
, 論理和はor
です.- 短絡評価なので, 右の値が評価されないことがあります.
- 否定は
!
です. - 優先度は知りません. (
ドキュメントにも書いてない?→ Precedence)