§2. 組み込み関数
builtins
Nix言語ではグローバルにbuiltins
という定数が設定されています。builtins
はAttrSetであり、attributeは関数になっています。基本的にbuiltins.<関数名>
という形で呼び出しますが、いくつかの頻出の関数はbuiltins
と同様にグローバルな名前空間で有効化されています。
本書で登場するNix式では、import
/throw
/derivation
関数以外は明示的にbuiltins.<関数名>
という形で記述します。
重要な組み込み関数
組み込み関数の中でも特に重要な関数です。これらは全てグローバルな名前空間で有効化されています。
import
import :: Path -> <式>
Nix言語でファイル分割を行うときに使用します。import
関数はPathを受け取り、Pathが指すNixファイルのNix式を返します。
試しにadd.nix
をmain.nix
からインポートしてみます。
./
├── add.nix
└── main.nix
{ a, b }: a + b
let
add = import ./add.nix;
in
add {
a = 1;
b = 2;
}
# 省略して以下の書き方をすることが多い
# import ./add.nix { a = 1; b = 2; }
$ nix eval --file ./main.nix
3
ファイルインポートが特別な構文ではなく、組み込み関数として用意されているのが面白いですね(後のセクションで述べますが内部的には特別な関数です)。
throw
throw :: String -> 虚無
throw
は例外を投げます。引数の文字列はエラーメッセージとして表示されます。
Nix言語には例外処理という概念がなく、例外が発生したらそこで評価が終了します。これは汎用的なプログラミング言語とは大きく異なるポイントです。Nix式の評価でエラーが発生するということは、ビルドのパラメーターが不正な場合など、ビルドを中止するべき状況であることがほとんどな上、サーバーのように継続的な処理も行わないため、このような設計になっています。言ってしまえばコンパイルエラーのような役割を担っています。
derivation
ビルドに関わる最重要関数です。1.4. Nix言語とderivationで詳しく解説します。
よく使う組み込み関数
readFile
readFile :: Path -> String
readFile
はファイルの内容を文字列として読み取ります。
$ echo "Hello, world!" > hello.txt
$ nix repl
nix-repl> builtins.readFile ./hello.txt
"Hello, world!"
toString
toString :: 任意の型 -> string
toString
は任意の値を文字列に変換する関数です。
プリミティブ型に適用すると…
# Number: 見た目通り
nix-repl> builtins.toString 1234
"1234"
# String: なにも変わらない
nix-repl> builtins.toString "Hello, world!"
"Hello, world!"
# Path: 絶対パスの文字列に変換
nix-repl> builtins.toString ./path/to/file
"/absolute/path/to/file"
# Boolean: trueなら"1"、falseなら空文字列
nix-repl> builtins.toString true
"1"
nil-repl> builtins.toString false
""
# Null: 空文字列
nix-repl> builtins.toString null
""
Listに適用すると…
# 文字列要素をスペース区切りで連結
nix-repl> builtins.toString ["Hello," "world!"]
"Hello, world!"
# 各要素にtoStringを適用して連結
nix-repl> builtins.toString [1 2 3]
"1 2 3"
# 異なる型の要素でも同じくtoStringを適用して連結
nix-repl> toString [1 true "String" null]
"1 1 String "
# 空リストは空文字列
nix-repl> builtins.toString []
""
AttrSetに適用すると…
基本的にtoString
をAttrSetに適用するとエラーになります。
ただし、__toString
またはoutPath
というattributeを持っていた場合は例外的に文字列に変換できます。
__toString
は関数でなければならず、引数にAttrSet自身を取ります。文字列変換時、toString
はこのAttrSet自体を__toString
の引数に渡し、その返り値にまたtoString
を適用した結果を返します。
let
attr = {
someAttr = 1234;
# toString適用時、selfにはattr自体が渡される
__toString = self: someAttr;
};
}
in
builtins.toString attr
# 評価結果: "1234"
outPath
はPath型でなければならず、toString
を適用するとそのPathを文字列に変換します。
builtins.toString {
outPath = /path/to/something;
}
# 評価結果: "/path/to/something"
なぜ、このような挙動になっているのかは1.4. Nix言語とderivationで詳しく解説します。
パース・シリアライズ系
DSLらしい特徴として、いくつかのデータ形式に対応したパース・シリアライズ関数が組み込まれています。
- fromJSON
- fromTOML
- toJSON
- toXML
# パース
fromJSON :: String -> AttrSet
fromTOML :: String -> AttrSet
# シリアライズ
toJSON :: AttrSet -> string
toXML :: AttrSet -> string
非純粋な関数
builtinsのいくつかの関数は純粋ではありません。これらの関数は副作用を持ち参照透過でないため、Nixの再現性を損ってしまいます。そのため、次の章で解説するNix言語のプロジェクト管理機能Flakesは非純粋な関数の使用を制限しており、明示的に--impure
オプションを付けて評価しないとエラーになります。REPLやFlakesで管理されていないNix式ではそのまま使用できます。
「非純粋」の定義
一部の読者は「前述のreadFile
なども非純粋なのでは?」と疑問に思ったかもしれません。一般的な純粋関数型言語ではあらゆるI/Oが副作用として明示的に扱われることを考えれば当然の疑問です。
Nix言語の純粋性は言語だけでなく、Nixストアやビルドシステムによって保証されています。ここは1.4. Nix言語とderivationに繋がる話なので詳しくは述べませんが、大域的には純粋に見えるようになっているのです。よって、本書での「非純粋」は「ビルドの再現性を損い得る」程度の意味で捉えた方がいいかもしれません。
筆者もこの表現をどうすべきか悩みましたが、最終的に公式リファレンスの「impure」という表現をそのまま使うことにしました。
currentSystem
currentSystem :: string
nix-repl> builtins.currentSystem
"x86_64-linux"
現在Nix式を評価しているシステムのアーキテクチャを文字列で返します。
getEnv
getEnv :: string -> string
nix-repl> builtins.getEnv "EDITOR"
"nvim"
getEnv
は環境変数の値を取得します。環境変数が存在しない場合は空文字列を返します。Flakes管理下のNix式でgetEnv
を使い、--impure
オプションを付けないで評価した場合は、常に空文字列を返します。
Fetcher
Nix言語においてインターネットからリソースを取得する関数はFetcherと呼ばれています。一見これも非純粋な関数に見えますが、Nixにはハッシュ関数を用いて冪等性を保ちながらインターネットアクセスを行う仕組みが用意されており、fetcherはそれを利用します。詳細は3.2. Fetcherで解説します。
その他の組み込み関数
文字列操作やリスト操作、型判定など、他にも多くの組み込み関数があります。以降、新しい組み込み関数が登場したら都度解説します。
詳細は公式リファレンスを参照してください。
【余談】非純粋な関数で遊ぶ
currentTime
という非純粋な組み込み関数があります。この関数は現在のUNIX時間を返します。
currentTime :: integer
nix-repl> builtins.currentTime
1722052322 # 日本時間で2024/07/27 12:52:02
もちろん、ビルドで現在時刻を取得するなど言語道断なので、こんな関数を使う機会は一切ありません。
……が、筆者がはりきってcurrentTime
を利用した関数を実装してしまったのでよかったら見てやってください。
today :: Number -> { year, month, day }
today.nix
うるう年を考慮してUNIX時間を年月日に変換する関数です。
Nix言語には剰余演算子がないので、can_divide
という関数を自分で定義して割り切れるかどうかを判定しています。builtins.elemAt
はListの指定したインデックスの要素を取得する関数です。
# Number -> { year, month, day }
unix_time:
let
base_year = 1970;
seconds_in_day = 86400;
total_days = unix_time / seconds_in_day;
can_divide = dividend: divisor: (dividend / divisor) * divisor == dividend;
is_leap_year =
year:
if can_divide year 400 then
true
else if can_divide year 100 then
false
else
can_divide year 4;
calc_year =
{ year, days }:
let
days_in_year = 365 + (if (is_leap_year year) then 1 else 0);
in
if days < days_in_year then
{ inherit year days; }
else
calc_year {
year = year + 1;
days = days - days_in_year;
};
calc_month =
{
year,
month,
days,
}:
let
days_per_month = [
31
(if (is_leap_year year) then 29 else 28)
31
30
31
30
31
31
30
31
30
31
];
days_in_month = builtins.elemAt days_per_month (month - 1);
in
if days < days_in_month then
{
inherit year month;
days = days + 1;
}
else
calc_month {
inherit year;
month = month + 1;
days = days - days_in_month;
};
year_and_remaining_days = calc_year {
year = base_year;
days = total_days;
};
month_and_day = calc_month {
year = year_and_remaining_days.year;
month = 1;
days = year_and_remaining_days.days;
};
in
{
year = year_and_remaining_days.year;
month = month_and_day.month;
day = month_and_day.days;
}
today
に9時間分の補正をかけたUNIX時間を渡すと、現在の年月日が返ってきます。
nix-repl> today = import ./today.nix
# 日本時間に直すために9時間分の補正をかける
nix-repl> jst_bias = 9 * 60 * 60
# 2024-08-16に評価した
nix-repl> today (builtins.currentTime + jst_bias)
{ day = 16; month = 8; year = 2024; }
汎用的な言語なら必須の関数ですが、Nix言語では無用の長物です。
このような実用性皆無のNix式の発展例として、Nix言語で無理矢理乱数生成を行うトンデモ関数を実装している方[1]がいます。
READMEにはこう記されています。
Impure, unreproducible, and indeterministic
「非純粋、再現不可能、不確定」