本文刊发在《程序员》杂志09年第二期上。是讨论函数式语言基本性质和发展方向的一篇文章。
表面的简洁/strong>
一、把大象装进冰箱
=====
在命令式语言(当然我们可以确指为C、Delphi、Java或C#等等)中,初学者的第一
个疑难便是这样的代码(*注1):
X=X+1
为什么?因为在数学概念中,上述等式是不能成立的。这种表达式是计算机的思维逻
辑:当它运算上述表达式(或语句)时,X被作为暂存单元——例如冰箱。为了让冰箱产
生变化,比如解决“把大象装进冰箱”这样的问题,我们需要如下三步:
把冰箱门打开,把大象放进去,把冰箱门关上
(图1:“把大象装进冰箱”的问题)
因为我们有两只手来分别负责拉住冰箱门和大象,所以整个操作过程看起来很完美,
但接下来我们再加上点需求:我们要“把大象拿出来,把长颈鹿装进去”,怎么办?是的,
应该这样:
把冰箱门打开,把大象拿出来,把长颈鹿装进去,把冰箱门关上
可惜长颈鹿有腿有思想,所以问题将会出在我们把大象拿出来的那一时刻:长颈鹿跑
掉了。为此,我们必须做很多的防护措施,例如先锁住长颈鹿,再锁住大象,以及在整个
过程中,保证冰箱门不会自动关上或打开……而代码:
X=X+1
的执行过程与此类似:当CPU开始存取X这个位置时,它只能在“当前X”与“下一次X”
之间选择二者之一。当多个线程(或多个CPU)开始存取X这个位置时,如果我们希望得
到相同的X值,那么我们就得在X操作过程中采取象上面一样的防护措施:加一个锁。以
保证要求所有线程都取完了这个“当前X”值,才会被切换到“下一次X”值。
由于这个限制,一旦多个线程都排着队来看看这个X的美妙身形,整个队列就全都慢
下来了。
解决这个问题的办法其实很简单:只要X可见,我们就永远不修改这个X。而这,就
是函数式应对大象问题的方法:如果冰箱里放着大象,就永远不要试图放长颈鹿。所以在
函数式语言Erlang中的变量一旦赋值,就不能再修改。
云风曾在SD2C 2007大会上说:解决问题的最好方法,就是不解决它。这个观点深得
函数式的精髓。
二、帽子戏法的关键,在于至少多一顶帽子
=====
杂技中的一种常见帽子戏法,是很多人围成一圈或排成一排然后飞速地传送手中的帽
子。这如果是一个人来表演,那么应该是左右手各一顶帽子,而多出的一顶则总在头上。
(图2:帽子戏法)
所以,关键在于至少要多出一顶帽子。正因为多出来这个帽子,所以我们看到杂技师
在我们面前构建了一个往复不休的循环。事实上程序设计里的“循环”也存在完全相同的
问题:我们至少需要一个变量来保存迭代中的计数,而且这个变量必须是可以修改的(*注
2)。然而这一要求既是玩转“帽子戏法”的必要条件,却又与我们上面讲过的“不修改变
量”的原则相违背。
函数式如何解决这个问题呢?
其实答案还是相同的答案:至少多一顶帽子。只不过,帽子不一定要放在头上,我们
可以把它放在传递的过程中——例如空中——就可以了。要知道,让杂技师用同样的方法
来掷苹果,那多出来的一个就总是在空中了。
在函数式中,我们如果要构建一个循环,那么可以使用函数递归来实现。这上述控制
循环过程的变量,则可以把它放在函数形式参数表——这种类似“空中”的地方。与“空
中”相同的是,我们在静态看函数时,那是参数表;而运行中时,它传入实参。
然而帽子戏法的表演者并没有三只手或更多只手,被循环帽子增加的时候,杂技师除
了加快速度之外,保证一个简单的原则也是极其重要的:总是从帽子队列的最末端取到下
一只帽子。这一原则保证了可以容纳更多帽子,而又不会少处理任何一个。同样的,递归
是消耗栈的,为了使栈空间不爆炸,解决的方法就是在最后一行代码上调用递归,即尾递
归。因为尾递归的存在,函数最末的调用就可以被优先为一行不消耗栈的跳转指令,就像
帽子戏法的杂技师从帽子队列上直接取走轮转到手边的帽子一样。
最后,对于一个函数来说,如果它只返回值而不修改函数外的任何东西,那么这个函
数就是安全的,它等价于它返回的这个值——如前所述的,这个值一旦有效(运算出结果
并传出),就不再变更。所以函数式中的函数调用,可以等价于一个表达式中运算的值。
如同函数调用,函数的递归也只返回值,所以也等价于一个表达式中运算的值。再进
一步的推论,递归实际上等效于循环求值。
复杂的表象下,总会有一个简单的原则。万人的与一个人的帽子戏法,其原则是一样
地:至少多一顶帽子,放在头上,或是空中。
三、计算机其实不认得"hello"
=====
32位的unicode,以及128位的GUID等等,都直接与我们现在或将来的存储单位以及
运算的通道大小有关。事实上即使我们有128位机器,我们也只打算在这样的通路上传送
一个字符"h"——而不是字符串"hello"。从更为准确的角度来说,事实上计算机也不认得字
符"h",而只认得数字0x68。同样的,它也不认得所谓的“真假(true/false)”,而只认得数字
的1/0。
(图3:图灵机的概念图)
我们编程的本质,其实不过是在求值一些数字而已。只是最终我们在自然语义上把这
些数字的一个连续或非连续的集合认为是布尔值、字符串、数组或对象。当我们认识到运
算求值的结果无非是数值,而表达形式又无非是连续或非连续时,我们就得到了基本的数
据抽象单元:值、值系列。再加上我们前面讲到的执行体(函数),我们就得到了整个函数
式语言的鼻祖——LISP——的基本运算模型:
(+ Xn)
其中,“( )”表明一个值系列(表),而“+”在这里指代某种运算,Xn表明值(或值
系列)。整个的表返回一个值,因此也可以将“整个的表(通常这里称为表达式)”等义为
一个值。任何的一个运算,最终输出的仍然只是一个或一系列数字,它被显示在屏幕上,
便成了文本;放在内存中,便成了数据。
当然,现实是这样的机器最终从科学领域走向了民用,在PC(个人计算机)普及的现
在,我们也需要让类似LISP这样的——绝对正确而又绝对非人性的——语言变得亲切一
些。于是稍微复杂而有用一点的函数式语言,例如Erlang,通过丰富了上述的基本运算模
型来使我们的视觉愉悦,或是在讨论它的代码时显得神经(略为)正常一些:
(表一:以Erlang为例的、简单的类型抽象)
而我们逆推一份具体的Erlang代码,其实仍然可以表达为上述的(+ Xn)。例如我们可
以在Erlang代码在编译阶段使用解析树(Abstract Form)中看到这样的抽象代码(abstract
code):
[{abstract_code,
{raw_abstract_v1, [{attribute,1,file,{"./simplest.erl",1}},
{attribute,1,module,simplest},
{function,3,test,0,
[{clause,3,[],[],[{atom,4|...}]}]
},
{eof,1}]
}
}]
无论是从形式,还是从实质来看,这种解析树(在erlang执行中将会按照解析树来生
成语法树并执行)与LISP语言的基本原则都是一致的。
四、从“函数等义于值”到“函数是值”
=====
现在,JavaScript语言被更为深入的挖掘并渐渐了解到它的函数式语言本质,而类似
Erlang这样的“天生伤害人的视力”的语言也移步前台。这些语言的努力,使我们终于看
到一个属于函数式语言的时代的曙光。在黎明之前的黑暗中,函数式以它诸多的、最不可
思议的特性迷惘着程序员的目光。连它最基本的概念说明,也如同玄学家的呓语:如同数
学函数是集合A(称为定义域)中成员到集合B(称为值域)中成员的映射,函数式语言
就是通过数学函数的定义、应用的说明和求值完成运算过程的。
类似于这种等同于“什么也没说”的解释,其实的确是在阐述函数式语言的精髓。为
了减轻你的痛苦(但绝非轻视你的智商),我通常换个说法来陈述它们:如果表达式“1+1=2”
中的“+”被理解为求值函数,那么所谓函数式语言,就是通过连续表达式运算求值的语言;
既然上面的表达式可以算出结果“=2”,那么函数式语言自然也可以通过不停地求值找到问
题的答案。
首先,在函数式语言中,函数只表明一个运算过程,并产生一个运算结果。这与表达
式中的运算符具有完全相同的性质——所以事实上一个函数式语言中,表达式的运算符被
实现为一个函数。例如erlang的核心模块中,可以导出类似这样的函数:
(表二:Erlang中的运算符其实对应于内核函数-部分举例)
所以,函数式语言的本质是表达式/函数的“连续求值”。既然我们的输出或存储最终
只是在关心“值”,那么显然连续求值的结果就可以直接作为他们的输入。如果把输出终端
或存储看成接受输入的设备,那么他也相当于一个函数;如果一台计算设备只对外界表达
为一个或一系列输出,并接受来自其他类似设备的输入,那么计算设备本身也可以视为一
个函数……
我们将这个过程放大,其实网络可以是函数式的。这个就是著名的语用网,它的理论
基础是petri网论(Petri nets theory)。而事实上,作为计算模型来理解,它与函数式语言是
相似、等价的(*注3)。
图4:perti网的“库所变换”
函数式语言通过函数实现了三个基本的运算逻辑(顺序、分支与循环),因此它与我们
常用的命令式语言是等价的(*注4)。但是由于存在存储问题,所以命令式语言是时序相关
的——即有存取某个存储单元的先后问题。而函数式语言由于“把大象装进冰箱”之后就
再也不可更改,因此变得时序无关。
以上述的petri网的例子来讲,由于时序无关,所以图左侧的“库所(圆圈)”中的两
个“消息(黑点)”分解成右侧的两个库所来处理时,其转换的代价为0(无锁),而这个
过程应用在多核机器或分布式网络上时,效率却提高了一倍。
更深层次地思考这个问题,由于在计算机系统中函数本身仍然是以数据形式存储的,
所以函数事实上也是“被运算的对象”和“运算结果”。函数的这种特性被称为高阶函数。
“函数等义于值”是函数式的基础,而“函数是值”,则是高阶函数的基础。
图5:JavaScript统一语言范型的基本模型(*注5)
当“函数是值”时,我们可以把一个函数传递到另一个地方去运算,而其运算结果仍
然是值,所以可以把一个等义的结果再传回来。注意这一过程,就是分布运算的实质,所
以,函数式在本质上、天生地就是支持分布运算的。无论我们是将“一段函数式代码”所
表达的整个运算过程分解成何种形式,并分布在何种复杂的运算环境或网络环境中,只要
最终在逻辑上它能得到一个值序列和一个运算,就能够成为更大范围的分布网络中的一个
结点。
而这,就是整个计算世界的全部(注6):(symbol)。
==========
注1:《Erlang程序设计》中,作者以这个例子为起始,来讲述Erlang变量的单一赋值。
注2:参见《结构程序设计》,讨论“如何刻画计算的进展”时,作者E.W.Dijkstra说:
(程序)如果含有循环语句,仅用语法指示器就不能描述计算的进展了……(而应该)引
进一个“动态指示器”毫不含糊地累计相应的现行循环的序数……因为,语法指示器无法
充当这种坐标系统的一个组成成分。
注3:"Implementing Coloured Petri Nets Using a Functional Programming Language" at
http://portal.acm.org/citation.cfm?id=993039,and Functional Nets at http://lamp.epfl.ch/fn/
注4:不同范型的计算机语言之间等价问题,可以归结到图灵等价这个命题上,这意
味着该运算系统或模型能够执行任何复杂程度的、图灵机可完成的数学运算。2007年,Alex
Smith证明了Wolfram提出了最小的“2,3图灵机(两种颜色,三种状态)”模型是最小完
备的图灵等价系统。
注5:这个统一过程用到了多项与函数式相关的基本设定:函数是执行体、函数等义
于值、函数是值。
注6:与“(+ Xn)”比较,这个表达式认为“运算 +”——也就是某个函数——其实也
是值,因此它也是“值系列”Xn中的一个部分。于是,当由自然语义中的symbol来指代
Xn时,整个表达式就变成了:(symbol)。建议阅读作者的其它两篇文章:
《从表达式到变量:一行scheme代码之所见》
http://blog.csdn.net/aimingoo/archive/2007/02/12/1508118.aspx
《从表达式到函数:表面的简洁》
http://blog.csdn.net/aimingoo/archive/2007/10/08/1815379.aspx
分享到:
相关推荐
这样表面简洁,内部是python,整体复制就可以使用了,通过main.py去运行python。快捷方式的参数不写是用python.exe打开main.py,大于1的数字也是python.exe打开main.py,只有等1是pythonw.exe打开main.py。快捷方式...
素织物表面简洁无明显花纹,常见的有染色和印花两种类型。小花纹织物利用变化组织形成细小的图案,而大花纹织物则通过基础组织和变化组织的结合,创造出立体的图案。机织物的生产流程可分免浆织物、白织物和色织物。...
Matsumoto的目标是使Ruby自然而不简单,就像人类身体一样,表面简洁但内部复杂。 自公开发布以来,Ruby在全球范围内吸引了众多忠实的开发者。在2006年,Ruby获得了大众的认可,活跃用户群体遍布全球各大城市,相关...
【描述解析】:“日本钢板表面处理技术基础研究的进步和展望rar,日本钢板表面处理技术基础研究的进步和展望”虽然描述较为简洁,但重复了标题的关键信息,进一步确认了文档内容将聚焦于日本在该领域的研究动态和未来...
在探讨“PPT简洁模板”的知识点时,我们不仅限于表面的设计美学,更深入到其背后的逻辑、原则以及如何在实际应用中最大化简洁模板的优势。本文将从以下几个方面展开: ### 1. **理解简洁设计的基本原则** 简洁设计...
这种方法不仅能有效避免手动计算可能产生的误差,而且可以将复杂的数学计算过程转换为简洁的可视化操作,极大地提高数据处理的效率和准确性。 在液体表面张力系数测定实验中,通过MATLAB编程处理,可以精确地标记...
3D模型设计的过程通常包括以下步骤:概念草图、建模(包括基础形状构建、细节添加和雕刻)、UV展开(为纹理映射准备模型表面)、贴图(应用颜色、纹理和光照信息)、灯光设置、摄像机定位以及最终的渲染。...
坐标系统是用于定位地球表面上任何位置的数学框架。常见的有笛卡尔坐标、极坐标和地理坐标等。然而,由于历史原因和不同应用的需求,全球存在多种不同的坐标系统,如WGS84、CGCS2000、北京54、西安80等。在这些坐标...
- **基本概念**:YOLO (You Only Look Once) 是一种流行的实时物体检测框架,其标签格式简洁且易于处理。 - **格式介绍**:每个图像的标签文件为.txt格式,内容包含物体类别索引和物体边界框的位置信息。 - **具体...
4. **表面结构参数的表示方式发生变化**:参数代号改为大小写斜体形式,如Ra和Rz,且不再使用下角标,这使得参数表示更加简洁明了。 5. **表面结构参数的标准化**:新标准定义了三组新的表面结构参数,并且增加了...
描述部分简洁明了,直接重申了标题中的关键信息,暗示文档将深入探讨水溶性表面活性剂的组合物,以及它们在墨水和纸涂布配制剂中的应用。表面活性剂在化工中起着重要作用,它们能够降低水的表面张力,使得油水混合,...
【知识点解析】: 这篇文档主要是一系列简洁而经典...综上所述,虽然这些祝福话语表面上看似与IT无关,但实际上它们所蕴含的情感智慧和人际交往原则,对于构建良好的IT工作环境和提升个人职业素养具有重要的启示意义。
为了保护线路,防止短路,PCB表面会涂抹阻焊油,常见颜色为绿色。根据设计需求,PCB可分为单面、双面及多层板,层数越多,复杂度和成本越高。 Protel DXP是Protel系列的升级版本,它在原有基础上增加了许多新特性,...
标签“行业分类-设备装置-板纸用表面施”是对主题的简洁概括,突出了这是一项与板纸生产中表面施胶相关的技术和应用。 压缩包内的“板纸用表面施胶剂及其制备方法.pdf”很可能是详细的科研报告或专利文件,包含了...
拉丝效果往往通过图像处理软件(如Photoshop或PowerPoint内置工具)实现,通过调整颜色、光影和纹理参数来模拟金属表面的细腻纹理。 “简洁线条”则强调了模板的设计语言,线条作为设计元素在PPT中起到引导视线、...
论文的描述虽然简洁,但暗示了内容可能涵盖以下几个方面: 1. 去除函数的理论基础:深入探讨去除函数的数学模型和其在光学表面成型中的作用。 2. 实际应用:可能包含具体的案例研究,展示如何运用去除函数来改善...
文章还提到了Python,Python是一种广泛使用的高级编程语言,以其简洁的语法和强大的库支持而受到开发者的青睐。在深度学习和机器学习领域,Python是首选语言之一,大量的库如Tensorflow、Keras、PyTorch等都提供了...
描述中的内容虽然简洁,但暗示了这是一个关于优化聚晶金刚石表面处理的技术,可能涉及的工艺包括化学气相沉积(CVD)、物理气相沉积(PVD)或者其他的表面处理技术。这些方法会根据具体的应用需求和工况条件,选择...
同时,考虑到网页优化,设计师还会考虑代码的简洁性和加载速度,以确保网站在各种设备和网络环境下都能顺畅运行。 总之,OBLOG 表面是一个旨在帮助用户轻松创建美观博客的网页模版,包含了构成网站所需的各种元素。...
从软件开发的角度来看,Visual Basic因其简洁性和强大的编程能力而被选作开发语言,而MatrixVB插件则提供与Matlab语言的接口,使得软件能够在数据处理和图形绘制方面更为强大。这种混合编程的方式,让软件既拥有...