浅谈编程语言和自然语言的可比性。
引言
语言是沟通的工具。沟通意味着两个过程:输入和输出。自然语言的听说读写,听和读是输入,读和写是输出。编程语言也有输入和输出的两个过程,但其特殊性在于,它的输入者(读代码的一方)需要是计算机和(很多时候,比如修改代码时,也需要是)人类,输出者(写代码的一方)往往是人类。计算机和人类各有其所擅长的领域、各有一套迥异的行事模式,这决定了编程语言需要达到人机之间良好的平衡。在迎合人类思维需求的这一方面,自然语言的影子就往往会渗透进编程语言的设计之中。从另一方面来说,编程语言和自然语言的这种可比性,从根本上来说,源自二者皆是对事物发展过程的一种描述。如何对两者进行有意义的比较是一个有趣的话题,同时也是有价值的,因为一方面可以更好理解编程语言中的各种概念,另一方面在某种意义上可以提供对“双语”学习宏观视角上的指导。王垠(2018)就类比了英语语法和程序语法之间的关联。本文一记我对这个话题的看法。
形式
书写上,自然语言依赖于各种字母表或者汉字,编程语言往往由ACSII字符组成,主要是英语字母、数字和各种符号,这些构成要件首先在自然语言中被广泛地运用,再出现在计算机键盘上,为编程语言所采用。由此可见,编程语言的书写形式很大程度上依附于自然语言。
自然语言的另一种形式:声音,是编程语言不强调的,因为自其功能而言,计算机运行代码,并不需要朗读代码。当人们需要朗读代码的时候,编程语言的声音系统更是依附于自然语言的。
关键字
编程语言的关键字对应的是自然语言中的词汇。编程语言一直在尽力使用自然语言(常常是英语)的词汇作为其关键字,以增强其可读性。if
传达出的条件控制语义,和英语中“如果”的意思是很贴合的。SQL语言中的SELECT 、FROM、BETWEEN … AND …等等,也是“自然语言可读的”。再如ES6引入的let
,让声明(可变)变量的语句直接等于相应的英语语句。新近出现的文言lang之所以让人叫绝,主要是因为其编程语句实现了“文言可读”。
为什么学习较为古老的Shell脚本会让人觉得十分困难?因为它过分依赖了各种符号,而非自然语言作为关键字进行表意。对于计算机而言,只要有明确的对应法则,它总是能轻松对应符号与相应的规则,不会进行任何抱怨;但对于人类来说,记住${ #foo}
表示取foo
变量的长度是比较困难的,尽管关键字#
通常表示数字,但这里表示取长度的对应法则并不一目了然。这就是为什么在当下更为流行的语言,如Python中采用len(foo)
、JavaScript中采用foo.length
,关键字从自然语言的角度是更容易理解的,很容易看出其表达的意思是”the length of foo
“。
规则的多样性
编程语言中的表达式对应的是自然语言中的短语或者句子,一种由关键词加上语法结构组成的结构。语法结构是语言规则的直接体现。
编程语言,出于其自身要求的可实现性,对语法的严谨性要求更强;相比之下,经过长期历史演化的自然语言,通常具有更为松散的语法结构,这体现在各种规则的常常难以用逻辑解构的“例外”上。然而,自然语言中的语法结构中多样且不规则的特征,影响了编程语言的设计,并导致了编程语言中往往具有的语法结构规则的多样性。
为了说明自然语言中规则的多样性,我们不妨先看看一个所谓规则非常简单的自然语言——世界语。以下引用自知乎专栏:
构词法:比较有趣的一点,世界语两个意思相反的词,通常用mal或非mal来表示。也就是说,以mal为首的词,都要比其反义词多出一个音节。并且世界语里所有意义相反的词都这么处理。例子:mallonga(世界语),short(英语)。
看见带mal的词时,反应速度依然不快。比如malproksima,我首先一看到mal,只会意识到它是一个词的反义词,再看到proksima(近的),哦,那就是它的反义词“远”啰。总是存在一个思考过程。
而且这还会使没有高低优劣之分的一组反义词不对等,如dekstra(右,right)和maldekstra(左,left),世界语就把优先权搬给了dekstra。
除此之外,我还觉得这使世界语表达的丰富性下降了。
这种构词方式已经开始影响到了我对词义的理解。Rapida的意思是“快”(fast,rapid)。要是malrapida出现在我眼前,它给我的第一印象是“不快”,只有在翻译句子时才意识到要翻译成slow(慢),而不是not fast(不快)。
稍微了解了一点世界语的造词方式后,很容易意识到规则的简单并不是自然语言应该追求的,也不是我们惯用的汉语、英语等自然语言的内在构造逻辑。
如何理解编程语言同样蕴含着语法规则的多样性?简单的例子是,我们可以认为函数也是一种特殊的变量,例如
1 | const double = x => x * 2; |
或
1 | double = lambda x: x * 2 |
然而声明函数往往采用的是和声明变量不同的语法结构,即往往不用=
去赋值函数,而是用
1 | function double(x) { |
或
1 | def double(x): |
虽然有时这种写法的差异并不是syntactic sugar(差异如递归的可用性),但这揭示了一点:编程语言不惜创造新的语法结构去表达一种较为特殊的情形。这种逻辑是蕴含在自然语言的演化之中的,自然语词的创造往往不是从逻辑出发,而是从人们的对事物的较为直观的认识和知觉出发的。惯用自然语言的人们是习惯于这种特殊性或缺乏逻辑的,这显然是被编程语言的创造者考虑的。
另外一个简单的例子就是代数表达式,比如1 + 1
,这是编程语言对自然语言最赤裸的迎合。对于惯用自然语言来表达算数的人类来说,“一加一”是非常寻常的。可是,仔细思考一下+
对计算机意味着什么:
- 如果
+
是一个函数,那为什么我们不写+(1, 1)
? - 如果
+
是整数类型的一个方法,那么为什么我们不写1.+(1)
?
当然,你确实可以在Ruby里采取后一种写法,但一般没有人那么做,因为1 + 1
是完全可用而且更加可读的。
最后再说一说语法结构非常简单的语言Racket(或者影响它的Scheme),它排斥语法上的“例外”(所有表达式一律是(e, e1, e2, ..., en)
,其中e
表示过程,e1, e2, ...,en
表示参数),因此1 + 1
要写成(+ 1 1)
。这种语法结构对计算机是非常友好的,但并不非常人类友好,因为程序写出来非常反直觉,另外会有多到令人发指的括号。
总而言之,1 + 1
这样的中缀表达式对于编程语言来说是不寻常的,这是一种对计算机来说新颖甚至复杂的规则,迎合着人类使用自然语言的习惯。
编程范式
编程范式,对应的是自然语言中的架构段落或文章的方法。
此处略谈一下 imperative programming 和 declarative programming,这两者分别有个子集是 procedural programming (过程式编程)和 functional programming(函数式编程)。它们的差别和对照不是本文的重点,所以此处不严格区分。最后提一笔OOP。
过程式编程
过程式编程,对照自然语言,更像是一句话一句话说下来。比如随手写一个函数:
1 | function f(x) { |
这是典型的 procedural programming,也是典型的 imperative programming。用自然语言是容易解读的。
The function
f
needs an argumentx
.Let
y
equal tox
times 10.If
x
is greater than 0, then lety
increment by 1; else lety
increment by 2.The returning value of this function is
y
.
这也不难理解为什么 imperative programming 的名字就是从自然语言中祈使语气(imperative mood)来的。procedural programming 其实是目前大多数编程语言采用的范式,或者大多数人学习编程采用的范式,因为其实这非常符合自然语言类似故事情节的叙述。类似“从前有座山,山上有座庙”:先定义一个山,再用下一个句子补充山的特性或者充实对山的描述。从动作或行为的角度考虑,人们也是常常愿意先做一件事、然后再做另外一件。
函数式编程
与之相对的是 functional programming:
1 | fun f x = |
这个过程不是用若干个句子进行描述的,不是先做一件事、再做一件事,最后拿出一个返回值给你;而是,整个函数就是在用各种结构去构造一个最终结果。这里没有句子,只有一个用各种语法结构去修饰的短语作为结果。如果非要用自然语言去表达,那就大概是(为增强可读性加了括号):
The function
f
ofx
is ((y
plus (1 if x is greater than zero) (2 if not)) wherey
equals to (x
times 10)).
这会更像是一句冗长的“从前有座(上面有个庙的)山”。这种和自然语言相悖的思维方式,很有可能是当下函数式编程不太流行的原因。不过,数据库语言如SQL还是按declarative programming这种思维模式去写的:总是类似SELECT [some column] FROM [some table] WHERE [some condition is satisfied]
的架构,但是诸如subquery这样的操作可以把填充进去的东西变得非常复杂。
面向对象编程
面向对象编程,在自然语言中,就是集体和个体的区别。当我们说everyone的时候,其实是在说一个集体,但用的谓语是单数的,原因在于落脚点在”one”上了。写class的定义的时候,其实也是在说明一个集体的特征,但写的时候,也只是在描述每个实例所具有的属性或方法。对于每个个体来说,属性是名词性的(描述性的,往往可以说是名词化的形容词),方法是动词性的(这个实例可以干嘛)。
不同词性的词
自然语言中各种词性的词在编程语言中有一定的体现。
名词和代词:值和变量
“名词”在编程语言中相当于“值”,“代词”相当于“变量”,这基本是不言自明的。
形容词:名词化
在程序的世界中,往往是没有形容词的,因为形容词总是模糊的。我说“大”,你问我“多大”,我只能告诉你是值是多少;我说“红”,你问我“多红”,我得给你一个RGB值。这量化的过程,就把形容词名词化了。不过CSS里面调font-size
的时候可以直接写个large
,写color
也可以直接red
,当然这只能算是syntactic sugar,其实内部一定是转换成字号或RGB的。
动词:函数
“动词”的对应是“函数”,因为两者都强调一种“行为”。从编程语言的角度看,pure function强调无side effects,更接近数学上的函数,那就是说,这种行为只侧重于把输入值利用某些规则“变成”输出值,而不产生其他影响;procedural programming中side effects往往是必要的,这更像是复杂的自然世界中各种行为产生的后果,例如带来的各种状态的改变。一个笑谈是,如果把人的一生看成一个pure function,那就是大抵就是
1 | const life = person => null |
“人生是让人变成虚无的过程。”——因为任何人最终都是要化成一缕青烟的,由此可见人生的意义主要在于side effects。这可能是procedural programming流行的另外一个原因?笔者没有考证过,但不妨确信如此。
如果函数是动词,如何看待first class functions,即把函数看成其他变量作为另外函数的参数或返回值?这很容易让人想到英语中将动词转化成名词性的做法,用doing
或者to do
的结构。这是很有趣的。
1 | const increment = x => x + 1; |
很容易把map
里面的increment
想成to increment或者incrementing,像”What map does is to increment every element in [1, 2, 3] and produce a new array”。
文首提及的王垠的文章把动词看成句子的核心,的确,因为编程语言中函数的调用往往也是程序的核心。知道一个函数(function)需要什么输入值、会产生什么输出值、会有什么side effects,才能有效地利用它提供的功能(function)。知道一个动词的用法才能知道怎么用它进行造句,从而使用它提供的表意功能。
词典或文档
可以说,说话作文就是在调用语词的API。词典之于自然语言,正如文档之于编程语言:它们都揭示了我们应该如何去使用这门语言,说明了根本的规则,并提供适当的样例帮助理解。学习语言也需要常常翻看词典,学习编程需要常常查阅文档,这道理是一样的。
编程语言由于自身更强的逻辑性,这种规则更容易得到描述;自然语言的规则有时是模糊的,因此需要更多的样例提供对语词用法的直观的感知,这就是词典中例句或者collocation为什么在语言学习中具有很大的重要性,它们帮助语言学习者理解:在什么语境中这个词语应该被使用?这个词语应该进行什么样通常的搭配?这是调用语词,以达到其表达效果必要的知识。
结语
编程语言和自然语言的比较是一个很大的话题,远远不是这个四千字的短文能穷尽的。本文也仅仅是浅尝辄止,略谈一二,以飨读者。