☰
Learn X in Y Minutes:Paren 嵌入式 Lisp 方言入门指南
2026/10/7 15:05:22 网站建设 项目流程
  • 文档
  • 教程

【免费下载链接】learnxinyminutes-docs

Code documentation written as code! How novel and totally my idea!

项目地址:https://gitcode.com/gh_mirrors/le/learnxinyminutes-docs
点击查看免费下载

Paren 是一门以"嵌入式"为设计目标的 Lisp 方言,本文基于 de/paren.md(德语版)并对照仓库中的英文原版 paren.md、俄语版 ru/paren.md 与葡语版 pt-br/paren.md 整理而成。阅读本文后,你将掌握 Paren 的注释、原始类型、变量、列表、函数、控制流、变更与宏这七大类核心语法,并能直接上手编写可运行的 Paren 代码片段。部分示例借鉴自同仓库的 Racket 教程,阅读时可对照体会两者在 Lisp 家族中的异同。

Paren 是什么

Paren 是 Lisp 的一个方言,它被设计为一种嵌入式语言(embedded language)——也就是说,它的定位不是独立构建大型应用,而是嵌入到宿主程序中、作为可扩展脚本或配置/业务逻辑语言使用。与许多 Lisp 方言一样,Paren 的核心形态是S 表达式:代码即数据,一切运算都写作(f x y z ...)的形式,其中f是函数,x、y、z是操作数。

在 Learn X in Y Minutes 项目仓库中,Paren 教程遵循"以有效、带注释的代码讲解语言"的规范(见 README.md),并将同一份教程翻译为多语言版本:英文原版 paren.md、德语版 de/paren.md、俄语版 ru/paren.md、葡语版 pt-br/paren.md。各版本正文结构完全一致,仅在注释语言与个别示例措辞上有差异。

注释:分号与井号

Paren 的单行注释以分号;或井号#开头:

;;; Kommentare # Kommentare ;; Einzeilige Kommentare starten mit einem Semikolon oder einem Hashtag

德语原文特意演示了两种注释符号等价可用,这与 Racket(仅用;)等方言形成对照——Paren 对注释起始符更宽容,便于嵌入式场景下快速标注。

1. 原始数据类型与操作符

数字:int 与 double

Paren 的数字分为整数(int)与浮点数(double)两大类,字面量写法与 C 系语言接近,并支持科学计数法:

123 ; int 3.14 ; double 6.02e+23 ; double

类型之间可以显式转换,转换函数同时返回目标类型:

(int 3.14) ; => 3 : int (double 123) ; => 123 : double

注意(int 3.14)的结果是截断为 3,而非四舍五入。

函数应用与 quote

函数应用写作(f x y z ...),所有操作都以括号包裹的前缀形式表达。若想构造一段字面量数据(而不是立即求值),使用(quote ...)阻止其被求值:

(quote (+ 1 2)) ; => (+ 1 2)

(quote (+ 1 2))返回的是一段未被求值的列表(+ 1 2),这正是"代码即数据"的体现——列表既能被求值,也能被当作数据传递,为后续宏机制埋下伏笔。

算术运算

(+ 1 1) ; => 2 (- 8 1) ; => 7 (* 10 2) ; => 20 (^ 2 3) ; => 8 (/ 5 2) ; => 2 (% 5 2) ; => 1 (/ 5.0 2) ; => 2.5

几个值得注意的语义:

  • 幂运算使用^(对应 Racket 的(expt 2 3),见 racket.md);
  • 当操作数全为整数时,/执行整除:(/ 5 2)得2,%取余得1;
  • 只要出现浮点数,除法即转为浮点运算:(/ 5.0 2)得2.5。

布尔值:true / false

true ; for Wahr false ; for Falsch (! true) ; => Falsch (&& true false (prn "doesn't get here")) ; => Falsch (|| false true (prn "doesn't get here")) ; => Wahr

!是逻辑非,&&、||分别是逻辑与、逻辑或。关键特性是短路求值:(&& true false (prn "doesn't get here"))中,第二个操作数false已使整个表达式确定为假,因此第三个操作数(prn "doesn't get here")根本不会执行——示例中的输出语句不会打印任何内容。这为编写"仅在前置条件满足时才求值"的守卫表达式提供了保障。

字符即整数

与 C 语言一致,Paren 中的字符本质上是 int:

(char-at "A" 0) ; => 65 (chr 65) ; => "A"

(char-at "A" 0)取出字符串"A"下标 0 处的字符并返回其 ASCII 码 65;(chr 65)反向把整数 65 转换回字符串"A"。

字符串:固定长度的字符数组

"Hello, world!" "Benjamin \"Bugsy\" Siegel" ; Backslash ist ein Escape-Zeichen "Foo\tbar\r\n" ; beinhaltet C Escapes: \t \r \n

字符串是固定长度的字符数组,反斜杠\是转义字符,支持 C 风格的转义序列(\t、\r、\n)。字符串可以拼接:

(strcat "Hello " "world!") ; => "Hello world!"

并且可以像字符列表一样按下标访问:

(char-at "Apple" 0) ; => 65

