概要
Zigを触り始めた人が、戸惑うポイントの一つがこの書き方だと思います。
const std = @import("std");
C/C++やRust、Go、Pythonなどを経験しているほどこの関数呼び出しや代入のような書き方に違和感はあるかと思います。
ここ記事では、言語設計の観点から解説しようとおもいます。
書くのは以下の通りです。
- Zigの
@importは何者なのか - なぜ変数代入の形を採用しているのか
- 他言語のimport構文と何が違うのか
- それが設計思想として何の意味があるのか
結論:Zigでは「モジュールは値」である
はじめに結論として、Zigでは、モジュールは名前空間オブジェクト(=値)として扱われる
よって、
const std = @import("std");
という「値を変数に代入する」形になっています。
@importは関数ではなく「コンパイル時組み込み関数」
見た目は関数呼び出しですが、@importは通常の関数ではありません。
-
@importはbuiltin function - 実行時ではなくコンパイル時に評価される
- 戻り値は「モジュール(名前空間)」
const math = @import("std").math;
このように、importした結果に対して普通にドットアクセスできます。
ここで重要なのは、Zigにとってimportは:
- 文(statement)ではない
- 構文レベルの特別扱いではない
という点です。
他言語のimport構文との比較
Zigの設計を理解するために、他言語がどうしているかを見てみましょう。
C / C++:プリプロセッサ
#include <stdio.h>
- 単にテキスト展開
- スコープの概念がない
Rust:専用構文としてのuse
use std::io;
- 構文として組み込まれている
- importは文であり、値ではない
- 再束縛や条件付きimportは制限される
Go:ファイル単位の宣言
import "fmt"
- importはファイルの先頭に限定
- 非常にシンプル
- その代わり柔軟性は低い
Python :実行時import
import math
- 実行時オブジェクトとして扱われる
- 動的importが可能
- その分、最適化や解析は難しい
Zigの設計原則
1. 構文を増やさない
Zigは「特別な構文」を極端に嫌います。
-
import文を新設しない - 既存の式・代入ルールで表現する
結果として、
const std = @import("std");
という形になります。
2. すべてが明示的
Zigでは、以下が代入によって明示されるようになってます
- 名前がどこから来たか
- どのスコープにあるか
const std = @import("std");
const mem = std.mem;
const ArrayList = std.ArrayList;
何がどこから来たのかコードを見ただけで分かります。
3. importもスコープに束縛される
関数スコープでのimport
fn processData() void {
const std = @import("std");
std.debug.print("Processing...\n", .{});
}
- 関数スコープでimport可
- 必要な場所でのみimportする設計が可
条件付きimport
const builtin = @import("builtin");
const impl = if (builtin.os.tag == .linux)
@import("linux_impl.zig")
else
@import("generic_impl.zig");
これは多くの言語では不可能、もしくは推奨されない書き方です。
Zigでは、importはcomptimeに評価される式なので、条件分岐の結果として異なるモジュールを読み込めます。
## メリット
1. コンパイル時評価との親和性
Zigはcomptimeを言語の中核に据えています。
- import
- 型
- 定数
がすべて同じ「値」として扱われるため、importだけが特別という状況が生まれません。
const MyType = struct {
value: i32,
};
const std = @import("std");
const my_module = @import("my_module.zig");
型定義もimportも、同じconstによる束縛です。一貫性があります。
2. メタプログラミングが自然
fn loadModule(comptime name: []const u8) type {
return @import(name);
}
const Impl = loadModule("impl_a.zig");
importが式であるからこそ、こうした書き方が可能になります。
実用例:プラットフォーム別実装の切り替え
const builtin = @import("builtin");
const Allocator = if (builtin.os.tag == .windows)
@import("allocator_windows.zig").Allocator
else if (builtin.os.tag == .linux)
@import("allocator_linux.zig").Allocator
else
@import("allocator_generic.zig").Allocator;
pub fn main() void {
var allocator = Allocator.init();
// ...
}
コンパイル時に適切な実装が選択され、使われない実装はバイナリに含まれません。
3. 言語仕様がシンプル
複数のバリエーションが不要となります。
- import文
- use文
- from import構文
- import as構文
パフォーマンスへの影響
コンパイル時評価の利点
importがcomptime式であることで
- 使われないコードは完全に削除される
条件付きimportで使われない分岐のコードはバイナリに含まれない - 最適化の機会が増える
- バイナリサイズの削減
Zigの設計
Zigのimport構文は、単なる好みの問題ではなく、一種の哲学が表れています。
- importを特別扱いしない
- モジュールを値として扱う
- コンパイル時評価を中心に据える
まとめ
他言語のimportに慣れていると、Zigの書き方は確かに異質ですが、zigの設計思想が出ており、非常に有用な書き方を反映しています。