目次
- 背景
- Zigとの出会い
- なぜCではなくzigにしたのか
- C++、Rustは学習コストが高い
- zigとは
- zigの基本的な文法
- stringがない
- エラーハンドリング
- データ構造
- 低レベル言語を扱う上での概念を理解する
- ポインタ
- メモリ管理とallocator
- ツールが便利
- zigの学習方法
- 読んだもの
- 作ったもの
- 個人的には使ってないけど人気のリソース
- zigをおすすめできる人
- zigを学ぶのを避けた方がいい理由
- まとめ
- PR
- 関連記事
背景
こんにちは。 かりんとうマニア(@karintozuki)です。
どうも、業務ではPHPしか触らないPHPer、かりんとうです。
この記事ではPHPerが低レベル言語zigを勉強してみて何をどう学んだかなどを紹介していきます。
詳しい言語の紹介というよりは個人の感想・レポみたいな感じで軽く読んでください。
Zigとの出会い
PHPばっかり触っていると他の言語にも興味が出てきますね。
正直、JS, Python, RubyなどPHPと同じスクリプティング言語は割とすんなり覚えられそうなので、全然おもむきが違う言語に挑戦したいと思っていました。
そういうおもむきが違う言語といえば関数型プログラミング言語と低レベル言語ではないでしょうか。
ところでPaul Grahamのハッカーと画家にこんな一節があります。
ハッカーになりたければ簡単なPythonとJavaでプログラミングを覚えて、もっと本気ならUnixをハックするためにCを勉強しろ。本当にガチなやつはLispを勉強しろ。
(筆者による超意訳です。この文自体もHow to become a hackerというエッセイからの引用らしいのですが原典を見つけられませんでした。)
というわけでLispはおいといて(Lispを推すための引用だったのに。ごめんPaul Graham。)Cを勉強したいな、という気持ちがあったわけです。
なぜCではなくzigにしたのか
ではなぜCを勉強しなかったのか。
Cはきついからです。なんというかAPIが古いですね(Cが好きな人すみません)。
大学でCをやったのですが結構辛かった記憶があります。
変数を先頭で宣言しないといけない(C99からはその限りではないらしいですが)とか例外処理ができないとかネームスペース的なのもないし、ビルドに使うMakeとかもよく分からんって感じです。
ただ、これはあくまで大学でC入門を一コマやっただけの人の感想です。
自分がCに偏見を持っているように、PHPを批判する人のほとんどはモダンな書き方を知らないだけ、ということもあるので、モダンなCがあるんじゃないかと考えました。
そこでModern Cでググって出てきた記事の最初の一文がこれです。
https://postd.cc/how-to-c-in-2016-1/
C言語の第1のルールは、「もし避けられるならC言語を使うな」ということです。
なんかこれに妙に納得してしまい、他の選択肢を探し始めました。
(しばらくZigを勉強して低レベルの世界を経験してからは安全性という意味でもCはきつい=安全なコードを書くのが難しいと思っています。ただ、低レベルの勉強にはCの知識は必須というのも事実だと思うので、いつか勉強したいと思います。)
C++、Rustは学習コストが高い
モダンな低レベル言語といえばRustです。もちろん良い言語なのでしょうが、学習コストが高そうです。
さっさと動くものが作りたかったのと、仕事の傍ら趣味で書くコードなので一旦Rustはやめといたほうが良いかなと思いました。
C++も似たような理由で巨大すぎる言語体系であるということで敬遠しました。
zigとは
そこでzigに出会いました。
zigはモダンな低レベル言語でありながらもシンプルな言語であることを目指しています。
ミニマリストという意味で低レベル界のGoって感じもしますし、機能としてはCの正統進化という感じもします。実際、Cの代替となることを目指しているようです。
zigは「あなたのプログラミングの知識をデバッグする代わりに、アプリケーションをデバッグする」というスローガンを掲げています。
複雑で多機能な言語は便利ですけどちょっと混乱してしまいますよね。zigはシンプルでコードを書くことに集中できるように慎重に言語機能が選択されているように感じました。
また、暗黙的な操作(動的メモリ割り当てやマクロなど)を排除することを目指していて、書かれたコードがそのまま実行されるようになっています。
zigの基本的な文法
ここからは実際のコードを紹介します。
シンプルな言語を目指しているだけあって文法自体はそんなに難しいことは無かったです。
Hello worldに毛が生えたようなものとfizzbuzzはこんな感じになります
1 | // 標準ライブラリのインポート |
std.debug.print() がいわゆるprintf()です。一つ目の引数に文字列、二つ目の引数にフォーマットしたい変数を入れられます。
余談ですが、このprint関数がstd.debug配下に置かれているのはzigが暗黙的な操作を禁止していることに関連しています。PHPなどのechoの中ではバッファリングのためのメモリ割り当てなどが暗黙的に行われているため、zigは単純なechoやprintといった関数は用意していないようです。
ちょっと入門の範囲ではないですが、.{}は匿名structで、その場限りのstructを使うときのための文法です。ここでは可変長の引数を取るために使われていますね。
PHPとは違い静的型付け言語なので型を宣言しないといけませんが、推論が効くのでそれほど冗長な感じはせず、typehint付きのPHPを書いている気分で書けました。
1 | // 型が推論されるので、わざわざ変数の型を書く必要は無い |
関数宣言はこんな感じでこちらは引数と返り値の型を指定する必要があります。
usizeはintみたいなものです。
1 | fn timesTwo(n: usize) usize { |
main 関数が最初に実行されます。関数はpublicとprivateがあり、publicな関数にはpub fnを使って宣言します。そうするとその関数は他のファイルから実行できるようになります。mainにはpubがついてますね。
if文は普通ですね。
1 | if (i % 3 == 0) { |
zigで面白いのはif文をインラインで三項演算子のように使えることです。
1 | const is_success = false; |
for ループは少し変わっていて、いわゆる3つの初期化、終了条件、反復処理の式を取るような書き方はできず、PHPでいうところのforeachが基本になっています。それぞれの要素は|n|のようにパイプで囲った変数に値が格納されます。
1 | const nums = [_]u8{1,3,5}; |
ここからはPHPerとして面白いなと思ったことを紹介します。
stringがない
これはびっくりしましたが、zigには文字列という型がありません。
文字列はu8(8bit=1byteのunsigned整数)のスライスという形で処理されます。
(スライスについては後述します。)
これはASCII文字を使う際にはそんなに問題にならないのですが、日本語などのマルチバイト文字を扱う時には特に注意が必要です。
1 | // これは想定通りa, b, cが一行ずつ表示される |
一応3rdパーティーの文字列ライブラリもあるのですが、筆者はBase8192というゴリゴリ漢字を使うプログラムを標準ライブラリだけで書けたので意外とこれでいいやという感じもしています。
エラーハンドリング
zigでは最近の言語らしくエラーは値として扱われます。
ですが、エラーのための特別な文法が用意されているため、PHPなどの例外を持つ言語と似たような書き方ができる、といった印象を受けました。(Goとかは「エラーはただの値である」という思想をもっと徹底している感じがします。)
もし、関数がエラーを返す場合は返り値に!を使います。
1 | fn printDay(i: u8) error{InvalidArgument}!void { |
エラーの作り方(?)はまだ勉強しないといけないのですが、以下のようにしてエラーを作ることができます。
1 | // MyErrorというエラーを作る |
返り値は返すエラーを指定することもできますが、ただ!をつけておけばエラーが返ることを明示出来ます。エラーが返る可能性がある関数では!を省略することはできずコンパイルエラーになります。
返り値に!をつけることで関数の返り値がエラーと正常時の返り値のUnionになります。
1 | // InvalidArgumentエラー、もしくはvoid |
自分が使う限りではただ!をつけておくだけで事足りていました。ただもっと大きなプロジェクトなどエラーの型をしっかり指定したいケースがあるかもしれません。
また、tryとcatchというキーワードがありますが、PHPや普通の例外を持つ言語のそれとは少し違います。これがあることで普通の例外のような大域脱出っぽいものを実装できます。
先ほどのmain関数ではtryキーワードを使ってprintDay関数を読んでいました。tryを使うことでもし例外が返ってきたらその例外をそのまま返します。PHPの例外と似たような挙動になります。
1 |
|
catchではエラーが起きたときの処理を記述します。ここで注意することはcatchはtryと一緒に使うことはできないということです。catchを使うともうエラーが返らないので返り値の型はvoidになっています。
1 | pub fn main() void { |
zigのエラーの良いところはPHPのような例外と違って、関数の返り値の型がエラーとのUnionになるので、エラーを正しく処理しないとコンパイルエラーになります。たまにそれがうっとおしい感じもしますが、正しいコードを書くために必要なことだと思いました。
データ構造
PHPにおいてデータ構造は配列を使えばほぼOKでした笑。
zigでは同じ型をいくつか保持するようなデータ構造に配列、スライス、ArrayListがあります。
配列は型宣言時に長さが決まっていないといけません。以下の例のように長さ(ここでは3)が型宣言に必要ですがアンダースコアを使うと少し楽です。
1 | const numbers: [3]u8 = .{1,2,3}; |
Sliceは配列と似ていますが、その実態は配列とその長さを持ったstructです。文字列はu8のスライスとして表現されます。
1 | const str:[]const u8 = "文字列はスライス"; |
スライスを使うと何が嬉しいかというとCの配列のようにバッファーオーバーフローなどが起きる危険性を減らすことができます。こういった安全性を増す機構が言語標準であるのが初心者としてはありがたいですね。
コンパイル時に長さが分からないときはPHPの配列のように動的に長さが変わるArrrayListという構造を使います。これも内部的にはスライスが使われていて.itemsで内部で使われているスライスにアクセスできます。
1 | var al = std.ArrayList(u8).init(allocator); |
3つもあるのでどれを使うか迷うかもしれませんが、基本的にはスライスを使ってみて、スライスで対応できない際はArrayListを使う、という感じでした。配列はあまり使いませんでした。
OOP
zigにはクラスがないので、厳密な意味ではOOPはできません。
ですが、クラスの代わりにstructがあり、そこで関数を定義できるのでOOPっぽい書き方は十分可能です。以下はUserストラクトを定義する例です。
1 | const User = struct { |
Userストラクト内の関数はuser.printInfo();のようにドットオペレータを使って呼び出せます。
ストラクト内の関数は最初の引数にUserを取ります。これはOOPで言うところのthisに相当します。
printInfo はUserの値を変更しないので最初の引数がUserの値、しかしchangeNameはnameフィールドを変更するのでUserのポインタを受け取っていることに注意してください。
また注意すべき点としてFieldは全部public(!)です。たいして関数はpubの有無で他のモジュール(ファイル)から呼び出せるかどうかを決められます。
低レベル言語を扱う上での概念を理解する
zigを勉強するうえで文法は、シンプルな言語を目指しているだけあり、そんなに難しくないです。
しかしPHPerの私には低レベル言語特有の概念を理解することが難しかったです。この章ではそういった概念を紹介します。
ポインタ
低レベル言語あるあるとしてポインタがありますね。
zigのポインタ関連の文法はこんな感じです。
1 | var n:u8 = 0; |
基本的にCっぽい文法ですが、参照先の値を使うときはCのような*n_ptrではなくn_ptr.*を使います。多分ポインタの型を示すのと参照先の値をとる文法が同じだとわかりづらいから別にしたんだと思います。
大学の講義でCをやったときはピンと来なかったポインタですが個人的にはメモリ管理などを勉強するようになってもっと解像度が上がった気がしました。
メモリ管理とallocator
ポインタに続いて低レベル言語のとっつきづらい分野がメモリ管理だと思います。
メモリとはみたいなのは色んな記事が出ているので特にここでは詳しく書きませんが、PHPerの私としてはStackとHeapの違いを理解するところから始めるのが良いと思いました。
StackとHeap
(勉強し始めなので間違っているかもしれませんが)Stackとは関数の中の変数を保持するメモリでその関数が値を返す時には中身は消えてしまいます。また、容量も大きくありません。Stack領域に大きすぎる変数を作ってStackメモリを全て使ってしまうのがStack Overflowですね。
スコープが狭い代わりに手動でメモリを割り当て・解放する必要がないので簡単・高速です。
対してHeapは関数を超えて存在でき、(多分)容量もPCのメモリが許す限り使うことができます。しかし、割り当てたメモリはプログラマが手動で解放しないといけません。それを忘れてしまうことでメモリリークなどの不具合が起きてしまいます。
zigのAllocator
zigでメモリを割り当てる際はAllocatorを使います。zig を使う際に結構混乱するのがこのAllocator だと思うので少し解説してみます。
まずzigでは暗黙的なメモリ割り当てをしないことになっているので、ライブラリなどでメモリ割り当てが起きる関数は引数にallocatorがあります。
例としてはArrayListのinit関数などですね。内部でメモリ割り当てが行われるので、init関数がAllcoatorを受け取るようになっています。
1 | var arraylist = std.ArrayList(u8).init(allocator); |
ライブラリや自分で書くコードでも至るところでAllocatorを受け取る関数を書くようになるので覚えておきましょう。
Allocatorは用途に応じてさまざまなものがあります。全部紹介すると長いのでよく使うものを紹介します。
General Purpose Allocator (GPA)
GPAはとりあえずこれを使っておけって感じのアロケーターです。
実際にプログラムを書くときはmain関数でGPAを定義しておいてプログラム全体で使い回します。各モジュールは呼び出し元からallocatorを受け取るようにしておけばいちいちallocatorを定義することなくすんなりコードが書けます。特別理由がなければその書き方でほとんど対応できるんではないでしょうか。
Arena Allocator
Arenaはメモリ割り当てされたメモリが複数あるときに便利なアロケーターです。Arenaでは割り当てしたメモリを一箇所でまとめて解放できます。
例えばStructが複数のフィールドをHeapに持っていた場合、Arenaを使うことでそのStructを使い終わった段階でそのフィールドを一度に解放できます。実際のコードでは複数の変数を個別に管理するよりこのようにある段階でまとめて使い終わるみたいな場面が結構多いので重宝します。
ちなみにこういうStructには初期化用のinit関数と最後に呼び出すdeinit関数を用意するのが定石のようです。
Arenaのもう一つの特徴としてはArenaアロケーターを作成する際にはバックエンド(?)として他のAllocatorを渡す必要があることです。他のアロケーターからArenaを作成するコードは以下のようになります。
余談ですがこのコードのallocatorはallocatorインターフェースを持つ変数です。zigのインターフェースを作るのは結構特殊なんですが、使う際は普通のインターフェースと同じで決まった関数を持つ変数になってます。allocatorインターフェースはallocやfreeなどの関数を持ちます。
1 | // 他のアロケータを受け取ってその上にArenaを作る |
FixedBuffer Allocator
FixedBuffer Allocatorはアロケーターとは名ばかり実際にはHeap割り当てを行わず、Stack上に作ったBufferだけでメモリ管理をします。
これはライブラリの関数などを使う際にAllcoatorが引数に必要だけれど、メモリ使用量がStackに載る大きさだと分かっているときに便利です。
テストアロケーター
std.testing.allocator と書くだけで簡単にテスト用のアロケータが得られます。
これはシンプルに一行でアロケーターが作れて便利なのとメモリリークなどがあった際にはしっかりエラーで教えてくれて便利です。
注意事項としてはテストの中でしか使えません。
最後に公式の言語リファレンスにどのアロケーターを使うべきか、という章があるのでそこも読んでみてください。
https://ziglang.org/documentation/0.15.2/#Choosing-an-Allocator
ツールが便利
zigはビルトインのツールが割と便利だったので紹介します。
テスト
個人的には言語にテストが組み込まれているのはありがたいです。
以下のような感じでtestキーワードを使うことでテストが書けます。
またassertionなどは標準ライブラリのstd.testingで大体のassertionがカバーされています。
1 | fn add(x: usize, y: usize) usize { |
テストはテスト対象のコードと同じファイルに直接書いても良いし、test用ファイルを分けて書いても良いみたいです。個人的にはそんなにでかいプロジェクトでもないので対象のコードと同じファイルに直接書いてしまっていました。
フォーマッター
プロジェクトルートでzig fmt .コマンドを打つとフォーマットしてくれます。これも言語標準でフォーマッターがあることで無用な論争を避けることができて良いですね。
ビルドツール
CやC++を書くうえでの悩み事としてMakeなどのビルドツールを覚えるのが辛いというのがあるらしいです。
zigではzig.buildというファイルにzigでビルドを記述することができます。
zigでかけるのでビルドツールの習得に時間を割かなくて良い、というのがウリだと思うのですが、個人的にはzig.buildは何をやっているか最初は分からず、勉強する必要があり、学習コストゼロというのは少し盛っていると思いました。ですが、一度覚えてしまえばメインのプログラムのビルド以外にも必要なファイルの生成など色んなことができるので便利だなと思いました。
この記事ではビルドの細かいところには触れませんが、また記事を書くかもしれません。
クロスコンパイル対応
もともとコンパイル言語をそんなに触ったことがなかったので、ありがたみが最初は分からなかったのですが、zigはクロスコンパイルが簡単にできます。
クロスコンパイルとはWindowsやMacなど違うOSで動くバイナリを生成することです。
例えばCをmacOSで書いてLinuxで動くバイナリを生成しようとするとVirtual Machineなどに頼らないといけません。ですが、zigでは標準でクロスコンパイルをサポートしていて、コンパイルするときにターゲットオプションを指定するだけでできてしまいます。
何かソフトウェアを作ってバイナリを配布したいときにとても便利ですね。
zigの学習方法
ここからは自分がどうやってzigを学んだかを紹介していきます。
読んだもの
zig公式
zig公式ドキュメントのGetting StartedとOverviewはざっくり目を通すと良いかなと思います。
https://ziglang.org/learn/getting-started/
https://ziglang.org/learn/overview/
zig.guide
zig.guideは言語仕様が一通りまとまっているので、頭から読むというよりはコードを書きながらわからない文法を参照する感じですね。
Introduction to Zig
しばらく小さいプロジェクトでコードを書けるようになってきたあとはIntroduction to zigという本を読みました。
これは電子書籍で購入もできますが、ブラウザ版は無料で読めます。
https://pedropark99.github.io/zig-book/
初めて低レベル言語を触る、GCがある言語から来た人がターゲットとして書かれた本でとても良かったです。
概念の説明が何章か続いた後に習った内容を利用してプロジェクトを作るという運びで丁寧に概念を説明してくれてzig、ひいては低レベルの概念を理解するのに良い本でした。
作ったもの
プログラミング言語を学習するうえで、やはり実際にコードを書くこと以上に勉強になることはありませんね。何か小さなプロジェクトをやってみるのが楽しいと思うので、参考になるかわかりませんが、私が作った趣味プロジェクトを紹介します。
gh-notify
GitHub Actionが終わったらデスクトップ通知を飛ばしてくれるCLIツールです。内部的にはghとnotify-sendコマンドを読んでるだけなので普通にShellスクリプト書いた方が100倍筋がいいのですが、Heapに割り当てた文字列の扱いや標準ライブラリを使ってJSONをパースするなど普通に勉強になりました。
https://github.com/karintomania/gh-notify
zeff
これは絵文字を探してくれるCLIツールです。termboxというCで書かれたTUIライブラリを使っていて、Cのライブラリとのバインディングや曖昧検索のアルゴリズムを手書きしたり色々と勉強になりました。絵文字はUTF8でマルチバイトであることに加え、複数の絵文字を組み合わせて一つの絵文字を作るパターン(👩 + 💻 → 👩💻)もあるで、扱いに苦労しました。
zeff
Base8192
これはBase64みたいなエンコードを8192種類の漢字を使ってやるという半分ジョークな自作エンコーディングを作りました。エンコーダ・デコーダをzigで書いて、そのエンコーダのWasmバイナリをBase8192でエンコーディング、ウェブサイトで使用するというセルフホストならぬセルフエンコーディングをやりました。
これもメモリ操作、またWasmへのコンパイルなどを勉強できて楽しかったです。もともとJSで書いてたプロジェクトだったのでWasm実装がJS版より早くなったときは(まあ当たり前なんですが。)嬉しかったですね。
Base8192
個人的には使ってないけど人気のリソース
ziglingsというレポジトリがあり、小さなプログラムを少し手直しして動くようにする、みたいな実際にコードを書きながら勉強していくような内容です。zigの学習で調べると結構評価が高いようなので今度やってみたいと思います。
https://codeberg.org/ziglings/exercises/#ziglings
zigをおすすめできる人
最後にこんな人はzigがおすすめ、というのを書いていきます。
- 低レベル・低レイヤに興味があるけど学習コストをあまり払いたくない人
- 英語を読むのが苦にならない人
- Bun・Ghostty(ともちろんzig自体)などのzigで書かれたプロダクトに貢献したい人
シンプルな哲学に惹かれる人にはおすすめです!
zigを学ぶのを避けた方がいい理由
個人的にzigは良い言語ですが、学ぶデメリットももちろんあります。
以下のような感じでしょうか。
- 破壊的変更がある(zigはまだver0.15)
- マイナーで情報が少ない
- 基本的に英語しか情報がない
- メモリ安全である必要がある
まだ正式にVersion 1に達していないというので使用を避ける人が多い印象ですね。
とはいえ破壊的変更も日常的に起きるわけではないですし、Bunなどはプロダクション運用されているので個人で使うぶんには全然問題ないくらいには安定していると思います。
英語しか情報がないのは仕方ないですが、プログラミングをする上で英語の知識はとても大事なのでぜひ勉強しましょう。私もできるだけzig入門したい人向けの記事を書いていければいいと思っています。
zigはメモリ安全という点においてはRustのように完全に安全であることを目指していません。すでにRustがある現代で、メモリ安全ではない言語を使うことは賛否両論あるみたいですが、zigには安全性を高める仕組みが用意されているためCより安全なコードが書けます。
Rustのような学習コストを要せず、かつCのように危険なコードを書きがちというわけでもなく、zigはちょうどいいバランスだと思います。
まとめ
TerraformやGhosttyで有名なプログラマ、ミッチェル橋本がなぜGhosttyを書くのにzigを使ったのか、またはなぜRustや他の言語ではないのかという質問に対して、zigが好きだから、というド直球な答えをしていました。(出典:https://mitchellh.com/writing/ghostty-and-useful-zig-patterns)なんか言語選択って真面目にやらなきゃいけないイメージですけど、こういう好きだから選ぶというのもいいですよね。
個人的にはしばらくzigでプログラミングをしてみて、よりコンピュータに近い、特にガベージコレクタのお世話にならないコードを書くのは楽しいなと感じました。
この記事が少しでもzigに興味がある人の助けになれば幸いです。
それじゃ今日はこの辺で。
PR
エンジニアならドメインのひとつやふたつ、持っておきたいですよね。ドメイン取得にはお名前ドットコムがおすすめです。
関連記事
こちらの記事もおすすめです。