打印输出

(pr "Ich bin" "Paren. ") (prn "Schön dich zu treffen!")

pr与prn均用于输出,prn会在结尾追加换行。两者都接受多个参数并按序输出。

2. 变量:set 与局部绑定

使用(set 变量名 值)创建或重新赋值一个变量:

(set some-var 5) ; => 5 some-var ; => 5

变量名的可用字符范围很宽松:除了(、)、;、#、"之外的任意字符都可出现在变量名中,因此连字符命名的some-var完全合法,无需引号包裹。

访问一个从未赋值的变量会抛出异常:

; x ; => Unknown variable: x : nil

(示例中被注释掉,说明若直接求值x,运行时将报告Unknown variable: x。)

与全局set相对的是局部绑定,它直接借用 Lambda 演算的思想——通过立即调用匿名函数将参数绑定到作用域内:

((fn (a b) (+ a b)) 1 2) ; => 3

这里a、b只在(fn ...)内部与1、2绑定,求值(+ a b)得3,且不会污染外部环境。

3. 集合:列表

Paren 的列表是类向量(vector-like)的数据结构,随机访问的时间复杂度为O(1)——这与传统 Lisp 的链表(O(n) 访问)截然不同,是 Paren 面向嵌入式场景做出的实用化取舍。

构造列表:cons / list / quote

(cons 1 (cons 2 (cons 3 (list)))) ; => (1 2 3) (list 1 2 3) ; => (1 2 3) (quote (+ 1 2)) ; => (+ 1 2)
  • (list 1 2 3)是便捷的变参构造函数,直接生成列表;
  • cons把元素添加到列表头部:(cons 0 (list 1 2 3))得(0 1 2 3);
  • quote同样可用于生成字面量列表。

高阶操作:map / filter / length

列表是 Paren 的基础类型,因此围绕它的函数非常丰富,原文给出三个典型例子:

(map inc (list 1 2 3)) ; => (2 3 4) (filter (fn (x) (== 0 (% x 2))) (list 1 2 3 4)) ; => (2 4) (length (list 1 2 3 4)) ; => 4
  • map对每个元素应用函数:inc自增后得到(2 3 4);
  • filter保留满足谓词的元素:(fn (x) (== 0 (% x 2)))判断偶数,筛出(2 4)——这里同时展示了fn与==、%的组合用法;
  • length返回元素个数。

4. 函数:fn 与 defn

用fn创建函数。函数总是返回其最后一个表达式的值:

(fn () "Hello World") ; => (fn () Hello World) : fn

fn表达式本身求值后是一个函数值(类型标注为fn)。调用所有函数——包括匿名的 lambda 表达式——都需要加括号:

((fn () "Hello World")) ; => "Hello World"

把函数赋值给变量即可反复调用:

(set hello-world (fn () "Hello World")) (hello-world) ; => "Hello World"

以上写法可以用函数定义语法糖defn缩短(事实上defn本身就是一个宏,见下文宏一节):

(defn hello-world2 () "Hello World")

其中()是该函数的参数列表。带参数的版本:

(set hello (fn (name) (strcat "Hello " name))) (hello "Steve") ; => "Hello Steve"

等价于语法糖形式:

(defn hello2 (name) (strcat "Hello " name))

defn只是把"定义函数"这一高频操作压缩为更紧凑的写法,底层仍是fn+ 绑定。

5. 相等性:==

数值比较使用==,且比较是数值语义而非类型严格相等:

(== 3 3.0) ; => wahr (== 2 1) ; => falsch

(== 3 3.0)返回真,说明整数 3 与浮点数 3.0 在数值上相等——这一点与要求"类型与值都相等"的语言不同,更贴近数值计算的直觉。

6. 控制流

条件:if

(if true ; test Ausdruck "this is true" ; then Ausdruck "this is false") ; else Ausdruck ; => "this is true"

if采用三段式结构:测试表达式、真分支、假分支,整体是一个表达式,求值结果为分支表达式的值。

for 循环:面向数字

;; (for SYMBOL START ENDE SCHRITT AUSDRUCK ..) (for i 0 10 2 (pr i "")) ; => schreibt 0 2 4 6 8 10 (for i 0.0 10 2.5 (pr i "")) ; => schreibt 0 2.5 5 7.5 10

for的完整形态为(for 符号 起始值 结束值 步长 表达式 ...):

  • 第一个示例从 0 到 10、步长 2,依次打印0 2 4 6 8 10;
  • 第二个示例说明for同样支持浮点步长,打印0 2.5 5 7.5 10——pr的两个参数之间用空字符串""分隔,避免数字连成一片。

while 循环

((fn (i) (while (< i 10) (pr i) (++ i))) 0) ; => schreibt 0123456789

这里把while封装在立即调用的匿名函数中,从而获得一个局部计数器i:条件(< i 10)成立时循环体依次执行(pr i)与(++ i)(自增),最终打印0123456789。++是 Paren 提供的内置自增操作符,配合set/fn即可实现带局部状态的循环结构。

7. 变更:set 的两种用途

