我设计了一个静态类型的 lisp : meta-lisp

还有咱们的 emacs mode:

目前还在初级阶段,还不方便给大家使用,算是可以和大家分享一下 lisp 语法设计的思路。

未来写完子举的,到 x86 的编译器,我就发布一个方便大家使用的版本。

3 个赞

我也想写出自己的 LISP,所以有几个问题想请教前辈。

按照 Common Lisp 风格,关系紧密的变量和函数一般名字也是一样的,但是 Scheme 等因为变量函数同处一个命名空间就不能这么做。但是我真的很喜欢变量函数重名,这样就不需要多想一个名字了;另一方面,变量函数分属不同命名空间会导致函数调用不得不使用 funcall(把函数转换成表达式倒简单),在 Common Lisp 里已经有点麻烦了,在函数式编程语言里就更麻烦了。就没有两全其美的方法吗?

另一个问题是多参数函数。多参数函数应该像 Haskell 那样默认柯里化呢,还是按照 LISP 惯例一样打包呢?用默认柯里化的好处是比较容易抄 Haskell 代码,但是和 LISP 语法完全不合,看来好像只有不用默认柯里化一种方式了。有一种基于 Common Lisp 的静态类型语言叫做 Coalton,它不使用默认柯里化(大概是今年三月份改的,以前还是照抄 Haskell),并且由此得到了关键字参数的能力。但是 Coalton 不支持变参函数,目前只能用宏代替。可能是因为变参函数的类型不好写吧。但是没有变参函数也太浪费 LISP 的括号语法了,有没有什么办法让变参函数存在于静态类型语言中呢?

另外还有类型相关的问题。大部分函数式语言里,类型定义和变量定义都使用不同的语法,能统一这两种语法的大都用的是依赖类型论,基本跳脱编程语言的领域了。有没有办法定义类型层面的函数,然后通过类型层面的变量定义和类型层面的表达式来定义类型呢?Typeclass 好像也可以这样做,因为 typeclass 的方法指的是哪个函数在编译时就已经确定了。

语法让我想起了Pie语言

lisp-1和lisp-2的差别,如果你关注的只是要不要funcall那不算是问题,二者对实现的主要影响是要不要做两套env,这会影响你的closure设计。

从实现上说,scheme的eval thunk本来就是curry的,这样好做fcc,但haskell的thunk反而不是curry的,因为haskell求值是lazy的,图求值机器不需要过早的做currying。

要写类型层面的函数不如tweak下编译器。

按照 Common Lisp 风格,关系紧密的变量和函数一般名字也是一样的,但是 Scheme 等因为变量函数同处一个命名空间就不能这么做。但是我真的很喜欢变量函数重名,这样就不需要多想一个名字了;另一方面,变量函数分属不同命名空间会导致函数调用不得不使用 funcall(把函数转换成表达式倒简单),在 Common Lisp 里已经有点麻烦了,在函数式编程语言里就更麻烦了。就没有两全其美的方法吗?

我也喜欢简单的具有一致性的命名方式。 想要达到的效果是,需要命名一个函数的时候,可以很简单找到名字。

首先我是这样解决 scheme 的 list 函数与 list 作为变量的冲突的:

[<exp> ...]
(@list <exp> ...)

创建列表。

[1 2 3]
["a" "b" "c"]

方括号 [...](@list ...) 的语法糖。

上面的例子等价于:

(@list 1 2 3)
(@list "a" "b" "c")

增加 @ 前缀,是为了避免占用 list 这个变量名。


关于,函数命名,我使用 <修饰>-<核心> 的方式,类似汉语构词语法中的偏正结构。

例如:

列表

列表类型 (list-t E) 上的操作。

类型与构造

  • list-t — 列表类型构造器
  • make-list — 创建空列表
  • list? — 判断是否为列表
  • cons — 在头部添加元素

访问

信息

修改

变换

生成

排序

遍历与映射

折叠

量化

子列表

分组与查找

集合

集合类型 (set-t E) 上的操作。

类型与构造

  • set-t — 集合类型构造器
  • make-set — 从列表创建集合

