言語リファレンス
このページは Jsonnet を詳細に解説します。 ここではこの言語への基本的な知識があることを前提とするので、あなたが Jsonnet を使い始めたばかりであれば、まず チュートリアル をご覧ください。 一方で、この言語についての 100% をカバーするつもりはありません。 より完全で正確な記述を必要とする場合は、 言語仕様 および 標準ライブラリのドキュメント をご覧ください。
(訳注) この日本語訳のライセンス等については最下部の翻訳者によるメモ をご覧ください。
Jsonnet プロジェクトのスコープ
Jsonnet は複雑なシステムの設定を主たる目的として設計されています。 標準的なユースケースは相互認識のない複数のサービスを統合する、というものです。 それぞれのサービスに対する設定を独立して書くと、大量の重複を抱えることになり、メンテナンスが困難になりがちです。 Jsonnet を使うことで、あなたはあなたの思い通りに設定を指定でき、個々のサービスを全てプログラム的にセットアップできるようになります。
あなたがアプリケーションの作者であれば、Jsonnetを使えば専用の設定フォーマットを設計する作業から解放されるでしょう。 アプリケーションは単純に JSON またはその他の構造的フォーマットを読み込めばよく、 そのアプリケーションのユーザは自分の設定を Jsonnet (汎用設定言語)で記述できます。
Jsonnet は完全なプログラミング言語なので、任意のロジックを実装できます。 しかしながら、 Jsonnet では制限なしの入出力は許可されていません(環境からの独立(密封性)を参照)ので、 入出力への精密な制御が必要なケースにおいて実用的です。 設定以外にも、いくつかの応用が考えられます:
- 静的サイト生成
- 埋め込み言語
- JSONのアドホックな変換 -- これは、もしあなたが すでに Jsonnet を知っている場合に意味があります。 そうでなければ jq が間違いなくより良い選択肢です( JSonnet は短さや便利さよりも明瞭さを優先しており、これは書き捨ての利用には欠点となるでしょう)
- 教育 -- これは簡潔さと原理的アプローチによるものです。
それが何をするかではなく、何であるかを書く
Jsonnet は(オブジェクト指向を伴った)純粋関数的言語のひとつです。 Python や C++, Java, Go といった、ほぼ命令的な言語に慣れているプログラマは少しマインドセットを変える必要があります。
重用な違いは、Jsonnetでは物事を定義するにあたり、何を処理するかではなく、それが何なのかという観点で定義する、ということです。 map 関数の簡潔な実装を考えてみましょう:
local map(func, arr) =
if std.length(arr) == 0 then
[]
else
[func(arr[0])] + map(func, arr[1:])
;
// Example usage:
map(function(i) i * 2, [1, 2, 3, 4, 5])
これを声に出して読んでみると、次のようになります:
配列 arr
を関数 func
でマッピングした結果は次のいずれかである
1. 配列 arr
が空であれば、 空の配列である。
2. arr
が空でなければ、 arr
の最初の要素に関数 func
を適用した結果に続けて、残りの要素のマッピング結果をつないだものである。
伝統的な命令的言語による実装を読んだ場合は次のようになるでしょう。上と比べてください:
配列 arr
を関数 func
でマッピングするには:
1. 配列 arr
のコピー newArr
を作る
2. 配列 newArr
の各インデックス i
に対し
* newArr[i]
を、 newArr[i]
に関数 func
を適用したもので置き換える
3. newArr
を結果として返却する
Jsonnet の定義は数学の定義に似ています。 このようなスタイルの大きな利点は、状態と操作の順序について常に考慮しなければならない、ということから解放されることにあります。
式
Jsonnet のプログラムは 式 で構成されています。他のほとんどの言語にあるような、文や特殊なトップレベル定義は存在しません。例えば、インポートや条件分岐、関数、オブジェクトそしてローカル変数はすべて式です。
どんな種類の式も Jsonnet のプログラムになることができ、トップレベルオブジェクトは不要です。 例えば 2+2
や "foo"
または local bar = 21; bar * 2
は全て有効な Jsonnet プログラムです。
式を 評価 することで 値 を生成できます。この式の評価には、いかなる副作用もありません。
ある式がどの値に評価されるかは、環境 に依存します -- 環境とは、その式が参照する変数とその値のことです。 例えば式 x * x
は x
の値に依存しています。このような式は変数 x
が利用可能なコンテキストでのみ有効な式となります。 例えば local x = 2; x * x
や function(x) x * x
などです。
(訳注) 後者は原文では
function(x); x * x
となっていますが、エラーになるので上記が正しいと考えます。
Jsonnet の変数は レキシカルスコープ のルールに従います。 環境、およびその結果として現れる、ある式における変数の内容は、静的に決定されます:
local a = local x = 'a'; x;
local foo = local x = 'b'; a;
foo
レキシカルスコープの特別なケースが、次のような クロージャ の作成です:
local addNumber(number) = function(x) number + x;
local add2 = addNumber(2);
local add3 = addNumber(3);
[
add2(2),
add3(5)
]
値
Jsonnet の値の型は7つだけです:
- null (ヌル) -- 値が1つだけあります。つまり
null
です。 - boolean (真理値) –- 2つの値、
true
とfalse
があります。 - string (文字列) –- Unicode文字列(Unicodeコードポイントの並び)です。
- number (数値) –- IEEE754 64-bit 浮動小数点数です。
- function (関数), 引数として複数の値を取り、1つの値を返す純粋関数です。
- array (配列), 有限の長さを持つ、値の配列です。
- object (オブジェクト), 継承をサポートするJSONオブジェクトのスーパーセットです。
Jsonnet の値はすべて不変(イミュータブル)です。 あるオブジェクトのフィールドの値を変更することや、配列の要素を変更することはできません -- 可能なのは、必要な変更を適用して新しい値を作ることだけです。
std.type
を使って任意の値の型をチェックできます。
同値性(Equivalence) と 等価性(Equality)
a == b
が true
に評価されるとき、 a
と b
等しい(等価である、 equal)と言い、 a == b
が false
に評価されるとき、これらは等しくない(等価でない、unequal)、と言います。 値のペアには equal でも unequal でもない物があります。これは、関数の(したがって関数を含む配列およびオブジェクトも)等価性の検証ができないためです。
異なる型の値は決して等価になりません -- つまり暗黙のキャストは存在しません(例えば JavaScript とは違って)。
Jsonnet において a
と b
を区別することができない場合、 a
と b
が 同じ値である(the same value) あるいは 同値である(equivalent) と言います。 より形式的には、 f(a)
と f(b)
についてどちらか1つのみがエラーになるようなJsonnet関数 f
が存在しないのであれば、 a
と b
は同値です。
一般に、 同値な値は異なる表現形式を持ち、それはパフォーマンスに影響することがありますが、結果には影響しません。
同値な値は当然 uneqaul にはなりませんが、 equal な値であっても同値ではない場合があります(例えば { a: 1, b: 1}
と {a: 1, b: self.a}
)
(訳注)この2つのオブジェクトは
==
で比較するとtrue
へ評価されるので等価です。一方、後者だけがエラーになる次のような関数を定義できるので、同値ではありません。
local f(o) =
local c = o { a: o.a + 1 };
if c.a == c.b then
error 'Error!'
else
c;
Null
Null は Jsonnet において最もシンプルな型で、ただ一つの値 null
を持ちます。
null
に対する特別扱いは存在せず、他のものと同様、単なる値です。 特に、配列は null
の要素を持てますし、 オブジェクトは null
のフィールドを持てます。 null
は null
とのみ等価です。
真理値(Boolean)
真理値は2つの値を持ちます: true
と false
です。 これらの値のみが if
条件において利用可能です。
文字列(String)
Jsonnetにおける文字列はUnicodeコードポイントの並びです。
ほとんどの文脈では、ある文字列を単一のコードポイントの文字列の配列として扱えます(例えば std.legnth
や []
演算子がそのように動作します)。 比較(<
, <=
, >
, >=
) や等価性の検証(==
, !=
)も同様です -- どちらのケースにおいてもコードポイントは辞書順に比較されます。
文字列はコードポイントの配列に似ていますが、実際には同値ではありません。例えば std.type("foo") != std.type(["f", "o", "o"])
や "foo" != ["f", "o", "o"]
は真です)。
配列とは異なり、文字列は正格評価です。つまり、文字列を評価するにはその全ての内容が計算されなければなりません。
文字列はリテラルとして構築できますし、既存の文字列を切り取ることでも、既存の文字列を結合することでも、 Unicodeコードポイント数値の配列から変換することでも構築できます。
数値(Number)
Jsonnet の数値は IEEE754 で定義される 64-bit 浮動小数点数から nan (無限)と inf (非数)を除いたものです。 結果が無限や非数となるような演算はエラーになります。
整数は [-2^53, 2^53] の範囲において正確に表現できます。これは IEEE754 仕様によるものです。
関数(Function)
Jsonnetにおける関数は数学的な関数です。 関数は複数の引数と1つの本体の式を持ちます。ある関数の計算結果は、当該環境に引数を導入したうえで本体を評価した結果と同値です。
関数は function
リテラルで定義できます:
local func = function(x) x * 2;
func(21)
Jsonnet にはこれと同値な亜種であるシンタックスシュガーがあります:
local func(x) = x * 2;
func(21)
引数は関数が呼ばれる時点では評価 されません 。 引数は遅延評価で渡され、使われるときになってはじめて評価されます。 これにより、他の言語ではビルトインの機能かマクロが必要とされる、次のような論理演算の短絡評価が可能です:
local and3(a, b, c) = a && b && c;
and3(true, false, error "ここは決して評価されません")
error "メッセージ"
という式は与えられたメッセージを伴ってエラーを送出します。従って、この例では、 a && b
を計算する必要があるので a
および b
の値が評価されます。しかし、 a && b
は false
なので c
の値を評価する必要はありません。その結果エラーが送出されることはありません。
Jsonnetにおける関数は参照透過です。これは、任意の関数呼び出しがプログラムの意味を変えることなくその定義で置き換え可能である、ということです。 例えば次のコードを考えてみてください:
local pow2(n) = if n == 0 then 1 else 2 * pow2(n - 1);
pow2(17)
関数 pow2
の呼び出しはその定義によって次のように置き換え可能です:
local pow2(n) = if n == 0 then 1 else 2 * pow2(n - 1);
local n = 17;
if n == 0 then 1 else 2 * pow2(n - 1)
関数引数
関数の引数は必須引数またはオプショナル引数のどちらかです。 定義では必須引数もオプショナル引数も任意の順序で混在させられます。オプショナル引数にはデフォルト引数を指定しなければなりません。
ある関数を呼ぶとき、実引数は名前を指定することも、位置で指定することもできますが、名前指定の引数の前にすべての位置指定の引数が指定される必要があります。
例えば次のプログラムは有効です:
local foo(x, y=1) = x + y;
[
foo(1),
foo(1, 1),
foo(x=1, y=1),
foo(y=1, x=1),
foo(x=1),
]
オプショナル引数は常に名前指定で渡すことを推奨します(読みやすさのため)。 また、全ての引数が必須引数なのであれば、位置指定で渡すことを推奨します。なぜならその関数の作者は、おそらく引数名を安定的なインターフェースとはみなさないからです。
(注: 私達は、関数シグネチャにおいて名前指定のみまたは位置指定のみを明示する、後方互換性を壊すことのない方法を探しています。)
配列(Array)
Jsonnetにおける配列は、有限な長さを持つ、任意の値の並びです。 ある配列の中に、異なる型の値を混在させられます。配列の個々の要素は遅延評価されます。これは、配列を評価しても全ての要素が評価されるわけではないことを意味します。
local arr = [error "a", 2+2, error "b"];
arr[1]
上記の例では 2+2
は評価されますが、 配列の他の要素は評価されません。参考: Rationale for Lazy Semantics 。
Jsonnet では配列から独立したタプル型は存在しません。 他の言語においてタプルが自然であるような文脈においては、配列が利用されます。 例えばある関数から多値を返すような場合です。
配列を作成する最も単純な方法は配列リテラルです。これは [1, 2, "foo", 2+2]
のようなカンマで分けられただけの要素のリストです。
もっとも柔軟に配列を作成することができるのは std.makeArray(sz, func)
です。 この関数は構築する配列のサイズと、インデックス i をとり i 番目の要素を返すような関数を引数として取ります。 その他のすべての配列はこの関数で組み立てることができるでしょう。 実際には、より特殊化された関数を使うほうが大抵の場合は(常にではありませんが)より便利で、効率的なプログラムになるでしょう。
配列は +
演算子を使って連結できます。
配列 a
と b
は同じ長さで、かつ全てのインデックス i
について a[i] == b[i]
である場合に等価です。
配列同士の比較は辞書順です。つまり a[i] < b[i]
となる i
が存在し且つ j < i
であるすべての j
について a[j] == b[j]
であるとき、または a
が b
の(より短い)プレフィックスであるとき、 a
は b
よりも小さいことになります。
配列内包表記(Array Comprehensions)
Jsonnet は配列内包表記を提供します。これは、配列のマップ、フィルタ、直積の計算をするのにエレガントで使いやすい文法です。
最も単純なケースでは、内包表記は元の配列の要素1つひとつに対し1つの要素を生成します。 これは std.map
に似ています。
[x * x for x in std.range(1, 10)]
フィルタのための条件を追加することもできます -- if
コンポーネントを使います。 配列の要素は条件を満たす場合のみ生成されます。
[x for x in std.range(1, 10) if x % 3 == 0]
for
を2つ以上使った場合、 それぞれのループにおける値の組み合わせに対して1つの要素が生成されます。 これは元の配列の直積に対応します。
最初の for
が一番外側で、最後の for
が一番内側になります。次の例では x
のそれぞれの値に対し、 次の x
の値の処理に進む前に y の値が全て生成されます。
[
[x, y]
for x in std.range(1, 3)
for y in std.range(1, 3)
]
後続の for
コンポーネントでは、それより前の for
のコンポーネントに依存できます:
[
[x, y]
for x in std.range(1, 3)
for y in std.range(x, 3)
]
後続の for
で導入される変数は、それより前に導入された変数をシャドウすることがあります。
[
x
for x in std.range(1, 3)
for x in std.range(1, 3)
]
if
コンポーネントは自由に混在させられます。 条件を可能な限り手前の位置に置くことは、ほぼ全ての場合において意味があります -- これにより後続の for
コンポーネントにおける不要な繰り返しを避けられるのです。
[
[x, y]
for x in std.range(1, 10)
if x % 3 == 0
for y in std.range(1, 10)
if y % 2 == 0
]
配列内包表記に「魔法」のようなものはありません。内包表記は普通の関数を使うことによって達成可能なことを、より便利な文法で達成できるようにしています。特に、内包表記は常に std.flatMap
の連続へと「機械的に」変換可能です。 例えば次のプログラムは同値です:
[
[x * 2, y]
for x in [1, 2, 3, 4, 5]
for y in [1, 2, 3]
if x % 2 == 0
]
std.flatMap(
function(x) std.flatMap(
function(y) if x % 2 == 0 then [[x * 2, y]] else [],
[1, 2, 3]
),
[1, 2, 3, 4, 5]
)
オブジェクト(Object)
最も単純なケースでは、 Jsonnetオブジェクトは文字列キーから任意の値へのマッピングです。
{
"foo": 1,
"bar": {
"arr": [1, 2, 3],
"number": 10 + 7,
}
}
Jsonnetオブジェクトは .
と識別子をあわせて使うか、 []
に任意の式を組み合わせることでインデックス可能です。
local obj = {
"foo": 1,
"bar": {
"arr": [1, 2, 3],
"number": 10 + 7,
}
};
[
obj.foo,
obj["foo"],
obj["f" + "oo"]
]
継承
Jsonnet にクラスや宣言はありませんが、Jsonnet のオブジェクトはオブジェクト指向プログラミングの感覚で継承が可能です。 継承は +
演算によって任意の2つのオブジェクトに適用することで実現できます。 主流の言語における継承階層は静的なので、これは意外かもしれません。
単純なキーバーリューマッピングであるオブジェクトにとっては、継承は2番目のオブジェクトのフィールドで、それに対応する1番目のオブジェクトのフィールドを置き換えることと同じです。
{
a: 1,
b: 2,
}
+
{
a: 3
}
self
を使って同一オブジェクト内の別のフィールドを参照できます。オブジェクトを結合したときには、 super
を使って継承元のフィールドを参照できます。
local obj = {
name: "Alice",
greeting: "Hello, " + self.name,
};
[
obj,
obj + { name: "Bob" },
obj + { greeting: super.greeting + "!"},
obj + { name: "Bob", greeting: super.greeting + "!"},
]
一般に、Jsonnetオブジェクトを階層のスタック(積み重ね)だと考えると便利です。 ある階層は複数のフィールドから構成されます。 あるオブジェクトリテラルまたはオブジェクト内包表記は単一階層のオブジェクトです。オブジェクトの継承 A + B
は A
階層の上に B
階層を載せた新しいオブジェクトを作ります。
self
によるフィールドの参照は、当該フィールドを見つけるまでスタックの一番上からスタックの底へ向かって探すことに対応します。 super
による参照は、現在の階層のひとつ下から探し始めます。
継承により階層は他のオブジェクトに追加できるので、個々のフィールドは複数のオブジェクトの一部になることがあり、それぞれ異なる値を持ちます。 したがって、フィールドは「現在のオブジェクト」という文脈においてのみ評価できます。この文脈はオブジェクトが外部からインデックス付される場合に決定されます -- つまりある特定のオブジェクトのあるフィールドが要求されると、全ての self
や super
による参照は、その特定のオブジェクトの階層スタック内において決定されます。
関数をエミュレートする
関数はオブジェクトで簡単にエミューレートできます。これは良くないスタイルである一方、動的継承の強力さを説明します。
local add = {
params: {
a: error "please provide argument a",
b: error "please provide argument b",
},
result: self.params.a + self.params.b
};
(add + { params: { a: 1, b: 2} }).result
演算における性質
$D$, $E$, $F$ を任意のオブジェクトとします。 $\equiv$ は同値を意味するものとします。
- 結合性 は常に成り立ちます:
$(D + E) + F ≡ D + (E + F)$
- 同一性 は常に成り立ちます:
$D + \{ \} ≡ D$
$\{ \} + D ≡ D$
-
冪等性 は $D$ が
super
を含まなければ成り立ちます:
$D + D ≡ D$
-
可換性 は $D$ および $E$ が
super
を含まず且つ共通のフィールドを持たないとき成り立ちます:
$D + E ≡ E + D$
自己参照オブジェクト
オブジェクト指向プログラミングの機能を用いずとも、オブジェクトは自己参照的であり得ます。これは単に変数定義が再帰的であるためです:
local obj = {
name: "Alice",
greeting: "Hello, " + obj.name,
}; obj
このような obj
の参照は、 self
を使ったものとは 異なります。 ここで obj
を固定されたフィールドとその値を持つ特定のオブジェクトだとします。 すると obj.name
は固定的な値です。 一方で self
は値ではありませんので、「現在の」オブジェクトへの参照と self.name
は上書きされることがあります。
[
local obj = {
name: "Alice",
greeting: "Hello, " + obj.name + "!",
}; obj + {name: "Bob"},
{
name: "Alice",
greeting: "Hello, " + self.name + "!",
} + {name: "Bob"},
]
階層スタックのアナロジーに戻ると、 self
はスタックを意識しません -- つまり「現在のオブジェクト」内の検索を行います。 一方で obj
は特定の階層のスタックです -- つまり外部からの参照です。
これら双方の振る舞いは便利です。 現在定義されているとおりに当該オブジェクトのフィールドを参照するのか、それとも上書きを許すのかは、あなたが決めなければなりません。
可視性
Jsonnet オブジェクトには可視性という概念があります。これはマニフェステーション(顕在化、 manifestation, オブジェクトを表示すること)及び等価性の検証に影響します。 可視性は他の言語におけるプライベート/パブリックフィールドの概念とは異なります。
あるオブジェクトのフィールドの可視性は3種類存在します:
-
:
-- デフォルトであり、親オブジェクトの同名のフィールドが不可視で無い限り可視です。 -
::
-- 不可視です。 -
:::
-- 強制的な可視です。
例:
{
default: "foo",
default_then_hidden: "foo",
hidden:: "foo",
hidden_then_default:: "foo",
hidden_then_visible:: "foo",
visible::: "foo",
visible_then_hidden::: "foo",
}
+
{
default_then_hidden:: "foo",
hidden_then_default: "foo",
hidden_then_visible::: "foo",
visible_then_hidden:: "foo",
}
フィールドの値はその可視性の決定とは無関係です。
標準ライブラリ関数の std.objectHas
および std.objectHasAll
を使ってフィールドの可視性を検証できます。 最初の関数はあるオブジェクトの指定されたフィールドが可視であるかを検証します。2番目の関数はあるオブジェクトがそのフィールドを持っているかどうかを可視性に関係なく検証します。
入れ子状のフィールド継承
デフォルトでは入れ子になったオブジェクトは上書きされると完全に置き換えられます。例えば
{
nested_object: {
field_of_the_nested_object: "will disappear"
},
not_touched: "still there",
}
+
{
nested_object: {
new_field: "will be there"
}
}
これは次のようになります
{
"nested_object": {
"new_field": "will be there"
},
"not_touched": "still there"
}
継承における右側のオブジェクト内でフィールドセパレータとして +:
または +::
, +:::
を使うことで、置き換える代わりに新しいフィールドに古いフィールドを継承させることができます。
例:
{
a: ['a'],
b: ['c'],
c: { a: 'a', c: 'c' },
} +
{
a: ['a2'],
b+: ['c2'],
c+: { a: 'a2', b: 'b2' },
d+: { d: 'd' },
}
結果:
{
"a": [
"a2"
],
"b": [
"c",
"c2"
],
"c": {
"a": "a2",
"b": "b2",
"c": "c"
},
"d": {
"d": "d"
}
}
これらのフィールドセパレータ +:
+::
+:::
は、入れ子になったオブジェクト(この場合継承になります)や、配列(この場合連結になります)に対して適切です。コロンの数はそのフィールドの可視性を決定します。
対応するフィールドが左側のオブジェクトにないときに +:
を使ってもエラーにはなりません。 そのような場合には右側のオブジェクトのフィールドが直接使用されます。 例えば、 { foo +: { bar: "baz" } }
と {} + { foo +: { bar: "baz" } }
は両方とも { foo: { bar: buz } }
に評価されます。
全ての場合において、これらのフィールドセパレータは単なるシンタックスシュガーであり、 super
を使って同じ結果を達成できます。 より正確には { a +: b }
は { a : if "a" in super then super.a + b else b }
と同値です。(そして +::
および +:::
も同様です)
オブジェクトの等価性
2つのオブジェクトは、対応する可視フィールドが等価であればオブジェクト同士は等価です。 不可視フィールドは無視されるので、等価性を確認するときにいわゆる「ヘルパー」部分を無視することができます。
等価性を検証できないような可視フィールドを持ったオブジェクトについては、等価性を検証できません。例えば:
{
a: function() 42
} == {
a: function() 42
}
普通、等価性を検証できないフィールドは不可視にするべきです。なぜならカスタムのマニフェステーション手法を用いない限り、マニフェステーションできなくなってしまうからです。
オブジェクトのローカル変数
オブジェクト内に local
を宣言することが可能です。 そのローカル変数はすべてのフィールドで利用可能になります。
{
local foo = 1,
aaa: foo,
bbb: foo,
}
オブジェクトローカル変数はセミコロンではなく(フィールドと同じく)コンマで終わります。 オブジェクトローカル変数は独立した式ではなく、オブジェクトリテラル式の一部です。 形式的には、個々のフィールドにてそれぞれローカル変数を宣言することと等しいのですが、Jsonnetインタプリタはこれをより効率的な形で実装している可能性が高いです。
オブジェクトローカル変数は self
と super
にアクセス可能です -- つまりこれらは「オブジェクト内部」にあります。 結果として、 オブジェクトローカル変数は式によるフィールド名においては利用できません 。 これは一般に式によるフィールド名が、すでに作成済みのオブジェクト、つまりそのフィールド名が既知であることを要求するオブジェクトに依存しているからです。
式によるフィールド名
Jsonnet オブジェクトのフィールド名は、任意の文字列でよいですし、オブジェクト作成時に動的に計算することもできます。
{
a: 1,
"a a": 2,
"ąę": 3,
["aaa" + "bbb"]: 4,
}
あるオブジェクトが評価されると、全てのフィールド名が評価されます。 これは、フィールド名を決定する式ではオブジェクトローカル変数、 self
, super
を参照することができないということです。 言い換えるとこの式のスコープは「オブジェクト外」なのです。
環境からの独立(密封性)
Jsonnetのプログラムは純粋な計算です。これはつまり副作用がなく、明示的に渡された値にのみ依存します。 特に、Jsonnet が実行されるシステムの状態(OS, 環境変数、ファイルシステム、……)から独立しています。 このセマンティクスは ほぼ完全に数学的に定義される ものであり、わずかな例外は、Jsonnet が定評のあるポータブルな標準(IEEE754やUnicode)に依存していることです。
環境からデータを渡すことも、明示的にであればですが、可能です。 これにはJsonnetによって提供される抽象的機能を使うことで可能です。これには複数の利点があります:
- より少ない意外性 -- システムを変更しても、挙動が変化しません。 他の言語においては、プログラムの任意の部分が、さまざまなものに依存することがあります。 Jsonnet では明示的に渡されるものについてのみ心配すればよいです。
- 異なるマシン(例えば開発やCIのマシン)で実行するのがより簡単 -- プログラムには任意の値を渡せるので、どのようなシステムのどのような設定でも生成できます。
- 寿命が長い -- Jsonnet のプログラムは、他の技術が古くなるのに合わせて変更する必要がありません
- ポータビリティ -- どのようなプラットフォームでもJsonnetの実装を作るのは適度に簡単です
Jsonnet へデータを渡す
自己完結するプログラムを考える
下で説明する手法のいずれかを使う前に、完全に自分自身の設定だけで実行可能にならないか検討するのは良いことです。
このスタイルでは、設定は .jsonnet
および .libsonnet
ファイルの集合になります。 ひとつひとつの出力ファイルは各 .jsonnet
ファイルに対応し、すべての共有設定は .libsonnet
ファイルに書かれます。 生のデータは追加のファイルに置くことができ、これらは importstr
または importbin
を使ってインポートされます。 通常、すべてのコードとデータはリポジトリにコミットされます。 場合によっては生成された設定もリポジトリへチェックインされます。これにより意図しない変更を発見しやすくなります。
しかし、場合によってはこれは現実的ではありません。例えば生成される設定ファイルが秘密情報を含む必要があり、その秘密情報はコードとともにコミットしたくないとします。このような場合には秘密情報を外部から渡す必要があります。
トップレベル引数 (TLAs)
Jsonnet へデータを渡すための好ましい方法は、 トップレベル引数(Top-Level Arguments) を通す方法です。この仕組によりJsonnetプログラムを関数のように呼び出せます。
次のシンプルなプログラム add.jsonnet を考えます:
function(a, b) a + b
これは次のようにして呼びせます jsonnet add.jsonnet --tla-code a=1 --tla-code b=2
. そしてその評価結果は 3 になります。
関数へ評価される任意のプログラムは TLA を伴って呼べます。
jsonnet -e 'std.map' --tla-code 'func=function(x) x * x' --tla-code arr='[1, 2, 3]'
関数は値をパラメータ化する正規の方法であり、このような値をプログラムに渡す方法は、言語の残りの部分にうまく適合します。例えばTLAでの利用を想定したプログラムは他のJsonnetファイルからインポートされて通常の関数のように呼ばれることもあります:
local add = import 'add.jsonnet';
add(1, 2)
外部変数 (ExtVars)
外部変数を代わりに使うこともできます。 外部変数はインポートされる任意のライブラリ内を含む、プログラム内においてグローバルに利用でいます。 その名前とは逆に、 外部変数は local によって導入される変数において用いられている意味での変数ではありません。 外部変数は分離された名前空間を持ち、 std.extVar
関数を通してアクセスできます。
次のプログラム foo.jsonnet について考えましょう:
std.extVar("foo")
このプログラムは jsonnet foo.jsonnet --ext-str foo=bar
のように呼び出せます。
定義されていない外部変数を参照するとエラーになります。 ある外部変数が存在するかどうかを動的に検証する手段はありません。 Jsonnet プログラム内から外部変数をセットする方法もありません -- つまり、外部変数は実行前にセットされなければなりません。
外部変数はグローバルなので、 合成可能性の問題を生み出します。 2つのライブラリが同一の外部変数に依存しているけれど、それぞれ異なる意味で使っている、という状況を想像するのはたやすいことです。 このような理由から汎用ライブラリの作者には外部変数を使用しないよう強く推奨します。 あるライブラリにおいてグローバル設定が望ましいのであれば、そのライブラリを必要な設定をパラメータとしてとる関数にしてしまえばよいのです。 外部変数は「最終的な」構成 -- つまり個別の設定において、あるいは我の強いフレームワークにおいて、いずれにしろ構造を強制するようなものにおいてはあまり問題になりません。
翻訳者によるメモ
- これは、原文タイトル「Language Reference」の日本語訳です。
- 原文は https://jsonnet.org/ref/language.html にあります。
- 著作権者は Google Inc. で、本翻訳を含め ASLv2 または CC BY 2.5 でライセンスされます。
- (詳細) 原文を掲載しているサイトには下部に「Except as noted, this content is licensed under Creative Commons Attribution 2.5.」とありますが、ドキュメントのソースが管理されるJsonnet gitリポジトリ では当該リポジトリ自体は ASLv2 でライセンスされています。いずれにしろ、このメモにより利用条件はクリアしていると考えます。
- 適宜「訳注」を加えています。(本節以外の原文からの変更箇所は個々の訳注箇所で確認可能です)
- 翻訳対象となった原文のコミットは
7f0a595
(v0.20.0+)です