本文经过完成一个简单的练习题(Exercise),对照在命令式编程(Imperative Programming)语言和函数式编程(Functional Programming)语言中的多种实现,在函数式编程的思惟方式上给予你们一些直观的感觉和体验。python
在学习和讨论函数式编程的时候,我曾经被问到这么一个问题:函数式编程有什么特别优秀的地方?或者说有什么事情是函数式编程能够作到而在传统命令式编程里作不到或者很难作到的?记得这个问题当时回答得很敷衍,以致于如今都想不起来那时是怎么说的。以后又通过了长时间的学习,练习和思考,到目前为止仍是以为这是一个很难回答的问题。在此我愿意记录一下如今的理解,相信这个不会是最终的答案,随着学习的深刻和经验的积累,甚至于未来会有比较大的变化。首先,流行的编程语言,无论是命令式的仍是函数式的都已经被证明具备非凡的表现能力,变幻无穷的软件系统和应用就是证据。仅仅只有几条规则的lambda演算被证实是图灵完整的(https://en.wikipedia.org/wiki...),也就是说理论上仅仅基于lambda演算就能够实现全部的智能和逻辑。有一门研究性质的编程语言叫Brain F*ck,验证了只须要6个算符(或者能够称做编程原语)就能够实现一门图灵完整的编程语言,虽然它的Hello world简直就是天书(https://en.wikipedia.org/wiki...)。所以我相信咱们不可能找到什么东西是在命令式编程语言中不能实现的。区别仅在于一些问题域或是思惟方式与函数式编程的方式和风格特别地契合,从而演化出直接,简明,高效的表述,显得特别有表现力。但若是咱们试图在函数式编程语言里沿用命令式编程的思惟方式,就会遇到不少的困难,出来的结果也将会晦涩难懂,低效易错,难于修改或扩展。程序员
有一种植物,一年有两个生长周期,春天高度加倍,夏天高度增长1米,秋冬季休眠。给出该植物在某个冬季的初始高度 i(米),请计算给出在 n 个生长周期后的高度。编码实现函数 f(i, n)
这是一个来自某个在线解题(Online Judge)网站的题目。题目自己很是的简单直接。使得咱们没必要关注于精巧的数据结构和高深的算法,从而专一于不一样的思惟方式。算法
def f(i, n): l = i for c in range(n): l = l + 1 if (c % 2) else l * 2 return l
这里咱们选择了Python来做为命令式语言的表明。Python的实现直接明了。你们能够打开Python的交互式命令环境,键入并测试这段代码。一些可能的测试结果以下:编程
>>> f (1, 3) 6 >>> f (2, 7) 46 >>> f (2, 2) 5 >>> f (2, 3) 10 >>> f (2, 4) 11
让咱们简单分析一下代码:数组
Haskell是一门基于Lambda演算的“纯粹”的函数式编程语言。Haskell里没有语句(Statement)的概念,程序是由函数定义和表达式组成。函数和数值都是其中的一等公民(First Class Citizen),能够被方便地表述(Represent),存储(Store),传递(Pass/Transfer),使用(Use/Apply)和操纵(Manipulate)。一般的函数都是纯粹的(Pure),无反作用的(Ineffective),简单地说就是传入相同的参数,只能返回相同的结果。
如下是Haskell的一个实现:安全
f :: Int -> Int -> Int f i n = foldl (\acc c -> if (odd c) then acc + 1 else acc * 2) i (take n [0,1..])
你们能够将以上代码存成一个.hs文件(Haskell源代码文件的扩展名),而后加载到ghci中测试。Ghci是Haskell的一个交互式命令环境,关于ghci安装和使用不在本文的范围内,请参见网上的其它资料。也能够在ghci里直接键入以上内容,可是不要忘记先键入一行“:{”,而后在结尾添加一行“:}”, :{和:}是ghci中用于键入多行代码必须的标识。如下是在ghci中的函数定义和测试结果:数据结构
Prelude> :{ Prelude| f :: Int -> Int -> Int Prelude| f i n = foldl (\acc c -> if (odd c) then acc + 1 else acc * 2) i (take n [0,1..]) Prelude| :} Prelude> f 1 3 6 Prelude> f 2 7 46 Prelude> f 2 2 5 Prelude> f 2 3 10 Prelude> f 2 4 11
咱们来一步一步地解释代码:架构
f :: Int -> Int -> Int
是一个函数的类型申明。能够理解为申明了一个名为f的函数,它依次接受两个类型为Int的参数,计算并返回一个Int型的值做为结果。在Haskell的函数申明里,最后的那一个->XXX能够被理解为函数的返回值的类型定义。foldl (+) 0 [1,2,3]
能够将列表中的全部整数累加起来,而foldl (*) 1 [1,2,3]
则计算出列表中全部整数的乘积。take n [0,1..]
事实上构造了一个和Python中range(n)相同的序列。Haskell的惰性求值(Lazy evaluation)容许咱们方便的构造无穷序列并在适当的时候合理使用。(\acc c ->...)
是一个lambda函数,跟Python/Java中的lambda函数相似,能够理解为在申明/定义的地方一次性使用的匿名函数。定义中‘\’标识了lammbda函数定义的开始,以后紧跟函数的参数,多个参数间以空格隔开,以后有一个‘->’,而后就是函数的实现。lambda函数跟一般的函数同样,其实现都是一个表达式,在实现中能够访问全部的参数以及包含该lambda函数的表达式中定义和可访问的名字。在这里,咱们仅访问该lambda函数本身的参数。总结一下,咱们使用foldl函数实现了可控循环,使用条件表达式实现了条件分发,odd函数判断生长周期的属性(奇偶),take函数配合无穷的天然数序列构造了一个有限的序列用于循环控制。这个解法虽然工做,可是显得比较笨拙,特别是那个包含条件表达式的lambda函数。其根本缘由在于咱们一路秉承的命令式编程的思路,而后在函数式语言中寻找相应的结构或函数。那么在函数式编程语言中咱们能够怎样更好地思考并解决这个问题呢?编程语言
记得咱们提到过在函数式编程语言中函数是一等公民吗?咱们能够方便地表述,存储,传递而且使用函数?下面咱们来构造一个函数序列来解决这个问题:函数式编程
f' :: Int -> Int -> Int f' i n = foldl (flip ($)) i (take n $ cycle [(*2), (+1)])
foldl和take函数咱们以前已经了解了。 让咱们来看看别的新出现的东西。
2 + 4
时,事实上和函数调用(+) 2 4
彻底等价。(+1)的意思能够理解为咱们使用(+)这个接受两个参数的函数,而且传入数值1,使其绑定到(+)的第二个参数上,从而生成一个新的函数,该函数接受一个参数,并在该参数上加上数值1做为计算结果。这样的函数咱们称之为部分应用(Partial Applied)函数,或者更加专业一点的中文说法:偏函数。在C/C++/Python/Java这种严格求值(Strict Evaluation)的编程语言里,咱们很难作到调用一个函数,只传递给它部分的参数,使其“停”在这个中间状态,等待其它参数的传入(C++的模版库里有偏函数的实现,Python也有偏函数的库,都费了老劲了,使用又不方便,写出的代码难于理解,限制还多)。而Haskell是非严格求值(Non-strict evaluation)的或者说咱们以前提到过,惰性求值,这使得咱们很容易绑定部分的参数到一个函数,从而造成一个新的,接受更少参数的函数。事实上函数类型上的Int -> Int -> Int
,咱们说过能够理解为一个函数依次接受两个Int型的参数并返回一个Int型的值做为结果,“->”其实也是一个函数(运算符),它是右结合的, 那么Int -> Int -> Int
就等价于Int -> (Int -> Int)
,能够看到这实际上是说该函数接受一个Int类型的参数并返回一个(Int -> Int)类型的函数。回到咱们的主题,这里(*2)和(+1)均是这样的偏函数,当传入一个参数并求值时,分别将参数加倍和加一。cycle [(*2), (+1)]
就是这样一个无穷序列[(*2),(+1),(*2),(+1),(*2),(+1)......]
f $ a
等价于($) f a
也等价于f a
。因为$是右结合的,并且优先级最低,咱们经常用它来减小括号的使用。这里的(take n $ cycle [(*2), (+1)])
就等价于(take n (cycle [(*2), (+1)]))
总结一下:咱们经过cycle [(*2), (+1)]
构造了一个加倍和加一函数交替出现的无穷序列。而后用take函数依据给定的生长周期数目获得一个有限的函数序列,该序列能够看做是一个数据加工的动做序列。以后咱们使用foldl函数迭代该动做序列,依次将植物的高度做为参数喂给序列中的动做并把结果做为下次动做的参数。而将做为累积值的植物高度喂给动做函数的任务则由函数(flip ($))来完成。这个解题思路就比较符合函数式编程的思惟方式和风格了。那么还有没有更加酷一点的方法呢?有的!至少我以为下面的实现(3)就还要酷炫一点。
在实现(2)中咱们表述(Represent)了偏函数,而且将偏函数存储(Store)在列表中,以后将这些偏函数传递(Pass/Transfer)给高阶函数(高阶函数就是能处理函数的函数)foldl和(flip ($)),最后经过高阶函数把所需的参数喂给这些偏函数,应用(Apply)了它们并最终求值(Evaluation)成功。不过彷佛少了同样,说好的操纵(Manipulate)函数呢?别急,这就来:
f'' :: Int -> Int -> Int f'' n = foldl (flip (.)) id $ take n $ cycle [(*2), (+1)]
这里除了咱们的老朋友foldl,take,cycle以及偏函数(*2),(+1)以外,有这些新的内容:
Int -> Int -> Int
,但是它的实现为何只有一个参数f'' n而不是像以前的f和f'那样有两个参数(f i n和f' i n)呢?这是函数的参数约简在起做用。在数学上,对于两个一元函数f和g,若是对全部可能的输入参数a都有f a = g a,那么咱们就说函数f等价于函数g,记为f = g。在Haskell中这条规则彻底成立,无论何时若是有f i = g i,只要f和g中没有包含i,咱们均可以安全地把参数i约去而获得f = g。因此实际上f'' n = foldl ...
是由f'' n i = (foldl ...) i
经过两边约掉相同的i简化得来的。那为何参数i和n的顺序变了呢?下面咱们立刻会说到。Int -> Int
。那么咱们就须要一个一样类型的函数做为foldl的累积初始值。注意到这个初始函数会被组合到最后的组合函数链的尾端(也就是最内层)。若是咱们为这个函数命名h的话,最后的组合函数链看起来应该像这个样子.... (+1).(*2).(+1).(*2).h
。而咱们并不但愿这个函数会对结果有任何影响或改变,很天然地咱们认定这个函数应该直接返回传给它的参数而不作任何更改,该函数的定义应该是h=\x->x
。在Haskell里,预约义的id正是这么一个函数。因而咱们看到id被做为foldl的累积初始值。能够在ghci中键入并验证这个实现,注意参数n和i的顺序不一样于以前的f和f',除非你使用(flip f'')。
Prelude> :{ Prelude| f'' :: Int -> Int -> Int Prelude| f'' n = foldl (flip (.)) id $ take n $ cycle [(*2), (+1)] Prelude| :} Prelude> f'' 3 1 6 Prelude> f'' 7 2 46 Prelude> f'' 2 2 5 Prelude> f'' 3 2 10 Prelude> f'' 4 2 11
总结一下:咱们如方法(2)中同样构建了一个由偏函数(*2)和(+1)交替出现的动做序列。而后使用foldl和组合函数(.)将序列中的前n个元素组合(函数操纵Manipulate的一种情形)成为一个大的函数。最后该函数接受到做为参数的植物的初始高度,求值计算出结果,也就是最后的植物高度。
或者咱们能够这样理解:咱们在方法(3)中构建了一套逻辑/理论架构,在给定生长周期数目的时候,该理论架构发生塌缩,生成一个Int -> Int
的函数,咱们能够理解为规模较小的理论架构。而后当传入植物的初始高度时,这个较小的理论架构再次发生塌缩,从而计算/获得最终的结果,也就是咱们想要获得的植物的最终高度。这个理解完美地契合了函数式建模/编程的一种世界观,那就是编程就是设计和建模一个理论架构,而后当接收到外来刺激的时候,该理论架构就会发生塌缩,从而造成/产生在该刺激条件下的你们所期待的完美结果。是的,完美!