凹语言中文语法设计
设计理念
凹语言的中文语法(下面简称凹中文版)的设计理念是:
- 简洁:尽量减少冗余信息。例如:关键字尽量选择单字。
- 易读:代码读起来应当尽量贴近自然语言。关键语法信息应当足够醒目。
- 灵活:不必拘泥于英文编程语言的传统语法,可以尝试灵活的设计。
- 符号:作为中文编程语言,并不排除,而是妙用标点符号和拼音字母。
凹中文版的语法设计主要受到了文言编程语言的启发。 但与文言编程语言的主要区别,在于上述的第一条理念:简洁。
我认为文言文相对于白话文,最大的特色就是简洁。 而简洁的需求正是由于时代的背景所决定的:当时的书写工具和文字承载工具都非常昂贵,因此惜字如金并不只是比喻。
因此,为了尽量继承文言文的简洁精神,我在设计凹中语法时,与文言编程语言的风格有了很大的区别。
凹中文版的语法设计还参考了:
- 凹英文语法。凹语言的中文和英文语法是相互兼容的,每个语法结构都能找到对应。并且到现在为止,凹中的解析前端还是和凹语言英文语法强耦合的。
- Go语言。凹语言初版的实现是用Go写的,且前端代码也是从Go语言的前端移植过来的,因此在设计与实现中会更倾向于贴近Go的风格。
- Kotlin和D语言。凹语言的中文语法设计中,也借鉴了一些Kotlin和D的语法设计。
提示:凹中文版语法还处于早期探索阶段,很有可能会发生变化。
我们计划在完成凹中文前端的重构(即完全脱离现有的Go前端)之时,得到一套稳定的中文语法。
现在可运行的示例,请参考凹语言工程中的可执行示例。
简单示例
下面是Hello World的凹中文版示例:
引于 "书"
【启】:
书·说:“你好,凹语言!”
。
上面的代码中:
引于
(import
)是关键字。【】
是函数定义的标志,相当于func
。书·说:“你好,凹语言”
是函数调用,相当于:fmt.println(“你好,凹语言!”)
:
和。
是一个程序块的开始和结束符号。相当于{
和}
。
这些设计都是为了简洁和易读原则而做出的选择。
这段代码用凹英文版写出来就是:
import "fmt"
func main {
fmt.println("你好,凹语言!")
}
下面是一个更复杂的示例,展示了其他几个已经实现的基本语法:
引于 "书"
【启】:
// 基本函数调用
书·说:"你好,凹语言!"
// 调用自定义函数
书·说:"[自定义函数]:40+2="
书·曰:加:40、2
// 简单的条件判断
书·说:"[条件判断]:"
设零之数 = 0
若零==1则:
书·说:"是的,零和1是相等的。"
否则:
书·说:"错了,零和1是不同的。"
。
// 简单的自定义类型
设某=点{横:10, 纵:2}
书·说:"[自定义类型]点(10,2)的纵坐标和横坐标之和:"
书·曰:某·横 + 某·纵
书·说:"[自定义类型]点(10,2)的纵坐标和横坐标之平方和:"
书·曰:某·平方距:
// 循环
// 类似range
书·说:"[简单范围] 从0到3:"
自0至3,有i:
书·曰:i
。
// 类似三段式for循环,注意,由于中英文语义不同,这里的j==8是停止条件,和for循环的“持续条件”正好相反
书·说:"[循环语句] 从0到8:"
从j=0,到j==8,有j++:
书·曰:j
。
书·说:"[循环语句] 从10到0:"
设步=1
从i=10,到i>=100,有:
i+=步
书·曰:i
。
// 类似until语句
书·说:"[直到语句] 直到5:"
设i=0
直到i>=5,有:
书·曰:i
i++
。
// 多路选择
书·说:"[多路输出]k=3"
设k=3
当k:
为1,则:书·说:"一"
为2,则:书·说:"二"
为3,则:书·说:"三"
否则:书·说:"不中"
。
。
《点》:
横之数
纵之数
。
【点·平方距】() => 数 :
归于:此·纵*此·纵 + 此·横*此·横
。
【加】(甲, 乙之数) => 数 :
归于:甲+乙
。
详细介绍
本节详细介绍上面几种语法的设计,以及为何这样设计的缘由。
函数定义和函数块
先看Hello World:
引于 "书"
【启】:
书·说:“你好,凹语言!”
。
在凹中文版里,函数的定义用实心方括号【】
来表达。方括号之中的是函数名称。
这里的【启】
函数,是整个程序的启动函数,即我们常说的主函数
。 本来我打算直接用以没有直接选择【主】
,但写出来之后,这段代码读起来就有“主说:‘要有光’”的味道了,总有点怪怪的感觉。所以只好换了个字。
其实最开始我的设计几乎相当于对英文版的关键字替换:
函数 主函数 {
打印(“温故而知新,可以为师矣。”)
}
这里遇到了设计的第一个问题,直译的关键字变成中文之后读起来太过生硬了,而且也不够简洁。
这种风格的中文编程早就存在了,我相信这也是很多人一听到“中文编程”,就会产生的第一印象。
我认为这里有历史的原因,但也有习惯的原因。
”函数“、”打印“这种双字词,最开始翻译时,是用于技术文章,而不是程序的,因此简洁并不是翻译者的第一要务。 当时面对这些中文还没有的技术性新概念,采用这种翻译是非常合理的。
实际上,最早的英文编程语言中,关键字读起来也是冗长而生硬的:"procedure"(后来被简化成proc),”function“(最近才简化成func\fun\fn), 可以看出英文关键字的演变也是有一个从陌生到习惯、从冗长明确到简洁的过程的。
中文没法像英文那么容易缩略,但中文也有优势,我们有非常多的单字词可以选择。 因此我当时选择了”方“这个字,表示”方程、配方“的意味,用来替代”函数“;又选择了”曰“这个所有人都认识的单字来替代”打印“:
方 启 {
曰(”温故而知新,可以为师矣。“)
}
这样已经有一点”文言”编程语言的意味了。上面的代码完全可以用“文言”编程语言的风格来读:
吾有一方,其名曰【启】,其方如下:
一、曰:“温故而知新,可以为师矣。”
方止于此。
这个风格其实用来编程已经没有太大问题,既有简洁又清晰,相当于“文言”编程语言的简略版,其效用和英文版几乎一致。
但我多看了几趟,仍然觉得有点别扭。
再看看、再瞅瞅,终于发现别扭的地方了:我们没有适应过高度抽象话的中文编程,所以下意识还是会把它当做中文句子来读。 那么,不符合中文文本惯例的地方,就会显得有些跳脱,也就有了别扭感。
而这里最大的别扭感,来自于【空格】的使用。标准的中文文本里,是鲜有空格的。
我们的标点符号是全角,本身就自带了空白分隔。因此在这里用空格直接连接“方”和“启”字,就会有别扭感。
空格问题有两个解决办法:
一是把空格改成文本和标点,回归中文叙述。这也是“文言”编程语言选择的办法。因此即使它用的都是文言文,我们也觉得读起来比较顺畅。 但这个办法会增加大量冗余文字,在程序比较简单的时候还行,一旦比较复杂后,多出来的字词读起来就会浪费精力了; 另一个问题是这样平铺直述的表达是线性的,用来表示嵌套的逻辑时很难搞清楚层级。
第二个办法,就是巧妙地利用中文标点符号自带的空白,以他们来顶替空格的作用。
【启】:
曰:“温故而知新,可以为师矣。”
。
仔细观察,这里不论是实心方括号【】
,还是中文冒号:
,都既有充实的间隔感,又自带了空白,而且是符合中文阅读时内心的停顿节奏的空白。 因此即使我用了很奇怪的组合:
和。
来替代{}
,都不会显得很别扭。 (实际上我最初选的结束符号是办公文本常见的■
,这个符号本身就是结束符的意思。但由于这个符号远没有句号。
好打出来,而它在凹中文版中又那么常用,因此我放弃它改用句号了。)
这个办法还有一个好处,类似【】
这样的中文标点非常醒目,因此用来定位程序关键要素时很方便。读代码时,目光一扫就能感受到大概有几个函数。
看看上面的“文言”的例子,最抓眼球的字,是不是就是【启】
?这也是为什么我选择实心方括号而不是空心的原因。
另外,由于【】
的特殊性,我们连方
这个关键词都可以不用了。
至此,读者应该还有一个问题,那问什么要把{}
也换掉呢?
【启】{
曰(”温故而知新,可以为师矣。“)
}
这确实也是让我头疼的一个选择。'{'的作用本质上是开启新的名字空间。编程语言的名字空间是树状结构,{}
开辟一个新的子空间,这里头可以继续引用上层的名称,但也可以新建只有自己(以及自己的子孙空间)才能访问的局部名称。要在程序中表达这样一个新的子空间,需要一个开始符号和一个结束符号。
常见的编程语言有三种方式来表达子空间:
- 括号。包括C系列语言的花括号
{}
和LISP系的()
。 - 缩进。Python等语言采用的方法。优点是阅读的简洁性更强,更符合自然语言习惯。缺点是层次太多以后容易搞错层级。
- 单独的
end
。Lua等语言用这个方法。很多时候开启新的子空间之前都有特殊的程序结构,例如上面的“函数定义”,所以不需要单独指定开启字符。但为了避免混淆,需要指定一个结束字符。end
就是最常用的结束关键词。这种方式比{}
更接近自然语言的风格,同时也不用考虑缩进的问题,算是前两种方法的折衷。
我本来选择的是第三种:
【启】
曰(“温故而知新,可以为师矣。”)
。
但多看了几遍之后又觉得有点别扭,最后还是把【启】
后面的冒号加上了:
【启】:
曰(“温故而知新,可以为师矣。”)
。
这个冒号虽然实际上是冗余的,但在阅读时提供的节奏感,我现在认为是必要的。 这样实际上又回归了和{}
完全等效的局面::
对应{
,。
对应}
。还挺合适。
最后一个问题,问什么用冒号来表示函数调用:
曰:“温故而知新,可以为师矣。”
曰(“温故而知新,可以为师矣。”)
再对比一下,其实没有本质的区别。非要说的话,怪我选的这个例子吧。实在是和《论语》 原文太像了:
子曰:“温故而知新,可以为师矣。”
这种平铺直叙的命令式,实在是太符合自然语言习惯了。
那么冒号会不会遇到问题呢?多个参数时怎么办?参数又是函数调用的嵌套时,又该怎么办?这个问题比较复杂,我会在后面函数相关的话题里专门描述。总之,一旦嵌套了,还是得有括号帮忙。
至此我们完成了HelloWorld的对比,也大致了解了凹中文版在语法设计时所衡量的因素。接下来,我们看看更复杂的情况。
变量
凹英文版变量的定义方式如下:
// 使用关键字var
var a: int = 1
a = a + 1
// 快速定义语法
b := 2
凹语言的变量定义和Go语言基本一致,主要的区别在于变量和类型之间用:
分隔,更接近Kotlin等语言的风格。
在凹中文版中,用关键字“设”来表示变量的定义:
设甲=1
甲=甲+1
而用来赋值的操作符是=
和英文版一致。
当然,类似abc这样的拼音符号,我认为在中文编程中并不需要避讳。因此上面的代码也可以写成:
设a=1
a=a+1
要指明变量的类型,如果按照英文版的方式来的话,会是这样:
设a:数=1
a=a+1
但我感觉这样读起来会有些卡顿感。由于中文缺乏空格,这里的冒号就显得太突兀了。况且冒号已经用在代码块和函数调用上了。
解决办法有三种。
一种是变量和类型一起用括号包裹起来:
设(甲:数)=1
或者类似于函数定义,用独立的符号把变量名包裹起来,而不是包裹类型:
设「甲」数=1
第三种是直接把“之”字做成关键字,用来替代冒号:
设甲之数=1
现在暂时没有找到最佳的方案,我决定暂时用“之”关键字分隔的方式。等有了更多的代码体验之后,再确定正式方案。
数据类型
凹中文版最初版本只支持两个基本类型:《数》和《文》。它们分别对应与Go语言的int
和string
类型。更多的类型支持,留待整个编译器雏形搭建好之后再慢慢扩充。
复合类型中最常见的是数组(array)和映射(map)。
【启】:
// 数组
设a=[1,2,3]
// 映射
设b={1:2, 3:4, 5:6}
。
如果需要指明类型,则这么定义:
【启】:
// 数组
设a之[数]=[1,2,3]
// 映射
设b之{数:数}={1:2, 3:4, 5:6}
。
这里数组和映射的类型名称语法和英文版不一样。英文版沿用了Go的写法:
- 数组:
[]int
- 映射:
map[int]int
而中文版则选择了和字面量几乎一致的形式。
- 数组:
[数]
- 映射:
{数:数}
这也得益于中文版并没有采用{}
来表示代码块,因此可以把{}
留给映射的类型标记用。
TODO: 这个功能暂时还没实现。
自定义函数
凹英文版定义函数的语法如下:
func add(a: int, b: int) => int {
return a+b
}
func main {
println(add(1, 2))
}
和Go语言类似,但有两点区别:
- 参数列表和返回类型之间有个
=>
符号分隔,这样让函数定义在整个文件中更醒目。当前版本这个=>
是可以省略的。 - 如果函数参数为空,则可以把括号省略掉,比如这里的
func main
就直接接{
了。
凹中文版的形式如下:
【和】(a之数、b之数)=> 数:
归于:a+b
。
【启】:
曰:和:1、2
。
这里:
- 【和】是函数名,
【】
表示定义一个新函数,相当于func
。 - 参数列表和英文版基本一致,只不过参数的分隔由逗号
,
变成了顿号、
。 归于:
是关键字,和return
一样。
这里为什么选择和英文版几乎一致的形式?是因为我做过几个其他尝试之后,并没有找到更清晰且可读的方法。
因此在初始版本里,这么写已经足够好了。并且也符合凹中文版的设计原则:简洁、清晰可读且妙用了符号。因此我就不去特意发明新符号去替代了。
这里只有一个小遗憾,=>
这个箭头在中文里是没有的,用英文版的话,也没有足够好的空间,必须在右侧加一个空格。我现在用的字体会自动把=>
两个字符转换成⇒
这一个字符的样子,因此看起来还比较和谐。但由于⇒
这个字符没有办法直接输入,只能战术放弃。
函数的调用
函数的调用采用的是“:”加参数列表的格式,而不是传统语言中双括号的格式。
和:1、2
相当于add(1, 2)
。
如果需要嵌套,可以用括号表示优先级:
积:5、(和:2、3)
相当于:mul(5, add(2, 3))
可以看到,凹中文版的函数调用语法的实际效用,和英文版并没有本质差别。
用:
的主要好处是增加了简单调用的可读性,让普通代码中的一行行函数调用看起来更像是对话。但缺点是遇到复杂的调用组合,可能可读性不如英文版。我觉得这里也可能有习惯性的问题,所以打算先试用一段时间,看看是否真的有这个问题。
另外,函数调用的冒号和代码块的冒号是有冲突的,这一点需要再仔细验证一下。如果不行,可能考虑回归传统的()
调用。
还有一个问题,即如果没有参数,该如何表示?
我暂时的设计是如果没有参数,还是回归()
【感叹】:
书·曰:“呜呼!”
。
【启】:
感叹()
。
总之,这里的设计还是不太成熟,需要再探讨探讨。
类型
用户自定义类型在高级编程语言中是非常重要的设计。很多设计模式都是依托于类型系统。
凹中文版的类型系统基本继承自Go语言,即不支持继承等传统OOP,而支持面向数据的类型体系,以及基于组装(composition)和接口模式的类型体系。
先看最基本的类型定义。让我们定义一个“点”类型(即Point),它的有两个成员,一个表示横坐标(x),另一个表示纵坐标(y),类型为整数(int)。
凹英文版:
type Point struct {
x: int
y: int
}
func main {
p := Point{x:1, y:2}
println(p.x+p.y)
}
凹中文版:
《点》:
纵之数
横之数
。
【启】:
设p=点{横:1,纵:2}
曰:p·x+p·y
。
这里和英文版唯一的区别就是用《》
来表示类型定义,替代英文版的type <name> struct
。 在struct中,成员的定义和英文版一致,只是用之
来代替英文版的:
,用于分隔成员名称和类型。
类型的组合模式还没有仔细研究,初步设想如下:
《三维点》:
有:点
深之数
。
这里的关键字“有”表示has-a
关系,即《三维点》中包含《点》的成员。这样实际上和英文版的组合模式是一样的。
方法
方法是与类型绑定的函数。本质上它的运行和普通函数是一样的,但与类型绑定之后,我们可以非常自然地使用“主谓宾”的语法结构,而不是传统函数的“谓主宾”结构。
方法还有其他好处,比如可以和接口模式或组合模式结合起来,实现更复杂的类型系统。
凹英文版的方法定义如下:
func Point.Length() => int {
return math.sqrt(this.x*this.x + this.y*this.y)
}
fn main {
p := Point{x:1, y:2}
println(p.Length())
}
和Go不同之处在于,凹语言的方法定义之比普通函数在名称前多了一个前缀。Point.Length
,表示Length
方法是属于Point
类型的。 这种方式和Scala、Kotlin等语言风格类似。
在方法之内,用this
关键字表示Point
类型的实例;即,在p.Length()
中,this
其实就是p
。
凹中文版也沿用了这种方法,只是类型和方法名之间的间隔改为更适应中文的·
,而this
改为此
:
【点·长度】=> 数:
归于:开方:此·纵*此·纵+此·横*此·横
。
【启】:
设p为点{横:1,纵:2}
曰:p·长度()
。
控制流
if-else语句
凹英文版的if-else
语句如下:
a := 1
b := 2
if a>b {
println("bigger")
} else if a==b {
println("equals")
} else {
println("smaller")
}
凹中文版则是:
若1>0则:
曰:“1>0”
又若1=0则:
曰:“1==0”
否则:
曰:“1<=0”
。
这里的关键字“若”表示if
,关键字“又若”表示else if
,关键字“否则”表示else
,每个条件之后加了则
关键字,以增加可读性。
循环
凹英文版的循环有三种形式:
- 三段式for循环
sum := 0
for i:=0;i<10;i++ {
sum += i
}
println(sum)
- while循环
i := 1
for i <= 10 {
println(i)
i++
}
- 无限循环
for {
println("looping...")
if condition() {
break
}
}
第一种三段式循环,for <初始化>;<条件>;<递进>
,三段操作分别是初始化循环变量、判断循环结束条件、以及每次执行完循环体之后做的递进更新。 第二种和第三种形式其实是这三段操作的省略而已。
凹中文版这样支持三段式:从<初始化>,到<结束条件>,有<递进>:<循环体>
。例如:
设和=0
从i=0,到i==10,有i++:
和+=i
。
这个语法还是比较清晰的。
注意,我没有找到一个关键字可以表示“只要条件成立,就继续执行”的意思,所以只能用到
这样的字表示“只要条件城里,就结束循环”。正好和for循环的条件是反的。
没办法,这就是中英文的习惯不同。
非要和for
循环一致的话,大概只能:
设和=0
从i=0,若i<10,则i++:
和+=i
。
但只读这句话,总感觉想问“这个到哪里才结束啊?”。总之还是别扭。
为了支持for
循环的习惯用户,我还是支持了这种写法。
另外,由于用关键字替代了符号,在省略时就不如for
循环好看了:
设i=0
设和=0
从,到i==0,有:
和+=i
i++
。
这显然好看,所以我加了一个新关键字“直到“,可以这么写:
设i=0
设和=0
直到i==0,有:
和+=i
i++
。
这就和while
差不多了。或者更确切的说,相当于until
语句。
至于全部省略的无限循环,只好用类似于“循环”这样的关键字了。
循环:
若xx则:
停止
。
。
这显然不太雅观。还需要再改进。
switch语句
凹英文版的switch语句:
a := 2
switch i {
case 1:
println("one")
case 2:
println("two")
case 3, 4:
println("three and four")
case i < 10:
println("smaller than ten")
default:
println("nah")
}
中文版初步的想法是模仿Kotlin的when
语句:
当i:
为1,则:曰:“一”
为2,则:曰:“二”
为3,则:曰:“三”
否则:曰:“不中”
。
关键字当
、为
、则
、否则
分别表示switch
、case
、:
和default
。 这个语法和when
其实也一致,因此未来可以扩充到更复杂的语句。
接口
凹英文版的接口:
type duck interface {
quack()
}
这样,包含quack()
方法的类型都可以当做duck
来看待。
在凹中文版中,类型定义是双书名号《》
,因此接口的定义自然用单书名号〈〉
,所以:
〈鸭子〉:
【嘎嘎】
。
意思就是“鸭子”这个接口,包含一个“嘎嘎”方法。这样,任何包含“嘎嘎”方法的类型都可以当做“鸭子”来看待。
注:这个特性现在还没实现。
展望
到现在(2023年4月初)为止,上述的中文语法几乎都实现了,但是还有一些细节需要完善。
凹中文版2023年的目标就是完全重写前端解析模块,完善所有语言特性的语法,与英文版做到100%对应。并作出正式的凹中文版标准语法。
届时就可以考虑给中文语法扩充更贴近中文用户使用习惯的特殊新语法了。
凹中文版的目标,不是让人人都用中文版语法、抛弃英文版语法,而是想抛砖引玉,启发所有人都来设计与开发更适合总国人的编程语言。