本文就是我在学习函数式编程的过程中本身体悟到的一些东西,这里将用go,JavaScript以及Haskell三种语言来分析函数式编程的一些奥秘。JavaScript因为具备的一些优点可以让咱们能够实现函数式编程,而go做为一种强类型语言,虽然灵活性又稍有欠缺,可是也可以完成一些高阶函数的实现,Haskell语言做为正统的函数式编程语言,为了解释说明问题,做为对比参照。javascript
函数式编程也算是常常看到了,它的一些优点包括:java
虽然上面的优点看看上去好像很厉害的样子,可是,到底厉害在哪里呢?咱们能够经过下面的例子进行说明:编程
求和函数数组
Haskellapp
sum [1,2,3] -- 6 -- sum 的实现实际上是 foldr (+) 0 [1,2,3]
在Haskell中flodr
的函数定义是:编程语言
foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b
函数实现是:函数式编程
-- if the list is empty, the result is the initial value z; else -- apply f to the first element and the result of folding the rest foldr f z [] = z foldr f z (x:xs) = f x (foldr f z xs)
这是一个递归实现,在函数式编程中,递归定义是十分常见的。函数
foldr
函数其实作了这样的事情:foldr
接受三个参数,第一个参数是函数f
,第二个参数是初始值z
,第三个参数是一个列表。若是列表为空则返回初始化值z
,不然递归调用 foldr
,须要说明的是函数f
的类型是接受两个参数,返回一个值,两个参数类型都应该和z
相同(强类型语言中)。学习
在Haskell中咱们可以看到一个列表可以这样被求和,那么在JavaScript中,咱们是如何实现sum
函数的呢?ui
JavaScript
首先咱们实现js版本的foldr
function foldr(f,z,list){ //为了简洁起见,把类型判断省略了 // Object.prototype,toString.call(list) === '[object Array]' if(list === null || list.length == 0){ return z; } //这里的shift会改变参数的状态,会形成反作用 //return f(list.shift(),foldr(f,z,list)); //改用以下写法 return f(list[0],foldr(f,z,list.slice(1))); }
而后咱们再实现js版本的(+)
:
function add(a,b){ return a+b; }
那么咱们的sum
就变成:
function sum(list){ return foldr(add,0,list); }
最后咱们的js版的sum
也能够这样用了:
let a = [1,2,3]; sum(a); // 6
像js这样的弱类型的语言较为灵活,函数f
能够任意实现,对于foldr
函数也可以在多种数据类型之间复用,那么对于像go这样的强类型语言,结果又是怎么样的呢?
go
一样地,咱们实现如下go版本的foldr
:
func foldr(f func(a,b int) int,z int,list []int)int{ if len(list) == 0{ return z } return f(list[0],foldr(f,z,list[1:])) }
go由于有数组切片,因此使用起来较为简单,可是go又是强类型的语言,因此在声明函数的时候必需要把类型声明清楚。
再实现一下f
函数:
func add(a,b int) int{ return a+b; }
依葫芦画瓢咱们能够获得go版本的sum
函数:
func sum(list []int) int{ return foldr(add,0,list) }
能够看出来好像套路都差很少,真正在调用的时候是这样的:
func main(){ a := []int{1,2,3} sum(a) // 6 }
在Haskell中是没有循环的,由于循环能够经过递归实现,在上文咱们实现的sum
函数中,也没有用到任何循环语句,这和咱们原来的编程思惟有所不一样,刚开始我学写求和函数的时候,都是从for
,while
开始的,可是函数式给我打开了新世界的大门。
有了上面的基础,咱们发如今函数式编程中,代码的重用很是便利:
求积函数
javaScript
function muti(a,b){ return a*b; } function product(list){ return foldr(muti,1,list); }
go
func muti(a,b int) int{ return a*b; } func product(list []int) int{ return foldr(muti,1,list) }
Haskell
foldr (*) 1 [1,2,3,4] -- 24 -- or -- product 是Haskell预约义的函数 myproduct xs = foldr (*) 1 xs -- myproduct [1,2,3,4]
还有不少例如 anyTrue
、allTrue
的例子,如下仅给出js实现:
anyTure
JavaScript
function or(a,b){ return a || b; } function anyTrue(list){ return foldr(or,false,list); }
调用:
let b = [true,false,true]; console.log(anyTrue(b)); // true
allTure
JavaScript
function and(a,b){ return a && b; } function allTrue(list){ return foldr(and,true,list); }
调用:
let b = [true,false,true]; console.log(allTrue(b)); // false
固然咱们能够看出来这个flodr
函数贼好用,可是好像仍是有点疑惑,它是怎么工做的呢?看了一圈,flodr
就是一个递归函数,但其实在编程世界,它还有一个更加出名的名字——reduce
。咱们看看在js中是如何使用reduce
实现sum函数的:
求和函数reduce版
const _ = require("lodash"); _.reduce([1,2,3],function(sum,n){ return sum+n; });
在lodash
官方文档是这么定义的:
_.reduce alias _.foldl _.reduceRight alias _.foldr
好吧,我欺骗了大家,其实foldr
应该对应reduceRight
。
那么foldl
和foldr
到底有什么不一样呢?
其实这两个函数的不一样之处在于结合的方式不一样,以求差为例:
Haskell
foldr (-) 0 [1,2,3] -- 输出: 2 foldl (-) 0 [1,2,3] -- 输出: -6
为何两个输出是不一样的呢?这个和结合方向有关:
foldr (-) 0 [1,2,3]
至关于:
1-(2-(3-0)) = 2
而
foldl (-) 0 [1,2,3]
至关于:
((0-1)-2)-3) = -6
结合方向对于求和结果而言是没有区别的,可是对于求差,就有影响了:
JavaScript
const _ = require("lodash"); //reduce 至关于 foldl _.reduce([1,2,3],function(sum,n){ return sum-n; }); // 输出 -4
这个和说好的-6
好像又不同了,坑爹呢么不是?!这是由于,在lodash
的实现中,reduce
的初始值为数组的第一个元素,因此结果是1-2-3 = -4
。
那么咱们看看reduceRight == foldr
的结果:
JavaScript
const _ = require("lodash"); //reduceRight 至关于 foldr _.reduceRight([1,2,3],function(sum,n){ return sum-n; }); // 输出 0
咱们看到这个结果是0
也算是预期结果,由于3-2-1=0
。
注:上文为了易于理解和行文连贯,加入了一些我本身的理解。须要说明的是,在Haskell中,
foldl1
函数应该是和JavaScript的reduce
(lodash)函数是一致的,foldl1
函数将会把列表的第一个元素做为初始值。
如今咱们总结一下foldr
和foldl
的一些思路:
若是对列表[3,4,5,6]
应用函数f
初始值为z
进行foldr
的话,应该理解为:
f 3 (f 4 (f 5 ( f 6 z))) -- 当 f 为 +, z = 0 上式就变为: 3 + (4 + (5 + (6 + 0))) -- 前缀(+)形式则为: (+)3 ((+)4 ((+)5 ((+)6 0)))
若是对列表[3,4,5,6]
应用函数g
初始值为z
进行foldl
的话,应该理解为:
g(g (g (g z 3) 4) 5) 6 -- 固然咱们也能够相似地把 g 设为 +, z = 0, 上式就变为: (((0 + 3) + 4) + 5) + 6 -- 改为前缀形式 (+)((+)((+)((+)0 3) 4) 5) 6
从上面的例子能够看出,左折叠(foldl
)和右折叠(foldr
)二者有一个很关键的区别,就是,左折叠没法处理无限列表,可是右折叠能够。
前面咱们说的都是用预约义的函数+
,-
,*
…,(在函数式编程里,这些运算符其实也都是函数)用这些函数是为了可以让咱们更加便于理解,如今咱们看看用咱们本身定义的函数呢?试试逆转一个列表:
reverse
Haskell
flip' :: (a -> b -> c) -> b -> a -> c flip' f x y= f y x
上面的flip'
函数的做用就是传入第一个参数是一个函数,而后将两个参数的顺序调换一下(flip
是预约义函数)。
Hasekll
foldr flip' [] [1,2,3]
那么JavaScript的实现呢?
JavaScript
function flip(f, a, b){ return f(b,a); } //这个函数须要进行柯里化,不然没法在foldr中做为参数传入 var flip_ = _.curry(flip); function cons(a,b){ return a.concat(b); } function reverse(list){ return foldr(flip_(cons),[],list); }
调用结果又是怎么样的呢?
console.log(reverse([1,2,3])) // [ 3, 2, 1 ]
好了,如今咱们好像又看到了一个新东西——curry
,柯里化。简单地说,柯里化就是一个函数能够先接受一部分参数,而后返回一个接受剩下参数的函数。用上面的例子来讲,flip
函数在被柯里化以后获得的函数flip_
,能够先接受第一个参数cons
而后返回一个接受两个参数a,b
的函数,也就是咱们须要的链接函数。
在go语言里面,实现curry是一个很麻烦的事情,所以go的函数式编程支持仍是比较有限的。
接着咱们试试如何取得一个列表的长度,实现一个length
函数:
length
Haskell
-- 先定义实现一个count 函数 count :: a -> b ->c count a n = n + 1 -- 再实现一个length函数 length' = foldr (count) 0 -- 再调用 length' [1,2,3,4] -- 4
JavaScript
//先定义一个count函数 function count(a,n){ return n + 1; } //再实现length函数 function length(list){ return foldr(count,0,list); } //调用 console.log(length([1,2,3,4])); // 4
就是这么简单,好了,reduce
咱们讲完了,而后咱们看看map
,要知道map
函数是怎么来的,咱们要从一个比较简单的函数先入手,这个函数的功能是把整个列表的全部元素乘以2:
doubleall
haskell
-- 定义一个乘以2,并链接的函数 doubleandcons :: a -> [a] -> [a] doubleandcons x y = 2 * x : y doubleall x = foldr doubleandcons [] -- 调用 doubleall [1,2,3] -- 输出 -- [2,4,6]
JavaScript
function doubleandcons(a,list){ return [a * 2].concat(list) } function doubleall(list){ return foldr(doubleandcons,[],list) } //调用 console.log(doubleall([1,2,3])); // [2,4,6]
再来看看go怎么写:
go
go 的尴尬之处在于,须要很是明确的函数定义,因此咱们要从新写一个foldr
函数,来接受第二个参数为列表的f
。
func foldr2(f func(a int,b []int) []int,z []int,list []int)[]int{ if len(list) == 0{ return z } return f(list[0],foldr2(f,z,list[1:])) }
而后咱们再实现同上面相同的逻辑:
func doubleandcons(n int,list []int) []int{ return append([]int{n * 2},list...) } func doubleall(list []int) []int{ return foldr2(doubleandcons,make([]int,0),list) } // doubleall([]int{1,2,3,4}) //[2 4 6 8]
go这门强类型编译语言虽然支持必定的函数式编程,可是使用起来仍是有必定局限性的,起码代码复用上仍是不如js的。
接下来咱们关注一下其中的doubleandcons
函数,这个函数其实能够转换为这样的一个函数:
fandcons f el [a]= (f el) : [a] double el = el * 2 -- 只传入部分参数,柯里化 doubleandcons = fandcons double
如今咱们关注一下这里的fandcons
,其实这里能够通用表述为Cons·f
,这里的·
称为函数组合。而函数组合有这样的操做:
$$
(f. g) (h) = f (g(h))
$$
那么上面的咱们的函数就能够表述为:
$$
fandcons(f(el)) = (Cons.f)(el)= Cons (f(el))
$$
因此:
$$
fandcons(f(el),list) = (Cons.f) ( el , list) = Cons ((f(el)) ,list)
$$
最终版本就是:
$$
doubleall = foldr((Cons . double),Nil)
$$
这里的foldr(Cons.double)
其实就是咱们要的map double
,那么咱们的map
的原本面目就是:
$$
map = foldr((Cons.f), Nil)
$$
这里的
Nil
是foldr
函数的初始值。
好了map
已经现身了,让咱们再仔细看看一个map
函数应该怎么实现:
map
Haskell
fandcons :: (a->b) ->a -> [b] -> [b] fandcons f x y= (f x):y map' :: (a->b) -> [a] -> [b] map' f x = foldr (fandcons f) [] x -- 调用 map' (\x -> 2 * x) [1,2,3] -- 输出 [2,4,6]
这里用了Haskell的lambda表达式,其实就是f
的double
实现。
咱们也看看js版本的实现:
JavaScript
function fandcons(f, el, list){ return [f(el)].concat(list); } //须要柯里化 var fandcons_ = _.curry(fandcons); function map(f, list){ return foldr(fandcons_(f),[],list); } //调用 console.log(map(function(x){return 2*x},[1,2,3,4])); // 输出[ 2, 4, 6, 8 ]
这些须要柯里化的go我都不实现了,由于go实现柯里化比较复杂。
最后咱们再看看map
的一些神奇的操做:
矩阵求和
summatrix
Haskell
summatrix :: Num a => [[a]] -> a summatrix x = sum (map sum x) -- 调用 summatrix [[1,2,3],[4,5,6]] -- 21
这里必定要显式声明 参数a的类型,由于sum函数要求Num类型的参数
JavaScript
function sum(list){ return foldr(add,0,list); } function summatrix(matrix){ return sum(map(sum,matrix)); } //调用 mat = [[1,2,3],[4,5,6]]; console.log(summatrix(mat)); //输出 21
在学习函数式编程的过程当中,我感觉到了一种新的思惟模式的冲击,仿佛打开了一种全新的世界,没有循环,甚至没有分支,语法简洁优雅。我认为做为一名计算机从业人员都应该去接触一下函数式编程,可以让你的视野更加开阔,可以从另外一个角度去思考。
原文发布于本人我的博客,保留文章全部权利,未经容许不得转载。