Haskell:程序员的未来利器(2)--我们真的需要变量吗?

  变量,对于所有的程序员来说都是最最基本的东西,我们似乎从未怀疑过它存在的理由。可是为什么要怀疑呢?所有的语言几乎都是有变量的,变量是我们构造程序的最基本元素,把写程序比作建造一栋大厦的话,变量就是一块块的砖,没有砖怎么能造出大厦呢?等一等,没有砖真的就不能建大厦吗?大厦必须用砖吗?当然不是。可是大厦可以不用砖,那程序没有变量怎么可以呢?可以的,我们也有别的“新材料”(其实并不新)来构造程序,今天我们就来看看这“砖”有什么缺点,我们有什么“新材料”来建大厦。

  既然怀疑变量,我们就要先刨刨变量的根,而且要向祖坟上刨。在语言层面上来说,变量就像一个盒子,里面可以放不同的数据,我们可以把数据拿出来,做一些计算,再放进去。可是扒开这一层,到硬件架构上一看,变量其实就是一个名字与内存地址的绑定,也就是说,变量指向一块内存。再往上刨刨,变量其实就是自动机理论里的状态,在不同的时刻,变量或者说内存里存的数据就代表着现在所在的状态。注意,这里有个很重要的词,“时刻”,这是传统的命令式和OO语言的非常重要的一个元素,所有的代码都是在时间维度上展开的。当程序执行到某个时刻时,程序所有数据就代表了当前的状态。这些不是很好吗?是的,变量对我们来说就像一把刀,当我们用的好的时候,可以得到很高的效率,但是一不小心就会伤到别人或是自己,我们必须把所有代码的执行顺序搞清楚,清清楚楚地告诉计算机应该一步步的做什么,一不小心就可能赋错值、冲掉缓冲区、搞错执行顺序等等,当然,这些可能发生的错误逐渐的在新的语言里受到了限制。但是,随着多核时代的来临,并行计算却很难绕过变量带来的困难。

  并行的问题在于同步与互斥,为什么存在这些问题呢?因为我们共享了数据,当多个线程同时访问数据时就可能产生问题,就必须进行同步,怎么做呢?加锁,于是只要到了有访问共享数据的地方都要小心的加锁解锁,一不小心就可能死锁,这也是程序员畏惧并行的地方。更重要的是,即使我们的程序是正确的,当我们需要更多的线程更多的CPU核心的时候,程序的效率也会不升反降,因为我们将会频繁的加解锁,并行的线程也变得和串行差不多。有更好的方法吗?有人提议将数据库中使用的事务加到语言中,其实,Haskell就是“软件事务内存”(STMSoftware Transaction Memory)的第一个试验田,而且由于Haskell的强大类型系统,没有对语言本身做任何修改,就完美的实现了STM。但是,虽然STM将我们从繁重的加锁解锁劳动中解放了出来,提高了开发效率,也提高了程序的正确率,但执行效率并不比加锁强多少,而且也存在着上面的问题。其实,并行计算已经宣告了共享内存模型的失败,而且,我们的计算机科学家们早就解决了这个问题,那就是使用消息传递,这是在实践中得到检验的。但是,真的是共享惹得祸吗?我认为,我们错怪了共享,我们把变量的问题加到了共享上。是的,如果没有变量,如果我们不能修改已有的数据,那么,尽情的共享吧,不用有任何担心,有多少线程同时读一个数据都不会有问题。

  再举个例子,多核CPU的一个很重要的问题就是Cache的更新与同步,当一个core对本地cache写入时(注意,是修改已有的数据),我们不但要保证新数据与内存中的版本同步,还要同时更新其他核心的cache中的拷贝,并且更新过程中是不能允许这些核心访问这些数据的,IntelAMD的工程师们正拼命的想法解决这个问题,但是问题的根源在那呢?在变量。当我们禁止修改数据时,这个问题突然就不再是问题了。

  可是,没有变量我们怎么写程序?你一定会这样问。如果你有这样的疑问,那说明你被Alan TuringJohn von NewmannJohn Backus等人的理论“误导”了、“毒害”了,知道吗,John Backus因为发明了最早的命令式语言Fortran而荣获图灵奖,但却在图灵奖获奖演讲中否定了自己的成果,为函数式语言大唱赞歌,你可以找来他的演讲论文看看。当你从见到“i=i1”的不解甚至嘲笑到费劲心思、强迫自己的大脑来认为它的存在竟然有意义时,你开始逐步走向深渊了。但是我要告诉你,在haskell的世界里,这个式子确实没有意义。我想,你又一次的不解甚至嘲笑了吧。

  那么,我们怎么写程序呢?函数式语言的理论基础是与图灵机能力相同的Lambda演算,所有的函数式语言都是它的超集,都可以通过类似宏替换的形式化代换方式变成Lambda表达式,而且函数式语言的编译器也都是这么做的。这篇文章里我不想讲太多它的理论,如果有必要我会在以后的文章里讲。今天我就用一个数学的例子,让你有一个直观的了解。比如说要计算x=(12+32)*4-3*(23-7),使用我们的传统方法,会有这样的代码:

