前言
元语(meta-lisp)使用符号算式(S-expression)语法。
- 模块顶层由语句(statement)组成。
- 语句内由算式(expression)组成。
下面分组介绍 meta-lisp 的所有语法。
目录
注释
行注释
注释以 ; 开头,直到行尾。
在写行注释的时候 lisp 程序员通常写两个 ;;。
(@注释)
(@注释) 在编译时被忽略,其算式求值为 空值。
@注释 被解析器识别为语法关键字,其内容是字面符号算式,不会被求值。
字面量
原子
整数由数字组成,可选负号。
浮点数带小数点。
文本用双引号包裹。
符号用单引号开头,后面跟名字。
真假值用 真 和 假,它们不是字面量,而是绑定了真假值的变量。
空值用 空值,它也不是字面量,而是绑定了空值的变量。
(@列表)
创建列表。
增加 @ 前缀,是为了避免占用 列表 这个变量名。
(@集合)
创建集合。
(@散列)
创建散列表。
(@文本)
将多个文本算式拼接成一个文本。
上面的例子等价于:
(@引用)
创建符号或原子列表。
等价于:
(@符号算式)
将符号算式转化为 符号算式型 类型的值,保留每个子节点的源代码位置信息。
符号算式型 类型的定义:
使用示例:
变量
(定义)
定义模块级变量。
变量
变量引用一个已绑定的名字。
名字由字母、数字和 - 等字符组成。
限定变量
<模块名>/<名字> 引用其他模块中的名字。
限定名不需要先 (导入),可以直接使用。 (导入) 是专门用来取消 <模块名> 前缀的。
函数
函数作用
函数作用是最重要的语法。
如果符号算式的第一个位置不是语法关键词,就被认为是函数作用。
第一个位置是函数,其余是参数。
先求值函数位置的算式, 然后求值所有参数位置的算式, 然后进行函数作用。
当函数作用的参数个数不足时,会形成部分作用(通过柯里化实现)。
等价于:
并且 (整数加 1) 可以作为值传递给函数,或者作为结果返回。
等价于:
(函)
创建匿名函数。
(<形参> ...) 是形式参数列表, <函数体> 是一个或多个算式。 当函数被作用时,实际参数被绑定到形式参数,然后求值 <函数体>。
多个参数:
等价于:
(定义)
定义函数。
定义函数等价于定义值为 lambda 的变量。
等价于:
函数体 <函数体> 可以是多个算式:
类型
原子类型
| 类型 | 说明 | 例子 |
|---|---|---|
整数型 | 整数 | 42 -1 |
浮点型 | 浮点数 | 3.14 -2.5 |
文本型 | 文本 | "hello" |
符号型 | 符号 | 'foo |
真假型 | 真假值 | 真 假 |
空值型 | 空值 | 空值 |
文件型 | 文件 | (打开输入文件 "abc.txt") |
类型型 | 类型 | 整数型 |
结森型 | 结森值 | (解析结森 "42") |
符号算式型 | 带源码位置的符号算式 | (@符号算式 42) |
源码位置型 | 源码位置 | (作源码位置 "test" ...) |
源码区间型 | 源码区间 | (作源码区间 ...) |
源码坐标型 | 源码坐标 | (作源码坐标 0 0 0) |
容器类型
| 类型 | 说明 | 例子 |
|---|---|---|
(列表型 甲) | 元素类型为 甲 的不可变列表 | (@列表 1 2 3) |
(数组型 甲) | 元素类型为 甲 的可变数组 | (@数组 1 2 3) |
(集合型 甲) | 元素类型为 甲 的集合 | (@集合 1 2 3) |
(散列型 甲 乙) | 键为 甲 值为 乙 的散列表 | (@散列 "a" 1) |
(序对型 甲 乙) | 前项类型为 甲、后项类型为 乙 的序对 | (作序对 1 "a") |
(或有型 甲) | 可能存在类型为 甲 的值的或有值 | (有 42) |
(匣子型 甲) | 存放类型为 甲 的值的可变匣子 | (作匣子) |
(->)
函数类型。
接收 <参数类型> 参数,返回 <返回类型>。
例如:
(声明)
声称一个名字的类型。
编译器会从 (定义) 的 <函数体> 推导出类型, 然后检查是否与 (声明) 的类型一致。
(承认)
承认名字的类型。
与 (声明) 类似,但是编译器不会检查 (定义) 的 <函数体>。
(型例)
给出类型,以及属于这个类型的实例。
编译器会检查实例的类型是否匹配。 可用于澄清代码意图。
(泛型)
包含类型变量的类型。
类型变量通常用天干(甲、乙、丙……)表示,在 <类型> 中可以被引用。 用于 声明 中声称泛型函数。
(定义类型)
定义一个新的类型。<类型名> 成为类型构造器,<函数体> 是一个求值为类型的算式。 带类型参数时,参数可以在 <函数体> 中被引用。
例如:
使用举例:
条件
(若)
条件分支。
<条件> 被求值。 如果为真,求值 <真分支> 并返回。 否则求值 <假分支> 并返回。
(当)
条件为真时执行,用于副作用。
<条件> 为真时求值 <函数体>,否则跳过。 <函数体> 中可以写多个算式。 (当) 算式的返回值总是 空值。
(除非)
条件为假时执行,用于副作用。
<条件> 为假时求值 <函数体>,否则跳过。 <函数体> 中可以写多个算式。 (除非) 算式的返回值总是 空值。
(若则)
多分支条件。
依次求值每个 <问>。 第一个为真的分支的 <答> 被求值并返回。 末尾的 <问> 可以写 否则 作为默认分支。
(且)
短路与。
从左到右求值。遇到第一个假值就停止并返回该值。全真时返回最后一个值。
零个参数时返回 真。
(或)
短路或。
从左到右求值。遇到第一个真值就停止并返回该值。全假时返回最后一个值。
零个参数时返回 假。
顺序与绑定
(循序)
顺序执行。
<函数体> 可以是多个算式 <算式> ..., 依次求值,返回最后一个算式的值。 前面的算式通常是为了副作用。
(函) 和 (定义) 中的 <函数体> 函数体, 都类似于 (循序) 的 <函数体>。
(令)
顺序局部变量绑定。
每个 <算式> 可以引用前面绑定的名字。
(递归令)
类似 (令),但是允许递归与互相递归。
所有 <算式> 可以引用所有 <名字>。
互相递归的例子:
顺序依赖的例子:
局部 (定义)
<函数体> 中的局部可递归变量绑定。
允许 (定义) 在 <函数体> 中使用。 用来代替嵌套的 (递归令),以减少缩进。
互相递归的例子(与 (递归令) 的例子等价):
顺序依赖的例子:
(令) 与 (定义) 可以混合使用:
代数数据类型
代数数据类型(Algebraic data type)是 meta-lisp 的核心数据类型。
meta-lisp 提供了从显式(explicit)到便捷的多种语法来定义数据类型:
| 语法 | 用途 |
|---|---|
(定义代数类型) | 多构造器,所有名字由你指定 |
(定义结构类型) | 单构造器,所有名字由你指定 |
(定义结构) | 单构造器,按约定生成所有名字 |
(定义枚举) | 多构造器,按约定生成所有名字 |
所有便捷语法最终都展开为 (定义代数类型)。 其中最常用的是 (定义枚举) 和 (定义结构)。
(定义代数类型)
代数数据类型(algebraic data type)是构建复合数据结构的核心机制。
(定义代数类型) 是最基础的语法形式,其中所有名字由你显式指定。
只有 <修改器名> 是可选的。 如果不写,这个字段的引用就是不可修改的。
例如:
将会生成具有下列类型的函数:
使用举例:
对于只有单一构造器的代数类型而言, 谓词 为点 是多余的,没有意义的。 只有代数类型有多个构造器时, 所生成的谓词才有意义。
(定义代数类型) 所定义的类型可以带有类型参数。
例如:
将会生成具有下列类型的函数:
(定义结构类型)
与 (定义代数类型) 类似,但是只有一个构造器。
等价于:
(定义结构类型) 这个语法来自 scheme, 起源于 scheme 48 这个 scheme 方言, 原名 define-record-type(记录类型)。
我们给这个语法加上了 <字段名> 的类型声明, 即 (<字段名> <类型>)。
(定义代数类型) 模仿 (定义结构类型), 进一步支持了多个构造器。
(定义枚举)
定义多个构造器的代数数据类型。 每个构造器按照约定生成谓词、访问器和修改器的名字。
等价于:
对于某个给定的 <构造器名> 生成其名字的规则如下:
<谓词名>=为<构造器名>--为变量项<访问器名>=<构造器名><字段名>--变量项名<修改器名>=<构造器名>存<字段名>--变量项存名
(定义结构)
定义单构造器结构体。 <类型名> 类型名必须以 型 结尾, 格式为 <基名>型。 其中 <基名> 将被用于生成其他名字。
等价于:
对于某个给定的 <类型名> 生成其名字的规则如下:
<类型名>=<基名>型--点型<谓词名>=为<基名>--为点<构造器名>=作<基名>--作点<访问器名>=<基名><字段名>--点横坐标<修改器名>=<基名>存<字段名>--点存横坐标
(匹配)
用模式匹配的方式解构代数数据类型。
不透明类型
(定义黑盒类型)
定义不透明类型,隐藏内部表示。
例如 builtin 中的 匣子型,内部表示为 (数组型 甲):
在实现接口函数的时候,等价于声明了:
因此接口函数内部可以使用列表 API 来实现:
在使用接口函数的时候,等价于声明了:
外部代码只能通过接口函数来操作 匣子型:
模块
一个文件夹可以被视为一个项目(package), 项目中的所有 .meta 文件,都被视为属于这个项目。
一个项目中可以有多个模块(module)。
模块系统与文件系统解耦,存放模块的路径和文件名并不重要。 同一模块的代码,可以拆分到不同文件中。
(模块)
声明当前模块。
每个 .meta 文件必带有一个模块声明,一般放在开头。
在同一个项目中,可以通过 <模块名>/<名字> 引用别的模块中的名字。
同一个模块中的函数,就算写在不同的文件中,也可以相互递归。
偶数.meta:
奇数.meta:
(导入)
从其他模块导入指定名字。
导入后可以直接使用,不需要限定前缀。
之后,
可简写为:
(导入为)
导入模块并修改前缀。
<模块名>/<名字> 改为 <前缀>/<名字>。
之后,
可简写为:
(全导入)
导入模块中所有名字。
使得所有名字都可以不用前缀就能引用。
如果当前模块已经有重复的名字, 就略过这个名字不导入。
因此,不带前缀的引用,还是会引用到当前模块自己的名字。 或者说本地的定义,可以覆盖 (全导入) 所引入的定义。
(私有)
将名字标记为私有。
被标记为私有的名字不能被其他模块引用。
测试
(定义测试)
定义测试。
<函数体> 中可以包含多个断言。
支持以下断言:
(断言 算式)-- 断言算式为真。(断言非 算式)-- 断言算式为假。(断言相等 左边 右边)-- 断言左边与右边相等(使用相等比较)。(断言不等 左边 右边)-- 断言左边与右边不相等。