前奏
函数式编程
”官方“解释
- 函数式编程是范畴论的数学分支是一门很复杂的数学,认为世界上所有概念体系都可以抽象出一个个范畴
- 彼此之间存在某种关系的概念、事物、对象等等,都构成范畴。任何事物只要找出他们之间的关系,就能定义
- 箭头表示范畴成员之间的关系,正式的名称叫做”态射” (morphism)。范畴论认为,同一个范畴的所有成员,就是不同状态的”变形”(transformation)。通过”态射”,一个成员可以变形成另一个成员。
人话(个人见解)
- 范畴可以粗略的缩小理解为”容器“,而容器是用来包裹”值(也可以理解为一个抽象)“的。在FP中,我们一般不直接操作这个值,而是交给容器去操作,我们只需要告诉容器我们想要怎么变换这个值即可,而这个变换关系就可以理解为态射,容器内部会根据这个态射产生我们想要的变换
- 在FP中,我们摒弃了命令式编程,而是全面拥抱过程编程,我们只需要关注我们想要做什么,而不关注如何去做这个过程,其实就是把具体做事的”命令“隐藏了,举个栗子(下述这个例子有失恰当,因为数组本身的filter、map方法就有FP的影子,我们并没有使用,但此处仅仅演示一下两种编程方式的区别):
const R = require("ramda");
// 将一个user数组中age字段格式化,并根据active字段过滤
const users = [
{ name: "Jack", age: 23, active: true },
{ name: "Bob", age: 21, active: false },
];
// 命令式
const filteredUsers = [];
for (let i = 0; i < users.length; i++) {
if (users[i].active) {
filteredUsers.push({
...users[i],
age: `${users[i].age}岁`,
});
}
}
// 过程式
const filter = R.filter((u) => u.active);
const map = R.map(R.modify("age", (age) => `${age}岁`));
const filterUsers = R.compose(map, filter);
console.log(filterUsers(users));
FP在JavaScript中的应用
纯函数
- 对于相同的输入,永远会得到相同的输出
- 没有任何可观察的副作用,也不依赖外部环境的状态
偏应用函数、函数的柯里化
- 传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数
- 偏函数之所以“偏”,在就在于其只能处理那些能与至少一个case语句匹配的输入,而不能处理所有可能的输入
- 柯里化(Curried) 通过偏应用函数实现。它是把一个多参数函数转换为一个嵌套一元函数的过程
- 传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数
- 事实上柯里化是一种“预加载”函数的方法,通过传递较少的参数, 得到一个已经记住了这些参数的新函数,某种意义上讲,这是一种 对参数的“缓存”,是一种非常高效的编写函数的方法
函数组合
- 在纯函数的前提下,我们就可以随意组合多个”过程“,而不是将多个函数嵌套调用
Point Free
- 把一些对象自带的方法转化成纯函数,不要命名转瞬即逝的中间变量
- 不使用所要处理的值,只合成运算过程。中文可以译作”无值”风格
声明式与命令式代码
惰性求值
- 当输入很大但只有一个小的子集有效时,避免不必要的函数调用 就是所谓的惰性求值。惰性求值方法有很多如组合子(alt-类似于 || 先计算fun1如果返回值是false、null、undefined就不再执行 fun2、memoization、shortcut funsion),但是目的都是一样的,即尽可能的推迟求值,直到依赖的表达式被调用,通俗一点讲就是先组合,等到调用的时候再去执行
正文
Functor
我们已经知道如何书写函数式的程序了,即通过管道把数据在一系列纯函数间传递 的程序。我们也知道了,这些程序就是声明式的行为规范。但是,控制流(control flow)、异常处理(error handling)、异步操作(asynchronous actions)和状态 (state)呢?这个时候我们就需要请我们的Functor出场了,它的工作方式也相对简单,就是个黑盒,将我们的”值“包裹起来,并且负责跟其它过程管线对接
Container
Container就是最简单的Functor,将值包裹,并且实例化我们也不直接使用new关键字,而是使用of方法包裹了new,这样我们就不需要到处写new关键字了,毕竟他是面向过程的东西
var Container = function (x) {
this.__value = x;
};
Container.of = function (x) {
return new Container(x);
};
但是上述这个容器目前还不能跟其它管线对接,所以我们需要容器提供一个方法用来跟外部对接,我们将其称为map
,它接收一个用于变形的函数,返回包裹新值的容器,这个函数其实它也是所谓的态射
Container.prototype.map = function (f) {
return Container.of(f(this.__value));
};
这样就能跟其它管线对接了
Container.of(2).map(function (two) {
return two + 2;
});
//=> Container(4)
Container.of("bombs").map(R.concat(" away")).map(R.prop("length"));
//=> Container(10)
目前来看,Functor好像也没啥具体的作用,甚至有点鸡肋,那是因为我们这个Container只是最基础的,仅用于包裹一下值,有种_ => _
这个函数的感觉,下边的这些Functor则都是有各自应用场景的了
Maybe
- 程序中我们经常会碰到因很烦人的空值而导致程序报错,在js中常常也就是
null
和undefined
Maybe
就是用来处理空值异常的问题,在map
的时候特殊处理
function Maybe(x) {
this.__value = x;
}
Maybe.of = function (x) {
return new Maybe(x);
};
Maybe.prototype.isNothing = function () {
return this.__value === undefined || this.__value === null;
};
// 在进行变换之前对空值做了判断
Maybe.prototype.map = function (f) {
return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value));
};
来个小实战
var safeHead = function (xs) {
// 将值用Maybe包裹了
return Maybe.of(xs[0]);
};
var streetName = R.compose(R.map(R.prop("street")), safeHead, R.prop("addresses"));
streetName({ addresses: [] });
// Maybe(null)
streetName({ addresses: [{ street: "Shady Ln.", number: 4201 }] });
// Maybe("Shady Ln.")
Either
- 上面的
Maybe
也能处理异常,但是它更倾向于处理空值,并且也不能在错误时传递更多的信息,所以我们的Either就应运而生了,更强大的处理错误的Functor - 主要用于错误处理,通过这个Functor将我们的值做包裹后,我们就不需要担心因为我们最后调用时传递的参数不合法而导致整个程序出错了,因为Either会保证流程正常运转
- Either主要包含两个子类:
- 包裹正常值的:Right
- 包裹错误值的:Left
function Left(x) {
this.__value = x;
}
Left.of = function (x) {
return new Left(x);
};
// 当值为错误的情况下进行转化时,我们并没有实际执行态射,而是直接将错误值传递
Left.prototype.map = function (f) {
return this;
};
function Right(x) {
this.__value = x;
}
Right.of = function (x) {
return new Right(x);
};
// 正常值则进行正常的变形
Right.prototype.map = function (f) {
return Right.of(f(this.__value));
};
上述我们并没有定义父类Either
然后去继承,而是直接分开定义了两个子类,下面来看看实际应用场景
Right.of("rain").map(function (str) {
return "b" + str;
});
// Right("brain")
Left.of("rain").map(function (str) {
return "b" + str;
});
// Left("rain")
Right.of({ host: "localhost", port: 80 }).map(_.prop("host"));
// Right('localhost')
Left.of("rolls eyes...").map(_.prop("host"));
// Left('rolls eyes...')
/* 更加具体的例子 */
var moment = require("moment");
// getAge :: Date -> User -> Either(String, Number)
var getAge = R.curry(function (now, user) {
var birthdate = moment(user.birthdate, "YYYY-MM-DD");
if (!birthdate.isValid()) return Left.of("Birth date could not be parsed");
return Right.of(now.diff(birthdate, "years"));
});
getAge(moment(), { birthdate: "2005-12-12" });
// Right(9)
getAge(moment(), { birthdate: "balloons!" });
// Left("Birth date could not be parsed")
// 我们拿到了异常的时候的值(反馈)
IO
- 当我们涉及到操作window、dom或者说读取文件等这些不纯的操作时怎么办呢?直接将window包裹吗?肯定是不行的,我们的
IO
就来了 - 涉及到不纯的操作时(异步我们另说),我们需要一个容器来包裹这个这个操作本身,而不去管涉及了那些不纯的变量、方法等,好比:
输入路径得到文件内容
这是不纯的,因为和io交互了,但是输入路径得到一个返回文件内容的函数
这个是纯的,因为我们并没有执行io相关的操作,只有在调用的时候才真正与io交互,如你所见,我们将不纯的操作交给了调用者(甩锅啦)
var IO = function (f) {
this.__value = f;
};
IO.of = function (x) {
return new IO(function () {
return x;
});
};
IO.prototype.map = function (f) {
// 我们只是将变化组合起来了,并没有真正的执行,所以仍然是纯的
return new IO(R.compose(f, this.__value));
};
IO这个甩锅是不是很经典?来个小demo
////// 纯代码 ///////
// url :: IO String
var url = new IO(function () {
return window.location.href;
});
// toPairs = String -> [[String]]
var toPairs = R.compose(R.map(R.split("=")), R.split("&"));
// params :: String -> [[String]]
var params = R.compose(toPairs, R.last, R.split("?"));
// findParam :: String -> IO Maybe [String]
var findParam = function (key) {
return R.map(
R.compose(Maybe.of, R.filter(R.compose(R.eq(key), head)), params),
url
);
};
////// 非纯调用代码 ///////
// 调用 __value() 来运行它!但实际我们不会直接这么调用,而是封装一下
findParam("searchTerm").__value();
// Maybe(['searchTerm', 'wafflehouse'])
© 版权声明
文章版权归作者所有,未经允许请勿转载,侵权请联系 admin@trc20.tw 删除。
THE END