a=12

a=a+32

a=a*4

b=23

b=b-7

b=b*3

a=a-b

最终将a保存到x中,实际上,这个计算我们当然不会这样写,但是这确实是我们写程序的方式。但是我们在做数学计算时是这样吗?当然不是,我们会这样(假设我们很笨,这样的计算也要一步步来做):

a=12+32

b=a*4

c=23-7

d=3*c

x=b-d

感觉到不一样了吗?每做一次计算,我们就将结果放到一个新的地方,而不是修改已有的数据,所以这里的abcdx永远不会改变,我们可以放心的使用。再细细观察,我们发现第一段代码中顺序是极其重要的,我们不能将a=a+32a=a*4交换过来,否则就得到错误的结果,也就是说,我们必须清楚的告诉计算机该怎样一步步的进行计算。而第二段中,我们可以把它们理解为“定义”,我们分别定义了abcdx的意义,当我们需要得到x的值时,解释器就可以根据这些定义来进行计算,我们甚至不用关心这些定义的顺序,只要他们都有定义,解释器就可以自己决定先计算谁,在这里,没有顺序,没有时间,没有状态,只有数值。当然,Haskell虽然有解释器,但是一般都是编译的,它可以将我们的代码编译成类似于第一段那样的代码,但是,我们可以肯定的是,Haskell编译器的优化空间是远远大于传统语言的编译器的,因为我们只进行了定义,具体计算都是编译器自己说了算的。

  这就是函数式语言的计算方式。

  当然,你会说,即使这样可以计算,那效率一定很差,光内存的申请就是个问题,的确,函数式语言程序要一直申请内存存放新的数据,同时也有很多计算结果没有用处了成了垃圾,这也就是为什么LISP50多年前就有了垃圾回收。当然,这里面还有很多的效率问题,我会在后面的文章中讨论。但可以肯定的是,现在的Haskell以及一些其他的函数式语言的编译器是非常先进的,它们可以生成效率非常高的代码。而我认为函数式语言的效率问题更多的来自于程序员,因为我们有时会用命令式的思维去写函数式程序。比如,有人批评说Haskell中没有高效的哈希表,可是他不明白哈希表完全是一个基于变量和状态修改的结构,haskell永远不会比得上C,甚至Python,除非它放弃自己的优势特性。有人批评Haskell的快排实现很简单却非常慢,其实快排的算法思想也是基于命令式语言特性的。所以学习haskell更重要的是学习函数式语言的思维方式、设计适合于函数式语言的数据结构和算法的方法。

  我想大家现在应该更深刻的理解变量的优缺点了吧,没有了变量,我们不但可以解决问题,而且能解决(更确切的说是绕过)很多棘手的问题,也能够让程序员更自然的思考问题本身,而不是思考如何实现,从而得到更简洁更优雅的程序。当然,有时候也失去了一些效率,就像所有东西都是对象的说法很可笑一样,函数式语言也并不适合所有问题,我们也要学会使用正确的工具解决正确的问题。但是没有了变量,也就不会有循环,那程序的结构怎么控制?一个优秀的语言自然不会是因为它比别人少了什么东西,而是有什么别人没有的东西,所以我会在以后的文章中介绍haskell所具有的优秀特性.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值