set除了给变量赋新值,还能修改列表中的某个位置(place):

(set n 5) ; => 5 (set n (inc n)) ; => 6 n ; => 6 (set a (list 1 2)) ; => (1 2) (set (nth 0 a) 3) ; => 3 a ; => (3 2)
  • (set n (inc n))展示经典的"读取-自增-写回"模式;
  • (set (nth 0 a) 3)把列表a下标 0 处的元素改为 3,随后a的值变为(3 2)。nth在此作为左值位置参与赋值,说明 Paren 的set支持"变量或存储位置"两种目标,这与命令式语言的数组下标赋值是同一语义。

8. 宏:扩展语言语法

宏允许你扩展语言本身的语法。Paren 的宏机制非常轻量:宏本质上就是代码变换,把宏调用形式重写为底层形式再求值。

宏用defmacro定义,其形态与函数类似,但参数列表中的...表示"捕获剩余参数"。原文甚至用宏重新实现了defn:

(defmacro setfn (name ...) (set name (fn ...))) (defmacro defn (name ...) (def name (fn ...)))

也就是说,(defn)本身就是一个宏——它把(defn f (x) body)这样的形式展开为底层的绑定加函数构造。由于defn是由defmacro定义的宏,而非解释器的特例语法,这直观说明了 Paren 中"语法糖"与"宏"是同一件事。

实战:用宏添加中缀记法

既然宏可以做任意语法变换,原文演示了一个颇具说服力的例子——为前缀语言 Paren 添加中缀记法:

(defmacro infix (a op ...) (op a ...)) (infix 1 + 2 (infix 3 * 4)) ; => 15

(infix 1 + 2 (infix 3 * 4))被展开为(+ 1 2 (* 3 4)),求值得15。可以看到:

  • 宏参数a绑定到第一个操作数,op绑定到运算符,...捕获其余参数;
  • 展开结果把运算符挪到最前,恢复前缀形式;
  • 宏可以嵌套使用,内层(infix 3 * 4)先行展开为(* 3 4)。

宏的代价:非卫生性

;; Makros sind nicht hygenisch, Du kannst bestehende Variablen überschreiben!

Paren 的宏是非卫生(non-hygienic)的:展开后的代码直接插入调用点的作用域,因此宏内部引入的变量可能覆盖调用方已有的变量,引发"变量捕获"问题。这既是宏强大灵活性的来源,也是编写宏时需要警惕的边界——在 Paren 中,宏就是朴素的代码变换,语言不会替你隔离名字冲突。

文档形态:仓库中的 Paren 教程如何组织

作为 Learn X in Y Minutes 项目的一部分,Paren 教程遵循仓库统一的文档规范:

  • YAML frontmatter:每个教程文件头部都带有一段 YAML 元数据。以 de/paren.md 为例,其 frontmatter 记录了contributors(原作者 KIM Taegyoon、Claudson Martins)与translators(德语译者 Dennis Keller),用于署名与溯源;paren.md 则额外声明了name: Paren与建议文件名learnparen.paren,提示读者可将示例保存为.paren文件。
  • 格式校验:仓库的 lint/frontmatter.py 会解析每个 Markdown 文件头部的---块,用 yamllint 校验 YAML 合法性,并检查name、filename、contributors、translators等键的类型与结构(例如贡献者必须是["名字", "链接"]形式的二元列表),确保多语言文档的元数据一致、可被自动化工具消费。
  • 多语言对照:同一份 Paren 教程以英文 paren.md 为底本,衍生出 de/paren.md、ru/paren.md、pt-br/paren.md 等多个翻译版本,代码示例完全同步,仅注释语言不同——这使其天然适合作为跨语言学习材料或机器翻译的对齐语料。
  • 与 Racket 教程的关系:原文开篇注明"部分示例来自 Racket"。racket.md 同样是仓库中的 Lisp 家族教程,两者在 S 表达式、quote、map/filter、if等概念上一脉相承;差异点在于 Paren 面向嵌入式场景做了精简(列表为 O(1) 随机访问的向量结构、set支持位置赋值、宏极简且非卫生),而 Racket 是通用多范式语言,类型系统与语言设施更为丰富。对照阅读 paren.md 与 racket.md 可以更清楚地看出"嵌入式 Lisp 方言"与"通用 Lisp 实现"的设计分岔。

小结

Paren 以极小的语法面实现了 Lisp 家族的核心能力:S 表达式与quote提供"代码即数据",fn提供闭包与局部绑定,列表与map/filter提供函数式数据处理,set提供命令式变更,defmacro提供语法扩展。配合for/while循环与if条件,它足以胜任嵌入式脚本场景下的配置描述与逻辑编排。若想继续深入,可以从本仓库的 paren.md 英文原版出发,对照 de/paren.md 的德语注释逐行复习,再通过 racket.md 横向比较 Lisp 家族其他成员的设计取舍。

  • 文档
  • 教程

【免费下载链接】learnxinyminutes-docs

Code documentation written as code! How novel and totally my idea!

项目地址:https://gitcode.com/gh_mirrors/le/learnxinyminutes-docs
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询