Janet 编程语言:用于脚本和嵌入的现代 Lisp

Janet 是一种现代 Lisp 方言,侧重于简洁、可嵌入性以及易于分发。它被设计为一种具有一等函数、词法块作用域的命令式语言,核心仅包含八条主要指令:dodefvarsetifwhilebreakfn

本地分发与二进制编译

Janet 允许开发者将程序编译为本地可执行文件,并静态链接 Janet 运行时。此过程通过将 Janet 代码编译为字节码,将该字节码写入初始化 Janet 运行时的 .c 文件,然后使用系统 C 编译器编译该 C 文件来实现。

  • Zero-dependency binaries: 程序可以共享,而无需终端用户安装 Janet 运行时或项目依赖。
  • Small footprint: 一个简单的 “hello world” 二进制文件(Janet 1.27.0 在 aarch64 macOS 上)体积不足一兆字节(约 784K),包括垃圾回收器和字节码编译器。
  • Runtime evaluation: 由于字节码编译器已包含在二进制文件中,程序可以在运行时求值 Janet 代码。

使用 PEG 的高级文本解析

Janet 用解析表达式文法(Parsing Expression Grammars,PEG)取代传统的正则表达式。PEG 是结构化、可组合的一等解析器,相较于正则表达式有以下优势:

  • Predictability: 它们更可预测且不以行为单位,能够无缝解析多行文本。
  • Versatility: PEG 可以解析非正则语言,如 HTML 和 JSON,以及包含任意空字节的二进制文件格式。

虽然有些开发者认为 PEG 替代方案不是可交换的,这可能会使调试变得复杂,但它们仍然是处理复杂文本的强大替代方案,胜过标准正则表达式。

嵌入与脚本接口

Janet 被设计为一个小型 C 库,使其成为在更大应用程序中嵌入脚本接口的 Lua 可行替代方案。将 Janet 运行时链接到 C 项目中,开发者可以使用普通的 C 函数操作 Janet 值。

除了传统的软件嵌入,Janet 还能嵌入到网站中,以自定义可编程 DSL 创建静态站点。此外,该语言内置网络功能,这在其他小型解释器中常常缺失。

元编程与编译时执行

Janet 通过宏以及一种独特的编译时状态保留方式,提供强大的元编程能力。

Janet 宏允许开发者通过操作抽象语法树(AST)编写生成其他代码的代码。为保持引用透明性,Janet 允许在宏中对字面函数进行解除引用(unquoting)。

编译时到运行时的值传递

Janet 最独特的特性之一是能够在编译期间将程序状态序列化到磁盘。当程序被编译时,Janet 会执行所有顶层指令并对状态进行完整快照。该快照保留:

  • Shared references: 可变值在恢复快照后仍保持可变。
  • Generators: 生成器记住其精确的指令状态。
  • Closures: 闭包在编译时/运行时边界之间得以保留。

这使得高性能优化成为可能,例如预先计算复杂数据(如网格细分)或在编译时读取文件以嵌入资源。

语言设计与语法

Janet 偏离 Lisp 传统,以更符合现代开发者的使用习惯:

  • Naming Conventions: 它取代了传统 Lisp 术语;例如,CAR 对应 firstPROGN 对应 doLAMBDA 对应 fnSETQ 对应 set(尽管有些资料指出 def 用于不可变绑定)。
  • Collection Types: 语言提供可变和不可变集合。不可变集合使用值语义,而可变集合(前缀为 @)使用引用语义。
  • Syntactic Sugar: Janet 包含匿名函数的简写(|(+ 1 $))、用于参数展开的 “splats”(;args),以及使用反引号的灵活字符串字面量,以避免转义序列。

社区洞察与权衡

开发者之间的讨论突出了使用 Janet 时的若干实际考虑因素:

  • Performance: 用户报告启动速度非常快,有用户通过 hyperfine 记录到 1.4ms 的启动时间,使其成为系统脚本中 dash(1ms)的有竞争力的替代方案。
  • Shell Integration: sh 第三方库提供用于管道和重定向的 DSL(例如 ($ find . -name *.janet | say)),一些用户声称这使得 Janet 成为 Bash、Python 或 Awk 在较长系统脚本中的可行替代品。
  • Critiques: 有用户指出缺乏包管理的版本控制以及高级库(如复杂的 HTTP 路由)的稀缺。另一些用户则倾向于静态类型,或不喜欢 Lisp 方言中普遍使用的括号。

Sources