如梦令编程语言发布
如梦令 编程语言 (RML)
如梦令编程语言是在Rebol语言核心语法的基础上,做了一些自以为是的修改而来。谨以此为Rebol语法的传承,略尽绵薄之力。
基本概念
如梦令语言分属Lisp语系,代码本身是一个层层嵌套的Token列表,代码与数据具有同像性,与目前流行的C系编程语言有较大区别。RML的代码在运行时分割为Token列表,每个Token通过其字面表示的格式确定类型。
主流的编程语言通常会定义一系列的语法规则来实现程序所需的各种功能,而RML中只有一种核心语法:表达式的长度和实现的功能由头部Token确定,当获取到的Token数量满足头部Token的要求时,判断表达式完整,并进行求值。其与传统Lisp语言的区别在于,Lisp中表达式的长度依靠括号()
来确定,因此Lisp往往被吐槽括号太多,影响阅读。RML中剔除了这一依赖,便于书写,另一方面也带来了表达式定长的限制,通过修饰字的概念,能够减少这种限制带来的不便,这便是RML在语法上的取舍。
基本语法
第一行代码
print "hello word"
注释
;这是单行注释
RML目前实现了单行注释,从分号 ;
到行末的代码在解析时会被跳过。
数据类型
n i: 123 4.56 true "hello" #'A'
[1 2 3] (1 2 3) {a: 123} a/b/c %rml/go/rml.go #{0a0b}
Rebol中提供了多达50种数据类型,RML目前实现了常用的一些基本数据类型,从左到右分别是 单字
,设字
,整数
,小数
,逻辑
,字符串
,字符
,方块
,圆块
,对象
,路径
,文件
,二元
类型,数据类型有对应的字面表达格式,主要通过空白字符分隔。集合类型首先被视为单个Token,内部另行分割。
赋值
i: 123 j: 456
x: j: 789
RML中赋值并非特殊的语句,而是通过特定的Token类型设字
来实现值的绑定。设字
的格式规定是以:
结尾,设字接受一个Token,执行绑定后再次返回该Token,因此可以连续赋值。
取值
i ;输出123
单字
类型用来实现取值,其格式要求为一串连续的字符,且不与其他类型格式冲突,请尽量不要使用特殊字符,支持中文。由于前面我们对 i
赋值为 123
,因此此处控制台输出123。
除了单字外,还有其他的取值方式:
:i (i)
以上分别为 取字
类型和 圆块
类型。取字类型与单字类型的区别为,只进行取值操作,不会新建表达式,可用在传递 函数
类型等会新建表达式的Token。圆块类型具有较高的优先级,会被优先求值,可用于指定程序运算的顺序。
中缀运算
传统Lisp语言不支持中缀运算符,RML中为了贴近使用习惯,提供了中缀运算支持。中缀运算优先级比一般表达式高,但中缀运算符间优先级相同。
1 + 2 * 3 ;输出9
上面的代码输出为 9
,执行顺序为从左到右,依次执行。如果需要改变优先级,请使用圆块 ()
1 + (2 * 3) ;输出7
路径类型
路径类型用于集合类型的存取操作,形式上是一串连续的以 /
连接的Token,以此表示层级关系,例如
a/b/c/1
路径中可以插入圆块和取字进行动态求值,例如
b/(1 + 2)/a ;等同于b/3/a
方块类型
方块类型 []
等同于Lisp中的列表,类似于其他语言中的Array/List的概念,通过路径类型可以对方块进行设值、取值操作。
blk: [1 2 3]
blk/2 ;输出2
blk/2: 5
blk ;输出[1 5 3]
方块主要通过序号作为索引,且序号从1开始。
另外方块可以模拟键值对的形式进行操作:
blk: [a 1 b 2]
blk/b ;输出2
blk/b: 5
blk ;输出[a 1 b 5]
这种方式会按路径的字面值与方块中的元素依次比较,并操作匹配到的元素的下一个元素。请注意不要在长度较大的方块中使用这种方式,效率过低,不能替代map
方块类型是RML中最重要的数据类型,其底层是一个封装过的Token数组。事实上圆块 ()
和 路径类型的底层也是Token数组,只是通过类型标记的不同,实现不同的功能。
对象类型
RML中的对象对应了Rebol中语境的概念,其本质是一个具有层级关系的 map
,每个对象都存放了其父级对象的指针。与Rebol不同,RML中对象的字面格式类似于 js
obj: {a: 123 b: 456 c: {d: 789}}
对象类型支持嵌套,同样通过路径进行操作
obj/c/d ;输出789
obj/c/d: 987
obj/c/d ;输出987
自定义函数
RML中函数的定义方式如下:
f: func [n] [n * 2]
通过内置的原生函数 func
可以实现用户自定义函数。 func
需要两个参数,第一个是参数列表的方块,第二个是要执行的代码方块。函数会返回最后一个表达式的返回值,也可以通过 return
来手动返回值。
f 2 ;输出4
函数可以通过绑定的单字调用,当传入足够数量的参数时,解释器执行函数求值。
另外函数还能通过修饰字来拓展功能,详见文档。
循环
loop 3 [print "hello"]
repeat i 3 [print i]
for i 1 3 1 [print i]
while [i < 5] [print i]
RML中主要使用以上循环控制函数,loop
指定了循环次数,repeat
额外指定了计次变量,for
额外指定了循环起止值和步长,while
提供了循环判断的代码块。与其他语言一样,循环体内可以使用 break
和 continue
来手动控制循环流程。
分支
if 1 > 2 [print 1] ;跳过
either 1 > 2 [print 1] [print 2] ;打印2
目前RML提供了 if
和 either
函数,根据其后的第一个参数,确定要执行的代码。
由于RML只支持定长的表达式,所以不支持主流语言的连续使用 if
和 else if
不断拓展表达式的功能。
多线程
RML在Go语言的基础上提供了多线程并行的功能。
fork [fib 40] ;启动一个Goroutine进行计算,不等待
res: 0
fork/result [fib 40] res ;通过修饰字,指定分支线程返回值的接收者
另外还支持同时启动多个Goroutine,并决定是否等待。
spawn [ [print fib 40] [print fib 40] ] ;启动两个Goroutine进行计算,不等待
spawn/wait [ [print fib 40] [print fib 40] ] ;启动两个Goroutine进行计算,并等待其全部执行完毕
中文支持
RML使用遍历UTF8字符的方式来解析代码,因此可以完全支持中文。目前RML原生提供了中文编程支持,其中以 为
字结尾的Token,等同于以 :
结尾的设字,另外主要的函数都提供了对应的中文绑定。
打印 "你好,世界" ;中文版hello world
;下面是求10000以内素数的中文代码
集合为 []
历 甲 1 10000 1 [
是素数为 真
历 乙 2 (甲 除以 2) 1 [
若 甲 模 乙 等于 0 [是素数为 假 跳出]
]
若 是素数 [
添加* 集合 甲
]
]
打印 集合
即时帮助
RML的内置函数都定义在最底层的lib语境中,可以通过以下方式查看lib语境中的定义
lib? ;中文为 库?
RML支持在定义函数时添加关于函数和参数的描述,并可以在解释器中查看这些帮助信息。目前RML中初始化定义的中文函数都添加了相关的帮助信息,查看方式如下:
帮助 写出 ;英文对应 help write 或 ? write
;输出的帮助信息
FUNC:
desc: 将数据写出到文件中
args: 路径 要写出数据的文件
数据 要写出的数据,接受字符串和二元类型
props: /添加 无参,在文件的结尾添加数据而不是覆盖
性能
目前RML语言的性能很差。用于采用了逐字解释和动态作用域,整体性能相对主流语言有较大差距。尤其是在深层次递归函数运算中,由于语境的堆叠,影响了取值操作的性能。一定时期内,性能问题都不是RML主要考虑的问题。对计算性能有较高要求的部分,RML可以通过宿主语言定义原生函数,从而实现宿主语言的性能。例如求斐波那契数列,用RML实现 fib 30
在E5-1620CPU上大约耗时8秒,而通过在go中定义,在RML中调用的方式,则只需6毫秒,性能提高上千倍。
语言谱系
RML分属Lisp语系,对,就是那个一边被认为高逼格,同时又被疯狂吐槽括号太多的语言。从语言发展的历史来看,Lisp和C分别代表了编程语言的两个极点。C语言是贴近硬件和高性能的典范,而Lisp则是高度抽象的代表。虽然Lisp语言日渐式微,但由其首创的许多特性如 GC
在当今主流语言中得到了延续。
RML直接继承自Rebol语言。Rebol语言由Carl Sassenrath于1997年发布问世,并进行了持续数年的商业化运作,但最终没能获取商业上的成功。Carl是世界上第一个提供真彩色的操作系统AmigaOS的架构师,其编程功力可见一斑。
与Rebol的区别
目前RML和Rebol的主要区别有
- RML引入了语境类型的概念,区分为系统语境、用户语境和临时语境,同时引入了
put-word!
类型,用于在最近一个用户语境进行值绑定。目前RML中系统语境只有lib语境,循环及其他情况下由系统临时生成的语境属于临时语境,主语境和对象、函数则属于用户语境。使用put-word!
可以在函数内设置局部变量,取代了Rebol中使用的/local
。 - RML中取消了间接值的概念,所有Token的字面表达式前后一致。例如Rebol中一个函数
format
后再执行load
,得到的是[func [...] [...]]
,这个方块中有3个Token,而不是之前的单个函数,前后不一致。RML中统一使用!type{...}
的形式表达复杂数据类型,例如函数是!func{[...] [...]}
,其字面值解析后前后一致。 - Rebol不支持多线程,RML在Go语言的基础上提供了多线程并行功能。
- RML采用动态作用域,理论上拥有更高的动态特性,但性能也更差。
开发经过
本人于2015年接触Rebol,可谓爱不释手。
然而,自2012年开源起,Rebol已经几乎完全停止更新,目前仅有少数拥护者在维护自己的分支,Carl老先生似乎一头扎进机顶盒领域,不再留恋。而作为Rebol主要继承者的Red语言也在近年放慢了更新速度,持续跳票,1.0仿佛是遥不可及的梦。
在这样的形势下,出于对Rebol的热爱,以及希望检验自己对Rebol语言的认知,于是在2019年开启了这个项目。最开始是希望做一个完全中文化的Rebol,所以可以看到最早的版本使用的是习语言。之后由于个人感觉无法把握习语言这样的底层语言,所以换用nim。项目里的nim有两个版本,第一个版本是用面向对象的范式编写,对每段代码、每个表达式生成对应的执行器进行求值。第二个版本则是维护了一个栈,通过记录层层表达式的起止点进行求值,之后一直采用此种方式。用于当时nim语言尚未到达1.0,出现了诸多问题,于是换用D语言重写。D语言整体工程性良好,而且比较成熟,没踩什么坑,个人好评。之后又用Go语言重写,获得了更好的性能,并且拥有更强的语言生态,所以目前主要开发的是Go语言版本,其他版本可能不再更新。
开发目标
项目目标是使用最简单、清晰的代码结构实现RML的基本语法。依赖宿主语言(目前主要是Go)的生态,实现较高的可用性和可拓展性。
相关链接
github: https://github.com/NjinN/RML
相关: D语言版本实现分析