引言
本文就是我在学习函数式编程的过程当中自己体悟到的一些东西,这里将用go,JavaScript以及Haskell三种语言来分析函数式编程的一些奥秘。JavaScript由于具有的一些优势能够让我们可以实现函数式编程,而go作为一种强类型语言,虽然灵活性又稍有欠缺,但是也能够完成一些高阶函数的实现,Haskell语言作为正统的函数式编程语言,为了解释说明问题,作为对比参照。
正文
函数式编程也算是经常看到了,它的一些优势包括:
- 不包括赋值语句(assignment statement),一个变量一旦初始化,就无法被修改(immutable)
- 无副作用,函数除了计算结果,将不会产生任何副作用
- 因为无副作用,所以任何表达式在任何时候都能够evaluate
虽然上面的优势看看上去好像很厉害的样子,但是,到底厉害在哪里呢?我们可以通过下面的例子进行说明:
求和函数
Haskell
sum [1,2,3]
-- 6
-- sum 的实现其实是
foldr (+) 0 [1,2,3]
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)
这是一个递归实现,在函数式编程中,递归定义是十分常见的。
foldrfoldrfzzfoldrfz
sum
JavaScript
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)));
}
(+)
function add(a,b){
return a+b;
}
sum
function sum(list){
return foldr(add,0,list);
}
sum
let a = [1,2,3];
sum(a); // 6
ffoldr
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;
}
sum
func sum(list []int) int{
return foldr(add,0,list)
}
可以看出来好像套路都差不多,真正在调用的时候是这样的:
func main(){
a := []int{1,2,3}
sum(a) // 6
}
sumforwhile
有了上面的基础,我们发现在函数式编程中,代码的重用非常便利:
求积函数
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]
anyTrueallTrue
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
flodrflodrreducereduce
求和函数reduce版
const _ = require("lodash");
_.reduce([1,2,3],function(sum,n){
return sum+n;
});
lodash
_.reduce alias _.foldl
_.reduceRight alias _.foldr
foldrreduceRight
foldlfoldr
其实这两个函数的不同之处在于结合的方式不同,以求差为例:
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
-6lodashreduce1-2-3 = -4
reduceRight == foldr
JavaScript
const _ = require("lodash");
//reduceRight 相当于 foldr
_.reduceRight([1,2,3],function(sum,n){
return sum-n;
});
// 输出 0
03-2-1=0
foldl1reducefoldl1
foldrfoldl
[3,4,5,6]fzfoldr
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]gzfoldl
g(g (g (g z 3) 4) 5) 6
-- 当然我们也可以类似地把 g 设为 +, z = 0, 上式就变为:
(((0 + 3) + 4) + 5) + 6
-- 改成前缀形式
(+)((+)((+)((+)0 3) 4) 5) 6
foldlfoldr
+-*
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 ]
curryflipflip_consa,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
reducemapmap
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
foldrf
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
fandconsCons·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 doublemap
$$
map = foldr((Cons.f), Nil)
$$
Nilfoldr
mapmap
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]
fdouble
我们也看看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
结语
在学习函数式编程的过程中,我感受到了一种新的思维模式的冲击,仿佛打开了一种全新的世界,没有循环,甚至没有分支,语法简洁优雅。我认为作为一名计算机从业人员都应该去接触一下函数式编程,能够让你的视野更加开阔,能够从另一个角度去思考。
原文发布于本人个人博客,保留文章所有权利,未经允许不得转载。