0%

语言高阶-lisp

很像数学

lisp 真的很像数学,或许这就是 lisp 的特征。它像是一个从计算器程序变成的计算机一样有趣。而 C 则更像是一种由内存驱动变成的计算机。

另外一个很像数学的点就是,lisp 试图用最少的基本结构(可能是数据结构,也可能是过程结构)来描述最多的复杂结构,就像数学在用最少的公理来推导最多的定理一样。

数学家会详细研究房子着火之后的救火步骤,当房子安然无恙时,他们则会将房子点燃,然后心安理得地救火。

语言是什么

语言是解决某个问题的工具,正是因为问题不一样,所以不同的语言也是不一样的。即使很多语言都是所谓的“图灵完备”的(其实我还是不知道这个概念的准确意思),但是也并不能认为语言学到最后都是一样的,同质性的。恰恰相反,不同的语言强调了问题的不同属性,他们在本质上就是不同的。

有人说,解决问题的过程就是创建一门解决这个问题的语言的过程,这并非说是要写一个编译器,而是它将工程中的抽象层(比如说 OS 层,函数库层)理解成了一门 DSL,我觉得是一个很有意思的东西,毕竟我们在 ruby on rails 中也看到了这样的实现。

这就又引出了一个很有趣的东西,就是如果将工程看成一层层横向的,那么其实和软件工程中强调的纵向的模块化开发是略带矛盾的。我个人觉得应当由善于架构的人先将工程问题分割成横向问题,然后再由善于考虑细节的人将每一层分割成纵向模块。

语言的边界

我是学 C 语言入门的,而且一直在做系统软件的开发,我这么爱 C 语言,但是也不得不承认,C 在描述世界方面,还是有他的缺陷的。

世界是什么样子的,世界肯定不是 C 语言描述的样子的,在 C 描述的世界里,数据和过程泾渭分明,过程并不能被加工(并不绝对,也可以强行跳过安全措施后改代码段),只有数据可以被加工处理。这和现实世界是不同的,木头可以被木斧砍伐,砍伐后的木头又可以变成木斧,数据和过程没有明显的界限。所以 C 语言在某些方面很贫瘠,比如说他没法很直观地描述求导过程,因为求导的本质是“由一个过程产生另一个过程”,这种本质性的缺陷,是没有办法用“函数指针”这样的特性弥补的。

保留全部的解释型语言

编译型语言和解释型语言的区别在于,编译型语言在执行时会丧失源码信息,其本质是丧失作者的意图。“运行时”是一种“盲人”的状态,它已经不能再思考了,只能机械地执行。而解释型语言在执行时并不会丧失源码信息,他在执行的时候是一种“全知”的状态,最直观的表现就是解释型语言的反射和元编程都做得更好。

所谓的“解释型语言没有编译时和运行时”的区别,其实就是上面表述的一种故弄玄虚的表达。

正因为保留了全部的信息,这才使得过程可以被编辑,解释型语言才是真正描述世界更贴切的语言,数据和过程融为一体,这次是客观世界的样貌。

Lisp 在没被求值之前是数据,被求值时就成了代码

语言的渊源

python、ruby 和 rust 上面真的有很多的 lisp 的影子啊。很多概念如果用 C 语言的角度去看,会发现是很难理解的,比如说绑定、过程、弱类型、函数式编程和 lambda 表达式等。如果用 C 的角度去思考,会发现真的很难理解为啥要有这种东西,但是如果用 lisp 的角度去看,就会发现一切都是有渊源的。

比如说弱类型对于 C 来说是不可理解的,因为类型对于编译型语言很重要,它和 ABI 有关系。但是对于解释型语言,弱类型似乎是天生的,它那种像一棵语法树的特征,就很像所有的结构都继承了一个“基类”一样。所以它所有的类型都是那个基类类型。

函数是唯一的公民

我们常说,函数是一等公民。不过有没有一种可能,函数是唯一的公民,即使是结构体这样的数据结构也可以用函数来表达。

状态和副作用

没有状态的语言所描述的世界是静态的。就像一幅画一样。有状态的语言描述的世界是动态的,是有因果的(因果比动态更重要),就像电影一样。

正是因为状态的不同,同一个函数,函数的参数也相同,但是执行的结果却不同。这是一个很复杂的东西,虽然在 C 中习以为常,只要引入了全局变量或者静态变量,就有可能会导致这种结果。副作用就是修改了它们。

其实很多数学都是无状态的,不会因为对一个稀奇古怪的东西求导,就会导致学二食堂的饭变好吃,求导唯一的副作用就是草稿纸多了,头发少了。不过很多算法是有状态的,比如说当快速排序运行完后,数组必须被排好了,而不是跟没排一样。

这样看似乎我们需要副作用,需要状态,因为我们总要改变些什么。

所以,千万里,就这样开始了因果。