信息

修改

集合运算

遍历与变换

代码的效果:

(module builtin)

(claim list-map
  (polymorphic (A B)
    (-> (-> A B) (list-t A)
        (list-t B))))

(define (list-map f list)
  (if (list-empty? list)
    []
    (cons (f (car list)) (list-map f (cdr list)))))

(define-test list-map-test
  (assert-equal
    (list-map (iadd 10) [1 2 3])
    [11 12 13]))

另一个问题是多参数函数。多参数函数应该像 Haskell 那样默认柯里化呢,还是按照 LISP 惯例一样打包呢?用默认柯里化的好处是比较容易抄 Haskell 代码,但是和 LISP 语法完全不合,看来好像只有不用默认柯里化一种方式了。有一种基于 Common Lisp 的静态类型语言叫做 Coalton,它不使用默认柯里化(大概是今年三月份改的,以前还是照抄 Haskell),并且由此得到了关键字参数的能力。但是 Coalton 不支持变参函数,目前只能用宏代替。可能是因为变参函数的类型不好写吧。但是没有变参函数也太浪费 LISP 的括号语法了,有没有什么办法让变参函数存在于静态类型语言中呢?

默认柯里化对于函数编程来说很重要。

比如 (list-map (iadd 1) [1 2 3]) => [2 3 4]

lisp 是可以使用默认柯里化的, 只需要要求所有函数的参数个数固定。

比如我们可以区分 list-append 和 list-concat

(list-append [1 2 3] [4 5 6])
(list-concat [[1 2 3] [4 5 6]])

为了保持类型系统简单,我不使用变参函数,所以我的设计中没有支持。 如果你想要使用的话,可以 desugar 到 list 的函数。

  • 首先在一般的函数类型 (-> A B) 之外,设计一个新的 (->* A B)
  • 使得 (->* A B) 等价于 (-> (list-t A) B)
  • 对应的 lambda 可以使用 (lambda x ...) 而不是 (lambda (x) ...)

我觉得保持类型系统简单, 同时也保持自己写的代码简单和容易理解, 比变参数更重要。

没有变参数,反而可以设计出来更简洁的 API。 比如,最严重的反例是 python 的 numpy 和 pandas, 由于大量使用变参,导致 API 非常胡乱,没有一致性。

另外还有类型相关的问题。大部分函数式语言里,类型定义和变量定义都使用不同的语法,能统一这两种语法的大都用的是依赖类型论,基本跳脱编程语言的领域了。有没有办法定义类型层面的函数,然后通过类型层面的变量定义和类型层面的表达式来定义类型呢?Typeclass 好像也可以这样做,因为 typeclass 的方法指的是哪个函数在编译时就已经确定了。

完全可以这样做(但是与 typeclass 的思路不同)。

一般的设计是在 value-t 类型之内,有 type-value 这个 variant 来保存 type-t 类型:

(define-enum value-t
  (type-value (type type-t))
  (curry-value (target value-t) (arity int-t) (args (list-t value-t)))
  (definition-value (definition definition-t)))

如果想要达到你说的效果,可以直接不定义 type-t 类型, 直接把它嵌入在 value-t 中,用 value (比如 list 和 symbol)来表示类型。

int-t -> ['atom 'int]
string-t -> ['atom 'string]
(list-t A) -> ['list A]
(hash-t K Y) -> ['hash K Y]

在设计 meta-lisp 的过程中, 我一开始也是这样设计的, 但是得不偿失。

因为动态计算类型的需求,其实来自于复杂的类型系统。 比如包含各种子类型限制的类型系统,其中最典型的是 TypeScript 和 Scala。

如果保持类型系统简单,比如纯粹使用 HM 类型系统,就根本不需要动态计算类型。

在我的代码仓库内有一个保存开发日志的文件夹:

记录了我所有的设计决策。

这是我给自己看的,是为了避免自己在设计方面兜圈子,所以才记录的。

也许对你也有帮助。

你会发现我改了很多很多, 才形成现在的设计。

我对现在的设计很满意。