总之,在函数式编程中,函数就是一个管道(pipe)。这头进去一个值,那头就会出来一个新的值,没有其他作用。
一、函数的合成与柯里化
1.1函数的合成
如果一个函数要经过多个函数才可以变成另一个值,这时候就要把函数的中间步骤合并成一个函数,这就叫做函数的合成。
例如:
const compose = function (f, g) {
return function (x) {
return f(g(x));
};
}
1.2.柯里化
把一个多参数的函数,转化为单参数的函数
// 柯里化之前
function add(x, y) {
return x + y;
}
add(1, 2) // 3
// 柯里化之后
function addX(y) {
return function (x) {
return x + y;
};
}
addX(2)(1) // 3
二 ,函子
函数不仅可以用于同一个范畴之中值的转换,还可以用于将一个范畴转成另一个范畴。这就涉及到了函子(Functor)。
2.1函子的概念
函子是函数式编程里面最重要的数据类型,也是基本的运算单位和功能单位。
它的变形关系可以依次作用于每一个值,将当前容器变形成另一个容器。
2.1函子的代码实现
任何具有map方法的数据结构,都可以当做函子的实现
class Functor {
constructor(val) {
this.val = val;
}
map(f) {
return new Functor(f(this.val));
}
}
上面代码中,Functor是一个函子,它的map方法接受函数f作为参数,然后返回一个新的函子,里面包含的值是被f处理过的(f(this.val))。
一般约定,函子的标志就是容器具有map方法。该方法将容器里面的每一个值,映射到另一个容器。
(new Functor(2)).map(function (two) {
return two + 2;
});
// Functor(4)
(new Functor('flamethrowers')).map(function(s) {
return s.toUpperCase();
});
// Functor('FLAMETHROWERS')
(new Functor('bombs')).map(_.concat(' away')).map(_.prop('length'));
// Functor(10)
三 、of 方法
函数式编程一般约定,函子有一个of方法,用来生成新的容器。
下面用 of 替换掉 new
Functor.of = function(val) {
return new Functor(val);
};
这个例子可以改成
Functor.of(2).map(function (two) {
return two + 2;
});
// Functor(4)
四、Maybe 函子
Maybe 函子的map方法里面设置了空值检查。 防止出现空值而报错
Functor.of(null).map(function (s) {
return s.toUpperCase();
});
// TypeError
像这个函数就因为有空值所以就报错了
然而 Maybe 函子 就是这样检查的:
class Maybe extends Functor {
map(f) {
return this.val ? Maybe.of(f(this.val)) : Maybe.of(null);
}
}
有了 Maybe 函子,处理空值就不会出错了。
Maybe.of(null).map(function (s) {
return s.toUpperCase();
});
// Maybe(null)
五、Either 函子
1.Either 函子相当于 if,,,else,,,语句。
Either 函子内部有两个值:左值(Left)和右值(Right)。右值是正常情况下使用的值,左值是右值不存在时使用的默认值。
var addOne = function (x) {
return x + 1;
};
Either.of(5, 6).map(addOne);
// Either(5, 7);
Either.of(1, null).map(addOne);
// Either(2, null);
上面代码中,如果右值有值,就使用右值,否则使用左值。通过这种方式,Either 函子表达了条件运算。
2.Either 函子的另一个用途是代替try...catch,使用左值表示错误。
function parseJSON(json) {
try {
return Either.of(null, JSON.parse(json));
} catch (e: Error) {
return Either.of(e, null);
}
}
上面代码中,左值为空,就表示没有出错,否则左值会包含一个错误对象e。一般来说,所有可能出错的运算,都可以返回一个 Either 函子。
五、ap 函子
function addTwo(x) {
return x + 2;
}
const A = Functor.of(2);
const B = Functor.of(addTwo)
ap 函子就是可以让函子 B 内部的函数,可以使用函子 A 内部的值进行运算。
class Ap extends Functor {
ap(F) {
return Ap.of(this.val(F.val));
}
}
注意,ap方法的参数不是函数,而是另一个函子。
因此,上面的例子可以这样写
Ap.of(addTwo).ap(Functor.of(2))
// Ap(4)
ap 函子的意义在于,对于那些多参数的函数,就可以从多个容器之中取值,实现函子的链式操作。
function add(x) {
return function (y) {
return x + y;
};
}
Ap.of(add).ap(Maybe.of(2)).ap(Maybe.of(3));
//或者这样写
Ap.of(add(2)).ap(Maybe.of(3));
// Ap(5)
六、Monad 函子
函子是一个容器,可以包含任何值。这样就出现了多层嵌套的函子。
Maybe.of(
Maybe.of(
Maybe.of({name: 'Mulburry', number: 8402})
)
)
上面这个函子,一共有三个Maybe嵌套。如果要取出内部的值,就要连续取三次this.val。这当然很不方便,因此就出现了 Monad 函子。
Monad 函子的作用是,总是返回一个单层的函子。它有一个flatMap方法,与map方法作用相同,唯一的区别是如果生成了一个嵌套函子,它会取出后者内部的值,保证返回的永远是一个单层的容器,不会出现嵌套的情况。
class Monad extends Functor {
join() {
return this.val;
}
flatMap(f) {
return this.map(f).join();
}
}
上面代码中,如果函数f返回的是一个函子,那么this.map(f)就会生成一个嵌套的函子。所以,join方法保证了flatMap方法总是返回一个单层的函子。这意味着嵌套的函子会被铺平(flatten)。
七、IO 操作
Monad 函子的重要应用,就是实现 I/O (输入输出)操作。
var fs = require('fs');
var readFile = function(filename) {
return new IO(function() {
return fs.readFileSync(filename, 'utf-8');
});
};
var print = function(x) {
return new IO(function() {
console.log(x);
return x;
});
}
上面代码中,读取文件和打印本身都是不纯的操作,但是readFile和print却是纯函数,因为它们总是返回 IO 函子。
如果 IO 函子是一个Monad,具有flatMap方法,那么我们就可以像下面这样调用这两个函数。
readFile('./user.txt')
.flatMap(print)
由于返回还是 IO 函子,所以可以实现链式操作。因此,在大多数库里面,flatMap方法被改名成chain。
var tail = function(x) {
return new IO(function() {
return x[x.length - 1];
});
}
readFile('./user.txt')
.flatMap(tail)
.flatMap(print)
// 等同于
readFile('./user.txt')
.chain(tail)
.chain(print)