
编程语言模板引擎CLI【免费下载链接】jsonnetJsonnet - The data templating language项目地址https://gitcode.com/gh_mirrors/js/jsonnet点击查看免费下载本篇基于仓库内 doc/ref/language.html.md 语言参考文档编写面向已经通过 入门教程 掌握了 Jsonnet 基础语法、需要系统理解语言语义的读者。文章完整覆盖语言参考中的全部核心主题——声明式纯函数模型、七种值类型、惰性求值、对象继承与可见性、环境的独立性Hermeticity以及向程序注入外部数据的两种机制Top-Level Arguments 与 External Variables并逐一结合本仓库的 标准库实现、命令行解释器源码 和真实示例examples/top-level-tla.jsonnet做源码级佐证。读完本文你将具备独立阅读 语言规范 和 标准库文档、并能基于 TLA/ExtVar 写出可参数化、可复用的生产级 Jsonnet 配置的能力。Jsonnet 项目定位为复杂系统配置而生Jsonnet 被设计用于配置复杂系统。其典型应用场景是整合多个互不知晓的服务如果为每个服务独立编写配置会造成大量重复且难以维护。Jsonnet 允许你按自己的术语描述配置并以编程方式统一生成所有独立服务的配置见 doc/ref/language.html.md。如果你是应用程序作者Jsonnet 可以将你从设计自定义配置格式中解放出来应用只需消费 JSON 或其他结构化格式用户则可以用 Jsonnet一种通用配置语言来描述配置。作为一门完整的编程语言Jsonnet 可实现任意逻辑但它不允许不受限制的 IO详见下文环境的独立性一节因此特别适合需要精确控制输入输出的场景。除配置外Jsonnet 还有几个潜在应用方向原文档静态网站生成嵌入式表达式语言临时 JSON 转换——若你已熟悉 Jsonnet 可用它完成否则 jq 通常是更合适的选择Jsonnet 优先保证清晰性而非简洁顺手这对一次性使用反而是劣势教学——因其简洁且设计有原则。声明式思维定义是什么而非做什么Jsonnet 是纯函数式语言附带面向对象特性。习惯于 Python、C、Java、Go 等命令式语言的程序员需要转换思维方式你用事物是什么来定义事物而不是事物做什么。原文档给出了一个递归实现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])用自然语言朗读这段定义得到的是将函数func映射到数组arr的结果要么是空数组若arr为空将func应用于arr首元素的结果后接对剩余元素递归映射的结果若arr非空。对比命令式版本创建arr的副本newArr对每个索引i把newArr[i]替换为func(newArr[i])返回newArr。 Jsonnet 的定义方式与数学定义类似其巨大优势是你无需时刻操心状态与操作顺序。表达式一切皆为表达式Jsonnet 程序完全由表达式构成原文档没有其他语言中常见的语句或特殊顶层声明。导入、条件分支、函数、对象、局部绑定local——全都是表达式。任何表达式都可以作为完整的 Jsonnet 程序不要求顶层必须是对象。例如22、foo、local bar 21; bar * 2都是合法程序。求值无副作用表达式被求值后产生一个值求值过程没有任何副作用见 原文档。求值结果依赖环境表达式引用的变量取值构成其求值环境。例如x * x依赖于x的值只在local x 2; 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) ]add2与add3各自捕获了number的不同绑定即使定义时的词法环境相同。值系统七种类型一览Jsonnet 只有七种值类型原文档类型说明null唯一取值即nullbooleantrue和false两个取值stringUnicode 字符串Unicode 码点序列numberIEEE754 64 位浮点数function纯函数接受值作为参数并返回值array有限长度的值数组objectJSON 对象的超集支持继承所有 Jsonnet 值都是不可变的你无法修改某个对象字段或数组元素的值只能创建一个应用了所需修改的新值。可用std.type查询任意值的类型std.type本身及std.isString、std.isNumber、std.isObject等类型谓词均定义于 stdlib/std.jsonnet。相等与等价相等equala b求值为true。某些值对既不能说相等也不能说不相等——因为函数无法比较相等性因此包含函数的数组与对象亦然原文档。跨类型永不相等不同类型之间不存在隐式类型转换这与 JavaScript 不同。等价equivalent若在 Jsonnet 中无法以任何方式区分两个值则称它们等价。形式化地说不存在一个 Jsonnet 函数f使得f(a)与f(b)恰有一个报错原文档。等价的值可能有不同表示这会带来性能差异但不会影响结果。等价的值当然不可能不相等但相等的值未必等价——例如{ a: 1, b: 1 }与{ a: 1, b: self.a }相等但不等价。null 与 booleannull是最简单的类型只有一个值。它没有任何特殊处理与普通值无异数组可以含 null 元素对象可以有 null 字段null 只与自身相等。boolean只有true/false两个值它们是唯一能用于if条件的值原文档。string字符串是 Unicode码点序列。多数上下文中字符串可被当作单码点字符串数组处理——std.length与[]索引运算符均如此比较运算、、、与相等判断、!也遵循此模式即逐码点字典序比较。但字符串并不等价于码点数组std.type(foo) ! std.type([f, o, o])且foo ! [f, o, o]。与数组不同字符串是**严格strict**的求值一个字符串要求计算出它的全部内容原文档。字符串可通过字面量、已有字符串的切片、字符串拼接或从 Unicode 码点数字数组转换而来。numberJsonnet 数字是 IEEE754 定义的64 位浮点数但排除了nan和inf产生无穷或 NaN 的运算都是错误原文档。仓库测试套件中的 error.overflow.jsonnet、error.overflow2.jsonnet、error.overflow3.jsonnet 即用于验证这类溢出/非法浮点行为的报错路径。整数安全表示范围为[-2^53 1, 2^53 - 1]——这是 IEEE754 双精度格式的直接推论安全整数必须可被精确表示且不能由其他整数取整得到与 JavaScript 的Number.MAX_SAFE_INTEGER等定义一致。当前 C 与 Go 实现还将二进制位运算、、、|、^的参数限制在安全整数范围内原文档。对应的 safe_integer_conversion.jsonnet 测试覆盖了安全整数转换边界。function函数是数学意义上的函数有参数与函数体调用结果等价于把实参引入环境后求值函数体原文档。可以用函数字面量定义local func function(x) x * 2; func(21)也有等价的语法糖省略function关键字local func(x) x * 2; func(21)实参是惰性传递的函数被调用时实参并不立即求值而是用到时才求值。这允许表达其他语言需要内置特性或宏才能做到的事例如短路布尔运算local and3(a, b, c) a b c; and3(true, false, error this one is never evaluated)error message抛出携带消息字符串的错误。上例中a、b的值因计算a b而必须被求值但a b已是falsec无需求值错误永远不会被抛出。函数是**引用透明referentially transparent**的任何函数调用都可以被其定义替换而不改变程序含义原文档local pow2(n) if n 0 then 1 else 2 * pow2(n - 1); pow2(17)等价地写作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, y1) x y; [ foo(1), foo(1, 1), foo(x1, y1), foo(y1, x1), foo(x1), ]最佳实践可选参数总是以命名方式传递利于可读性当所有参数都是必需参数时推荐按位置传递——因为函数作者可能并未将参数名视为稳定接口的一部分。原文档还附注项目正在探索在不破坏向后兼容的前提下提供显式的仅命名或仅位置参数声明方式。array数组是任意值的有限序列可以混合不同类型。数组元素是惰性的求值数组不会立即求值全部元素原文档local arr [error a, 22, error b]; arr[1]上例只求值22其余元素的表达式不求值。设计动机参见 惰性语义的合理性。Jsonnet没有独立的元组类型其他语言习惯用元组的场景例如从函数返回多个值在 Jsonnet 中使用数组。创建数组最简单的方式是数组字面量逗号分隔的元素列表如[1, 2, foo, 2 2]。最灵活的方式是std.makeArray(sz, func)它接收目标长度和一个给定索引i返回第i个元素的函数。从原理上说所有数组功能都可以用std.makeArray构建实际编程中使用更专门的函数通常但不总是更顺手、也更高效。std.range即基于std.makeArray实现见 stdlib/std.jsonnetrange(from, to):: std.makeArray(to - from 1, function(i) i from),数组可用运算符拼接。数组a、b相等当且仅当长度相等且对所有索引i有a[i] b[i]。数组比较是字典序的若存在i使a[i] b[i]且对所有j i有a[j] b[j]或a是b的更短前缀则a小于b。数组推导式数组推导式为映射、过滤和笛卡尔积提供了优雅简洁的语法原文档。最简单的推导式为源数组的每个元素生成一个元素等价于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组件生成每个循环取值组合对应的元素即源数组的笛卡尔积。第一个for最外层最后一个最内层——下例中对每个x先穷尽所有y才进入下一个x[ [x, y] for x in std.range(1, 3) for y in std.range(1, 3) ]后续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组件可以自由混排。几乎总应尽早放置条件以避免后续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与简单条件表达式std.flatMap定义于 stdlib/std.jsonnet。例如下面两个程序完全等价[ [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, } }对象可以用.加标识符索引或用[]加任意表达式索引local obj { foo: 1, bar: { arr: [1, 2, 3], number: 10 7, } }; [ obj.foo, obj[foo], obj[f oo] ]对象继承Jsonnet 对象在 OOP 意义上支持继承尽管没有类或声明。继承通过可作用于任意两个对象的运算符实现——这与主流语言静态的继承层级形成对比原文档{ 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 !}, ]理解对象的最佳模型是一摞层layers层由字段组成。对象字面量或对象推导式是单层对象继承AB生成一个新对象其层序为B 层叠在 A 层之上原文档。通过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为任意对象≡表示等价原文档结合律恒成立(D E) F ≡ D (E F)单位元恒成立D { } ≡ D{ } D ≡ D幂等律在D不含super时成立D D ≡ D交换律在D、E均不含super且没有公共字段时成立D E ≡ E D自引用对象即便不使用 OOP 特性对象也可以自引用——因为变量定义本身可以递归原文档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则是一摞具体的层——来自外部的引用。两种行为各有用途你需要决策是引用当前定义的字段还是允许覆盖的字段。字段可见性VisibilitiesJsonnet 对象的可见性影响具体化manifest即打印输出对象与相等性检查与主流语言的 private/public 概念无关原文档。字段有三种可见性分隔符含义:默认父对象同名字段隐藏时本字段才隐藏否则可见::隐藏:::强制可见示例{ 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检查可见性前者检查对象是否存在指定名字的可见字段后者无论可见性如何都检查是否存在。两个函数定义于 stdlib/std.jsonnetobjectHas(o, f):: std.objectHasEx(o, f, false), objectHasAll(o, f):: std.objectHasEx(o, f, true),嵌套字段继承默认情况下嵌套对象被覆盖时会被整体替换。例如原文档{ 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: baz } }。这些分隔符本质是语法糖同样的结果用super也能实现。精确地说{ a : b }等价于{ a: if a in super then super.a b else b }::与:::同理。对象相等两个对象相等当且仅当它们的可见字段分别相等隐藏字段被忽略——这让对象中辅助性的部分不会干扰相等性判断原文档{ a: function() 42 } { a: function() 42 }上例不允许不可判等。实践建议无法判等例如函数的字段通常应设为隐藏因为它们无法被具体化除非使用自定义的具体化方法。对象局部变量Object Locals可以在对象内部声明local它对所有字段可见原文档{ local foo 1, aaa: foo, bbb: foo, }对象局部变量以逗号结尾字段是分号。它们不是独立表达式而是对象字面量表达式的一部分。形式上等价于在每个字段里分别声明该局部变量但解释器通常会实现得更高效。对象局部变量可以访问self与super——它们位于对象内部。由此推论对象局部变量在字段名表达式Field Name Expressions中不可用因为一般情况下它们依赖对象已创建完成而创建对象又要求字段名已知。对象断言Object Assertions对象可以包含断言——布尔表达式在任一字段被求值时被检查原文档local base { assert self.a 0 : a must be positive, a: self.x * self.y, }; base { x: 5, y: -5 }该表达式被具体化时断言将被检查本例中会失败a求值为 -25抛出带指定消息的错误。注意对象断言仅在对象的某个字段被求值时检查。由于 Jsonnet 是惰性语言若输出无需任何字段对象可能被定义但从未求值断言也不会触发。列出现有字段例如用std.objectFields或std.objectFieldsAll见 stdlib/std.jsonnet不会求值字段值。字段名表达式字段名可以是任意字符串并可在对象创建时动态计算原文档{ a: 1, a a: 2, ąę: 3, [aaa bbb]: 4, }对象被求值时所有字段名都会被求值。这意味着字段名不能引用对象局部变量、self或super——它们的作用域在对象外部。条件字段若字段名求值为null该字段将被丢弃、不进入结果对象。下面的代码求值为空对象原文档{ [if 1 2 then foo]: bar, }仓库示例 examples/top-level-tla.jsonnet 就运用了这一特性根据brunch参数决定是否生成某个鸡尾酒条目——这是条件字段 顶层函数参数结合的真实用法。环境的独立性HermeticityJsonnet 程序是纯计算无副作用且只依赖显式传入的值。行为与运行系统的设置操作系统、环境变量、文件系统等无关。其语义几乎完全以数学术语定义见 语言规范仅有少数例外依赖成熟的通用标准如 IEEE754 与 Unicode。数据可以从环境传入但必须显式地通过 Jsonnet 提供的抽象进行。这带来多重好处原文档更少意外改动系统不会改变程序行为。其他语言中程序任意部分都可能依赖任何东西Jsonnet 中只需关心显式传入的内容。更易跨机器运行开发机或 CI可以向程序传入任意值从而在任何系统上生成任意配置。长寿性Jsonnet 程序无需随其他技术的过时而改动。可移植性为任意平台创建 Jsonnet 实现都相当容易。向 Jsonnet 传递外部数据优先考虑自包含程序在动用下述任何机制前值得先评估完全自包含方案是否可行原文档配置是一组.jsonnet与.libsonnet文件每个输出文件对应一个.jsonnet文件所有共享设置放在.libsonnet文件中原始数据可放入额外文件并用importstr或importbin导入。通常所有代码与数据都提交进仓库有时生成的配置也一并入库便于发现无意的改动。但有时这并不现实——例如生成的配置必须包含秘密secrets你不想把它们与代码一起提交此时就必须从外部传入。顶层参数TLAs向 Jsonnet 传参的首选方式是顶层参数Top-level ArgumentsTLA该机制允许像调用函数一样调用 Jsonnet 程序原文档。程序add.jsonnetfunction(a, b) a b可用jsonnet add.jsonnet --tla-code a1 --tla-code b2调用求值结果为3。任何求值为函数的程序都可以用 TLA 调用jsonnet -e std.map --tla-code funcfunction(x) x * x --tla-code arr[1, 2, 3]函数是参数化值的规范方式因此 TLA 与语言其余部分配合良好。例如为 TLA 编写的程序也可以被其他 Jsonnet 文件import后作为普通函数调用local add import add.jsonnet; add(1, 2)仓库中的 examples/top-level-tla.jsonnet 是一个更完整的实战样例顶层函数function(prefix, brunchfalse)注意brunch带默认值配合条件字段为不同的prefix生成鸡尾酒配置。其 golden 输出 展示了以prefixHappy Hour、brunchtrue调用时应得到的 JSON。外部变量ExtVars另一种方式是外部变量ExtVars。ExtVars 在程序中全局可用包括任何被导入的库。注意与它们的名字相反它们与local引入的变量并不相同——它们有独立的命名空间只能通过std.extVar函数访问std.extVar属于解释器内置的原生标准库函数见 stdlib/std.jsonnet 对原生函数的说明。程序foo.jsonnetstd.extVar(foo)可用jsonnet foo.jsonnet --ext-str foobar调用。引用未声明的外部变量是错误没有动态检查 ExtVar 是否存在的方法也无法在 Jsonnet 程序内部设置外部变量——必须在执行前设置。由于 ExtVars 是全局的它们会带来组合性问题很容易出现两个库依赖同名 ExtVar 却含义不同。因此强烈建议通用库的作者不要使用 ExtVars。若希望为库提供全局配置更好的做法是把库做成一个函数参数即为所需配置。这个问题在最终配置中不那么严重——无论是单个配置文件还是强约束结构的框架。仓库中的 examples/top-level-ext.jsonnet 展示了用std.extVar(prefix)与std.extVar(brunch)驱动字段名与条件字段的等价写法可对照 TLA 版本理解两者的差异。命令行选项汇总查阅 cmd/jsonnet.cpp 的用法说明外部变量与顶层参数的完整命令行选项如下类别选项说明外部变量字符串-V/--ext-str var[val]以字符串提供值省略val时从同名环境变量读取外部变量字符串--ext-str-file varfile从文件读取字符串外部变量代码--ext-code var[code]以 Jsonnet 代码提供值省略code时从环境变量读取外部变量代码--ext-code-file varfile从文件读取代码顶层参数字符串-A/--tla-str var[val]以字符串提供值省略val时从环境变量读取顶层参数字符串--tla-str-file varfile从文件读取字符串顶层参数代码--tla-code var[code]以 Jsonnet 代码提供值省略code时从环境变量读取顶层参数代码--tla-code-file varfile从文件读取代码补充说明同样出自 cmd/jsonnet.cpp环境变量JSONNET_PATH是冒号Windows 上为分号分隔的目录列表按相反顺序加在--jpath指定的目录之前即左侧优先。例如JSONNET_PATHa:b jsonnet -J c -J d等价于JSONNET_PATHd:c:a:b jsonnet也等价于jsonnet -J b -J a -J c -J d。filename可以是-表示从 stdin 读取。多字符选项可展开如-abc等价于-a -b -c--会抑制后续参数中的选项处理。由于文件名与 Jsonnet 程序可能以-开头建议对未知参数使用--如jsonnet -- $FILENAME。TLA 与 ExtVar 的取舍很清晰TLA 走函数参数通道与语言的作用域与组合模型一致可被import后复用ExtVar 是全局命名空间适合最终配置中的一次性注入如密钥但应避免在通用库中使用。总结与延伸阅读Jsonnet 语言参考的核心脉络可以概括为三条主线声明式纯函数模型——一切皆表达式、无副作用、词法作用域与引用透明配合惰性求值让配置程序具备可预测性与可移植性以对象为中心的 OO 特性——继承、self/super、可见性、:嵌套继承、对象断言与条件字段构成复用与组合配置的基石严格的环境隔离——程序只依赖显式传入的数据通过 TLA 或 ExtVar 与外界交互兼顾了纯函数安全与真实配置如密钥注入的灵活性。想深入钻研可以继续阅读Jsonnet 语言规范——完整、精确的语言定义标准库文档——std命名空间中全部函数含本文提及的std.type、std.makeArray、std.range、std.map、std.flatMap、std.objectFields、std.objectHas等惰性语义设计动机入门教程若尚未掌握基础语法仓库内的标准库实现 stdlib/std.jsonnet 与测试套件 test_suite可对照验证本文涉及的各项语义。赞分享编程语言模板引擎CLI【免费下载链接】jsonnetJsonnet - The data templating language项目地址https://gitcode.com/gh_mirrors/js/jsonnet点击查看免费下载相关推荐Jsonnet环境变量管理extVar与TLA参数传递技巧Jsonnet环境变量管理extVar与TLA参数传递技巧 在Jsonnet数据模板语言开发中动态参数传递是实现配置灵活性的核心需求。本文将系统讲解e编程语言模板引擎CLICPython 表达式语言参考精读从原子、推导式到运算符优先级的完整语法与求值语义CPython 表达式语言参考精读从原子、推导式到运算符优先级的完整语法与求值语义 本篇是 CPython 语言参考Language Reference中编程语言语言运行时解释器标准库Roc 表达式语言参考从取值、引用计数到编译期求值的完整解析Roc 表达式语言参考从取值、引用计数到编译期求值的完整解析 本文围绕 Roc 语言参考中的表达式Expressions章节展开系统梳理「什么是表达式」上一篇LongNetTransformer模型深度解析构建高效长序列处理的神经网络架构下一篇终极指南vscode-markdown-mermaid如何让Markdown预览支持Mermaid流程图创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考