还有咱们的 emacs mode:
目前还在初级阶段,还不方便给大家使用,算是可以和大家分享一下 lisp 语法设计的思路。
未来写完子举的,到 x86 的编译器,我就发布一个方便大家使用的版本。
还有咱们的 emacs mode:
目前还在初级阶段,还不方便给大家使用,算是可以和大家分享一下 lisp 语法设计的思路。
未来写完子举的,到 x86 的编译器,我就发布一个方便大家使用的版本。
我也想写出自己的 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) 上的操作。
car — 取第一个元素cdr — 取除第一个外的剩余列表list-head — 取第一个元素(同 car)list-tail — 取剩余列表(同 cdr)list-first — 取第一个元素list-second — 取第二个元素list-third — 取第三个元素list-last — 取最后一个元素list-init — 取除最后一个外的所有元素list-get — 按索引取元素list-length — 列表长度list-empty? — 是否为空列表list-member? — 是否包含某元素list-copy — 复制列表list-put — 按索引替换元素(不可变)list-put! — 按索引替换元素(可变)list-push — 尾部追加(不可变)list-push! — 尾部追加(可变)list-push-front! — 头部添加(可变)list-pop! — 弹出尾部元素(可变)list-pop-front! — 弹出头部元素(可变)list-reverse — 反转list-to-set — 转集合list-range — 生成 0 到 n - 1 的整数列表list-enumerate — 为元素配对索引list-sort! — 原地排序list-sort — 排序(不可变)list-each — 遍历执行副作用list-each-index — 带索引的遍历list-flat-map — 映射并扁平化list-flat-map-index — 带索引的映射并扁平化list-map — 映射list-map-index — 带索引的映射list-map-zip — 同时映射两个列表list-zip — 按位置配对list-unzip — 解配对list-select — 筛选(保留满足条件的)list-reject — 反筛选(移除满足条件的)list-fold-left — 左折叠list-fold-left-index — 带索引的左折叠list-fold-right — 右折叠list-fold-right-index — 带索引的右折叠list-every? — 所有元素满足条件list-some? — 存在元素满足条件list-take — 取前 n 个元素list-drop — 去掉前 n 个元素list-append — 拼接两个列表list-concat — 拼接列表的列表list-group — 按条件分组list-find — 查找第一个满足条件的元素list-find-index — 查找第一个满足条件的索引集合类型 (set-t E) 上的操作。
set-size — 大小set-empty? — 是否为空集set-member? — 是否包含某元素set-subset? — 是否为子集set-copy — 复制集合set-add — 添加元素(不可变)set-add! — 添加元素(可变)set-delete — 删除元素(不可变)set-delete! — 删除元素(可变)set-clear! — 清空集合(可变)set-union — 并集set-inter — 交集set-difference — 差集set-disjoint? — 是否不相交set-each — 遍历执行副作用set-every? — 所有元素满足条件set-some? — 存在元素满足条件set-map — 映射set-select — 筛选set-reject — 反筛选set-to-list — 转列表代码的效果:
(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 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 类型系统,就根本不需要动态计算类型。
在我的代码仓库内有一个保存开发日志的文件夹:
记录了我所有的设计决策。
这是我给自己看的,是为了避免自己在设计方面兜圈子,所以才记录的。
也许对你也有帮助。
你会发现我改了很多很多, 才形成现在的设计。
我对现在的设计很满意。