16 October 2012

对于熟悉对象语言的程序员来说,函数语言显得有些深奥,特别是满眼看不懂的名词,什么 lambda,high order function, currying,很多不知所谓的名词构成了一道阻碍学习之路的长城。本文的目的就是介绍一下函数语言的特等,其与对象语言的主要区别在哪里,以及对函数语言普遍的误区。

误区 1: 函数语言就是操作各种函数的语言

函数语言这个名词自身就很容易引起这种偏颇的理解,很多人在初学函数语言的时候,看到示例程序里面把函数作为参数传来传去,就会以为函数语言的目的就是把函数作为参数。所有程序的目的都是为了操作数据,虽然函数语言提供了把函数作为参数的特性,但是最终的目的仍然是为了操作数据。把函数作为参数只是为了更方便得操作数据。比如 map 函数,用于把一个列表映射成另一个列表:

List(1, 2, 3) map { _ + 1 }  // get List(2, 3, 4)

虽然 map 函数的参数是一个函数,但是它的目的是为了生成数据的列表,所有的函数都是操作数据的,脱离了数据,函数也就毫无意义

误区 2: 函数语言很难理解

其实正好相反,函数语言提供了更高层的抽象,实际上比其他语言更容易理解。觉得函数语言难于理解是由于受现有语言影响太深。比如下面两段代码:

for (int i = 0; i < 10; i++) print(i);
(0 until 10) foreach print

如果拿给不懂任何编程的人看,他更容易理解哪个呢?对于更加复杂的代码,很多人理解的时候,是使用脑内模拟执行,跟踪调试的方法,这种理解程序的方法是最不可取的!很多关于编程的等级考试以及招聘笔试都会有理解程序的题目,回想一下你是怎么做这些题目的?是否是脑内模拟执行的呢?如果是,你已经被万恶的教育制度毒害了,请立刻忘记此方法!下面教你学会函数语言的思考方式。

函数式思考 1: 考虑整体,不要考虑个体

写一段程序,求 1 + 2 + 3 + … + 100

很多人就在想了,那不就是 1 + 2 = 3,然后再 + 3 = 6,然后再 + 4 = 10 …… 于是

int sum = 0;
for (int i = 0; i <= 100; i++)  sum += i

很对,但是你的思想被局限在一个一个数相加的过程中了,你*一步一步*得把所有数加起来。现在换个思维方式,1 加到 100,无非就是把所有数都加起来么,那么我加起来就对了么:

(1 to 100) reduce { _ + _ }

对于 reduce 函数怎么实现,大家都是高手,这里不需说明,基本所有的函数语言都有这个 reduce 的实现。这里我们把 1 到 100 这 100 个数字作为一个整体去理解,而不是分割得看成 100 个单独的数字。

函数式思考 2: 循序渐进,由简入繁

写一段程序,返回 100 内所有奇数

for (int i = 1; i <= 100; i += 2)  doSth(i)

不做评论,c/java 程序员的标准答案。现在我们重新审题,返回 100 内:

(1 to 100)

所有奇数:

(1 to 100) filter { _ % 2 == 1 }

这就是函数式程序员与传统程序员的区别,传统程序员:我需要 1 3 5 ….,函数式程序员:我需要 100 以内的数,我还需要一个过滤奇数的函数。

应用示例

或许所说的几点过于教条,难于理解,下面举几个简单的例子:

把一个字符序列转换成比特序列 (一字节 = 8比特)

你是不是再想:把第一字节转行成8比特,然后把第二字节转行成8比特,然后……打住!我们需要的是把毎字节转换成8比特。那么怎么把毎字节转换成8比特呢?你是不是在想:拿出第一比特,然后再拿出第二比特……再次打住!上面说了,要从整体考虑,我们要拿出所有比特:

val masks = (0 to 7).reverse map { 1 << _ }

然后:

def charToBits(byte: Byte) = masks map { mask => (byte & mask) != 0 }

最后,我们需要操作的是 Byte序列:

def charSeqToBits(bytes: Seq[Byte]) = bytes flatMap charToBits

怎么样?有没有什么感悟呢?