深入学习javascript函数式编程

栏目: 编程语言 · 发布时间: 5年前

内容简介:大家都知道普通函数是这样的:如果借助Array,可以这样实现:

大家都知道 JavaScript 可以作为 面向对象 或者 函数式 编程语言来使用,一般情况下大家理解的 函数式编程 无非包括 副作用函数组合柯里化 这些概念,其实并不然,如果往深了解学习会发现 函数式编程 还包括非常多的高级特性,比如 functormonad 等。国外课程网站 egghead 上有个教授(名字叫Frisby)基于 JavaScript 讲解的 函数式编程 非常棒,主要介绍了 boxsemigroupmonoidfunctorapplicative functormonadisomorphism 等函数式编程相关的高级主题内容。整个课程大概30节左右,本篇文章主要是对该课程的翻译与总结,有精力的强烈推荐大家观看原课程 Professor Frisby Introduces Composable Functional JavaScript 。课程最后有个小实践项目大家可以练练手,体会下这种不同的编程方式。 这里提前声明下,本个课程里面介绍的 monad高级特性 不见得大家都在项目中能用到,不过可以拓宽下知识面,另外也有助于学习 haskell 这类纯函数式编程

1. 使用容器( Box )创建线性数据流

普通函数是这样的:

function nextCharForNumberString (str) {
  const trimmed = str.trim();
  const number = parseInt(trimmed);
  const nextNumber = number + 1;
  return String.fromCharCode(nextNumber);
}

const result = nextCharForNumberString(' 64');
console.log(result); // "A"
复制代码

如果借助Array,可以这样实现:

const nextCharForNumberString = str =>
    [str]
    .map(s => s.trim())
    .map(s => parseInt(s))
    .map(i => i + 1)
    .map(i => String.fromCharCode(i));

const result = nextCharForNumberString(' 64');
console.log(result); // ["A"]
复制代码

这里我们把数据 str 装进了一个箱子(数组),然后连续多次调用箱子的 map 方法来处理箱子内部的数据。这种实现已经可以感受到一些奇妙之处了。再看一种基本思想相同的实现方式,只不过这次我们不借助数组,而是自己实现箱子:

const Box = x => ({
  map: f => Box(f(x)),
  fold: f => f(x),
  toString: () => `Box(${x})`
});

const nextCharForNumberString = str =>
    Box(str)
    .map(s => s.trim())
    .map(s => parseInt(s))
    .map(i => i + 1)
    .map(i => String.fromCharCode(i));

const result = nextCharForNumberString(' 64');
console.log(String(result)); // "Box(A)"
复制代码

至此我们自己动手实现了一个箱子。连续使用 map 可以组合一组操作,以创建线性的数据流。箱子中不仅可以放数据,还可以放函数,别忘了函数也是一等公民:

const Box = x => ({
  map: f => Box(f(x)),
  fold: f => f(x),
  toString: () => `Box(${x})`
});

const f0 = x => x * 100; // think fo as a data
const add1 = f => x => f(x) + 1; // think add1 as a function
const add2 = f => x => f(x) + 2; // think add2 as a function
const g = Box(f0)
.map(f => add1(f))
.map(f => add2(f))
.fold(f => f);

const res = g(1);
console.log(res); // 103
复制代码

这里当你对一个函数容器调用 map 时,其实是在做函数组合。

2. 使用 Box 重构命令式代码

这里使用的 Box 跟上一节一样:

const Box = x => ({
  map: f => Box(f(x)),
  fold: f => f(x),
  toString: () => `Box(${x})`
});
复制代码

命令式 moneyToFloat

const moneyToFloat = str =>
    parseFloat(str.replace(/\$/g, ''));
复制代码

BoxmoneyToFloat

const moneyToFloat = str =>
    Box(str)
    .map(s => s.replace(/\$/g, ''))
    .fold(r => parseFloat(r));
复制代码

我们这里使用 Box 重构了 moneyToFloatBox 擅长的地方就在于将嵌套表达式转成一个一个的 map ,这里虽然不是很复杂,但却是一种好的实践方式。

命令式 percentToFloat

const percentToFloat = str => {
  const replaced = str.replace(/\%/g, '');
  const number = parseFloat(replaced);
  return number * 0.01;
};
复制代码

BoxpercentToFloat

const percentToFloat = str =>
    Box(str)
    .map(str => str.replace(/\%/g, ''))
    .map(replaced => parseFloat(replaced))
    .fold(number => number * 0.01);
复制代码

我们这里又使用 Box 重构了 percentToFloat ,显然这种实现方式的数据流更加清晰。

命令式 applyDiscount

const applyDiscount = (price, discount) => {
  const cost = moneyToFloat(price);
  const savings = percentToFloat(discount);
  return cost - cost * savings;
};
复制代码

重构 applyDiscount 稍微麻烦点,因为该函数有两条数据流,不过我们可以借助闭包:

BoxapplyDiscount

const applyDiscount = (price, discount) =>
    Box(price)
    .map(price => moneyToFloat(price))
    .fold(cost =>
        Box(discount)
        .map(discount => percentToFloat(discount))
        .fold(savings => cost - cost * savings));
复制代码

现在可以看一下这组代码的输出了:

const result = applyDiscount('$5.00', '20%');

console.log(String(result)); // "4"
复制代码

如果我们在 moneyToFloatpercentToFloat 中不进行拆箱(即 fold ),那么 applyDiscount 就没必要在数据转换之前先装箱(即 Box )了:

const moneyToFloat = str =>
    Box(str)
    .map(s => s.replace(/\$/g, ''))
    .map(r => parseFloat(r)); // here we don't fold the result out

const percentToFloat = str =>
    Box(str)
    .map(str => str.replace(/\%/g, ''))
    .map(replaced => parseFloat(replaced))
    .map(number => number * 0.01); // here we don't fold the result out

const applyDiscount = (price, discount) =>
    moneyToFloat(price)
    .fold(cost =>
        percentToFloat(discount)
        .fold(savings => cost - cost * savings));

const result = applyDiscount('$5.00', '20%');

console.log(String(result)); // "4"
复制代码

3. 使用 Either 进行分支控制

Either 的意思是两者之一,不是 Right 就是 Left 。我们先实现 Right

const Right = x => ({
  map: f => Right(f(x)),
  toString: () => `Right(${x})`
});

const result = Right(3).map(x => x + 1).map(x => x / 2);
console.log(String(result)); // "Right(2)"
复制代码

这里我们暂且不实现 Rightfold ,而是先来实现 Left

const Left = x => ({
  map: f => Left(x),
  toString: () => `Left(${x})`
});

const result = Left(3).map(x => x + 1).map(x => x / 2);
console.log(String(result)); // "Left(3)"
复制代码

Left 容器跟 Right 是不同的,因为 Left 完全忽略了传入的数据转换函数,保持容器内部数据原样。有了 RightLeft ,我们可以对程序数据流进行分支控制。考虑到程序中经常会存在异常,因此容器通常都是未知类型 RightOrLeft

接下来我们实现 RightLeft 容器的 fold 方法,如果未知容器是 Right ,则使用第二个函数参数 g 进行拆箱:

const Right = x => ({
  map: f => Right(f(x)),
  fold: (f, g) => g(x),
  toString: () => `Right(${x})`
});
复制代码

如果未知容器是 Left ,则使用第一个函数参数 f 进行拆箱:

const Left = x => ({
  map: f => Left(x),
  fold: (f, g) => f(x),
  toString: () => `Left(${x})`
});
复制代码

测试一下 RightLeftfold 方法:

const result = Right(2).map(x => x + 1).map(x => x / 2).fold(x => 'error', x => x);
console.log(result); // 1.5
复制代码
const result = Left(2).map(x => x + 1).map(x => x / 2).fold(x => 'error', x => x);
console.log(result); // 2
复制代码

借助 Either 我们可以进行程序流程分支控制,例如进行异常处理、 null 检查等。

下面看一个例子:

const findColor = name =>
    ({red: '#ff4444', blue: '#3b5998', yellow: '#fff68f'})[name];

const result = findColor('red').slice(1).toUpperCase();
console.log(result); // "FF4444"
复制代码

这里如果我们给函数 findColor 传入 green ,则会报错。因此可以借助 Either 进行错误处理:

const findColor = name => {
  const found = {red: '#ff4444', blue: '#3b5998', yellow: '#fff68f'}[name];
  return found ? Right(found) : Left(null);
};

const result = findColor('green')
            .map(c => c.slice(1))
            .fold(e => 'no color',
                 c => c.toUpperCase());
console.log(result); // "no color"
复制代码

更进一步,我们可以提炼出一个专门用于 null 检测的 Either 容器,同时简化 findColor 代码:

const fromNullable = x =>
    x != null ? Right(x) : Left(null); // [!=] will test both null and undefined

const findColor = name =>
    fromNullable({red: '#ff4444', blue: '#3b5998', yellow: '#fff68f'}[name]);
复制代码

4. 利用 chain 解决 Either 的嵌套问题

看一个读取配置文件 config.json 的例子,如果位置文件读取失败则提供一个默认端口 3000 ,命令式代码实现如下:

const fs = require('fs');

const getPort = () => {
  try {
    const str = fs.readFileSync('config.json');
    const config = JSON.parse(str);
    return config.port;
  } catch (e) {
    return 3000;
  }
};

const result = getPort();
console.log(result); // 8888 or 3000
复制代码

我们使用 Either 重构:

const fs = require('fs');

const tryCatch = f => {
  try {
    return Right(f());
  } catch (e) {
    return Left(e);
  }
};

const getPort = () =>
    tryCatch(() => fs.readFileSync('config.json'))
    .map(c => JSON.parse(c))
    .fold(
        e => 3000,
        obj => obj.port
    );

const result = getPort();
console.log(result); // 8888 or 3000
复制代码

重构后就完美了吗?我们用到了 JSON.parse ,如果 config.json 文件格式有问题,程序就会报错:

SyntaxError: Unexpected end of JSON input

因此需要针对 JSON 解析失败做异常处理,我们可以继续使用 tryCatch 来解决这个问题:

const getPort = () =>
    tryCatch(() => fs.readFileSync('config.json'))
    .map(c => tryCatch(() => JSON.parse(c)))
    .fold(
        left => 3000, // 第一个tryCatch失败
        right => right.fold( // 第一个tryCatch成功
            e => 3000, // JSON.parse失败
            c => c.port
        )
    );
复制代码

这次重构我们使用了两次 tryCatch ,因此导致箱子套了两层,最后需要进行两次拆箱。为了解决这种箱子套箱子的问题,我们可以给 RightLeft 增加一个方法 chain

const Right = x => ({
  chain: f => f(x),
  map: f => Right(f(x)),
  fold: (f, g) => g(x),
  toString: () => `Right(${x})`
});

const Left = x => ({
  chain: f => Left(x),
  map: f => Left(x),
  fold: (f, g) => f(x),
  toString: () => `Left(${x})`
});
复制代码

当我们使用 map ,又不想在数据转换之后又增加一层箱子时,我们应该使用 chain

const getPort = () =>
    tryCatch(() => fs.readFileSync('config.json'))
    .chain(c => tryCatch(() => JSON.parse(c)))
    .fold(
        e => 3000,
        c => c.port
    );
复制代码

5. 命令式代码使用 Either 实现举例

const openSite = () => {
  if (current_user) {
      return renderPage(current_user);
    }
    else {
      return showLogin();
    }
};

const openSite = () =>
    fromNullable(current_user)
    .fold(showLogin, renderPage);
复制代码
const streetName = user => {
  const address = user.address;
  if (address) {
    const street = address.street;
    if (street) {
      return street.name;
    }
  }
  return 'no street';
};

const streetName = user =>
    fromNullable(user.address)
    .chain(a => fromNullable(a.street))
    .map(s => s.name)
    .fold(
        e => 'no street',
        n => n
    );
复制代码
const concatUniq = (x, ys) => {
  const found = ys.filter(y => y ===x)[0];
  return found ? ys : ys.concat(x);
};

const cancatUniq = (x, ys) =>
    fromNullable(ys.filter(y => y ===x)[0])
    .fold(null => ys.concat(x), y => ys);
复制代码
const wrapExamples = example => {
  if (example.previewPath) {
    try {
      example.preview = fs.readFileSync(example.previewPath);
    }
    catch (e) {}
  }
  return example;
};

const wrapExamples = example =>
    fromNullable(example.previewPath)
    .chain(path => tryCatch(() => fs.readFileSync(path)))
    .fold(
        () => example,
        preivew => Object.assign({preview}, example)
    );
复制代码

6. 半群

半群是一种具有 concat 方法的类型,并且该 concat 方法满足结合律。比如 ArrayString

const res = "a".concat("b").concat("c");
const res = [1, 2].concat([3, 4].concat([5, 6])); // law of association
复制代码

我们自定义 Sum 半群, Sum 类型用来求和:

const Sum = x => ({
  x,
  concat: o => Sum(x + o.x),
  toString: () => `Sum(${x})`
});

const res = Sum(1).concat(Sum(2));
console.log(String(res)); // "Sum(3)"
复制代码

继续自定义 All 半群, All 类型用来级联布尔类型:

const All = x => ({
  x,
  concat: o => All(x && o.x),
  toString: () => `All(${x})`
});

const res = All(true).concat(All(false));
console.log(String(res)); // "All(false)"
复制代码

继续定义 First 半群, First 类型链式调用 concat 方法不改变其初始值:

const First = x => ({
  x,
  concat: o => First(x),
  toString: () => `First(${x})`
});

const res = First('blah').concat(First('ice cream'));
console.log(String(res)); // "First(blah)"
复制代码

7. 半群举例

这里先占位,回头再补充。

const acct1 = Map({
  name: First('Nico'),
  isPaid: All(true),
  points: Sum(10),
  friends: ['Franklin']
});

const acct2 = Map({
  name: First('Nico'),
  isPaid: All(false),
  points: Sum(2),
  friends: ['Gatsby']
});

const res = acct1.concat(acct2);
console.log(res);
复制代码

8. monoid

半群满足结合律,如果半群还具有幺元(单位元),那么就是monoid。幺元与其他元素结合时不会改变那些元素,可以用公式表示如下:

e・a = a・e = a

我们将半群 Sum 升级实现为monoid只需实现一个 empty 方法,调用改方法即可得到该monoid的幺元:

const Sum = x => ({
  x,
  concat: o => Sum(x + o.x),
  toString: () => `Sum(${x})`
});

Sum.empty = () => Sum(0);

const res = Sum.empty().concat(Sum(1).concat(Sum(2)));
// const res = Sum(1).concat(Sum(2)).concat(Sum.empty());
console.log(String(res)); // "Sum(3)"
复制代码

接着我们继续将 All 升级实现为monoid:

const All = x => ({
  x,
  concat: o => All(x && o.x),
  toString: () => `All(${x})`
});

All.empty = () => All(true);

const res = All(true).concat(All(true)).concat(All.empty());
console.log(String(res)); // "All(true)"
复制代码

如果我们尝试着将半群 First 也升级为monoid就会发现不可行,比如 First('hello').concat(…) 的结果恒为 hello ,但是 First.empty().concat(First('hello')) 的结果就不一定是 hello 了,因此我们无法将半群 First 升级为monoid。这也说明monoid一定是半群,但是半群不一定是monoid。半群需要满足结合律,monoid不仅需要满足结合律,还必须存在幺元。

9. monoid举例

Sum(求和):

const Sum = x => ({
  x,
  concat: o => Sum(x + o.x),
  toString: () => `Sum(${x})`
});

Sum.empty = () => Sum(0);
复制代码

Product(求积):

const Product = x => ({
  x,
  concat: o => Product(x * o.x),
  toString: () => `Product(${x})`
});

Product.empty = () => Product(1);

const res = Product.empty().concat(Product(2)).concat(Product(3));
console.log(String(res)); // "Product(6)"
复制代码

Any(只要有一个为 true 即返回 true ,否则返回 false ):

const Any = x => ({
  x,
  concat: o => Any(x || o.x),
  toString: () => `Any(${x})`
});

Any.empty = () => Any(false);

const res = Any.empty().concat(Any(false)).concat(Any(false));
console.log(String(res)); // "Any(false)"
复制代码

All(所有均为 true 才返回 true ,否则返回 false ):

const All = x => ({
  x,
  concat: o => All(x && o.x),
  toString: () => `All(${x})`
});

All.empty = () => All(true);

const res = All(true).concat(All(true)).concat(All.empty());
console.log(String(res)); // "All(true)"
复制代码

Max(求最大值):

const Max = x => ({
  x,
  concat: o => Max(x > o.x ? x : o.x),
  toString: () => `Max(${x})`
});

Max.empty = () => Max(-Infinity);

const res = Max.empty().concat(Max(100)).concat(Max(200));
console.log(String(res)); // "Max(200)"
复制代码

Min(求最小值):

const Min = x => ({
  x,
  concat: o => Min(x < o.x ? x : o.x),
  toString: () => `Min(${x})`
});

Min.empty = () => Min(Infinity);

const res = Min.empty().concat(Min(100)).concat(Min(200));
console.log(String(res)); // "Min(100)"
复制代码

10. 使用 foldMap 对集合汇总

假设我们需要对一个 Sum 集合进行汇总,可以这样实现:

const res = [Sum(1), Sum(2), Sum(3)]
	.reduce((acc, x) => acc.concat(x), Sum.empty());

console.log(res); // Sum(6)
复制代码

考虑到这个操作的一般性,可以抽成一个函数 fold 。用 node 安装 immutableimmutable-extimmutable-ext 提供了 fold 方法:

const {Map, List} = require('immutable-ext');
const {Sum} = require('./monoid');

const res = List.of(Sum(1), Sum(2), Sum(3))
	.fold(Sum.empty());

console.log(res); // Sum(6)
复制代码

也许你会觉得 fold 接受的参数应该是一个函数,因为前面几节介绍的 fold 就是这样的,比如 BoxRight

Box(3).fold(x => x); // 3
Right(3).fold(e => e, x => x); // 3
复制代码

没错,不过 fold 的本质就是拆箱。前面对 BoxRight 类型拆箱是将其值取出来;而现在对集合拆箱则是为了将集合的汇总结果取出来。而将一个集合中的多个值汇总成一个值就需要传入初始值 Sum.empty() 。因此当你看到 fold 时,应该看成是为了从一个类型中取值出来,而这个类型可能是一个仅含一个值的类型(比如 BoxRight ),也可能是一个monoid集合。

我们继续看另外一种集合 Map

const res = Map({brian: Sum(3), sara: Sum(5)})
	.fold(Sum.empty());

console.log(res); // Sum(8)
复制代码

这里的 Map 是monoid集合,如果是普通数据集合可以先使用集合的 map 方法将该集合转换成monoid集合:

const res = Map({brian: 3, sara: 5})
	.map(Sum)
	.fold(Sum.empty());

console.log(res); // Sum(8)
复制代码
const res = List.of(1, 2, 3)
	.map(Sum)
	.fold(Sum.empty());

console.log(res); // Sum(6)
复制代码

我们可以把这种对普通数据类型集合调用 map 转换成monoid类型集合,然后再调用 fold 进行数据汇总的操作抽出来,即为 foldMap

const res = List.of(1, 2, 3)
	.foldMap(Sum, Sum.empty());

console.log(res); // Sum(6)
复制代码

11. 使用 LazyBox 延迟求值

首先回顾一下前面 Box 的例子:

const Box = x => ({
  map: f => Box(f(x)),
  fold: f => f(x),
  toString: () => `Box(${x})`
});

const res = Box(' 64')
            .map(s => s.trim())
            .map(s => parseInt(s))
            .map(i => i + 1)
            .map(i => String.fromCharCode(i))
            .fold(x => x.toLowerCase());

console.log(String(res)); // a
复制代码

这里进行了一系列的数据转换,最后转换成了 a 。现在我们可以定义一个 LazyBox ,延迟执行这一系列数据转换函数,直到最后扣动扳机:

const LazyBox = g => ({
  map: f => LazyBox(() => f(g())),
  fold: f => f(g())
});

const res = LazyBox(() => ' 64')
			.map(s => s.trim())
            .map(s => parseInt(s))
            .map(i => i + 1)
            .map(i => String.fromCharCode(i))
            .fold(x => x.toLowerCase());

console.log(res); // a
复制代码

LazyBox 的参数是一个参数为空的函数。在 LazyBox 上调用 map 并不会立即执行传入的数据转换函数,每调用一次 map 待执行函数队列中就会多一个函数,直到最后调用 fold 扣动扳机,前面所有的数据转换函数一触一发,一个接一个的执行。这种模式有助于实现纯函数。

12. 在 Task 中捕获副作用

本节依然是讨论Lazy特性,只不过基于 data.task 库,该库可以通过npm安装。假设我们要实现一个发射火箭的函数,如果我们这样实现,那么该函数显然不是纯函数:

const launchMissiles = () =>
	console.log('launch missiles!'); // 使用console.log模仿发射火箭
复制代码

如果使用 data.task 可以借助其Lazy特性,延迟执行:

const Task = require('data.task');

const launchMissiles = () =>
	new Task((rej, res) => {
      console.log('launch missiles!');
      res('missile');
	});
复制代码

显然这样实现 launchMissiles 即为纯函数。我们可以继续在其基础上组合其他逻辑:

const app = launchMissiles().map(x => x + '!');

app
.map(x => x + '!')
.fork(
	e => console.log('err', e),
  	x => console.log('success', x)
);

// launch missiles!
// success missile!!
复制代码

调用 fork 方法才会扣动扳机,执行前面定义的 Task 以及一系列数据转换函数,如果不调用 forkTask 中的 console.log 操作就不会执行。

13. 使用 Task 处理异步任务

假设我们要实现读文件,替换文件内容,然后写文件的操作,命令式代码如下:

const fs = require('fs');

const app = () =>
	fs.readFile('config.json', 'utf-8', (err, contents) => {
      if (err) throw err;
      const  newContents = contents.replace(/8/g, '6');
      fs.writeFile('config1.json', newContents,
      	(err, success) => {
        if (err) throw err;
        console.log('success');
      })
	});

app();
复制代码

这里实现的 app 内部会抛出异常,不是纯函数。我们可以借助 Task 重构如下:

const Task = require('data.task');
const fs = require('fs');

const readFile = (filename, enc) =>
	new Task((rej, res) =>
    	fs.readFile(filename, enc, (err, contents) =>
        	err ? rej(err) : res(contents)));

const writeFile = (filename, contents) =>
	new Task((rej, res) =>
    	fs.writeFile(filename, contents, (err, success) =>
        	err ? rej(err) : res(success)));

const app = () =>
	readFile('config.json', 'utf-8')
	.map(contents => contents.replace(/8/g, '6'))
	.chain(contents => writeFile('config1.json', contents));

app().fork(
	e => console.log(e),
  	x => console.log('success')
);
复制代码

这里实现的 app 是纯函数,调用 app().fork 才会执行一系列动作。再看看 data.task 官网的顺序读两个文件的例子:

const fs = require('fs');
const Task = require('data.task');

const readFile = path =>
    new Task((rej, res) =>
        fs.readFile(path, 'utf-8', (error, contents) =>
            error ? rej(error) : res(contents)));

const concatenated = readFile('Task_test_file1.txt')
                    .chain(a =>
                        readFile('Task_test_file2.txt')
                        .map(b => a + b));

concatenated.fork(console.error, console.log);
复制代码

14. Functor

Functor是具有 map 方法的类型,并且需要满足下面两个条件:

fx.map(f).map(g) == fx.map(x => g(f(x)))
fx.map(id) == id(fx), where const id = x => x

Box 类型为例说明:

const Box = x => ({
  map: f => Box(f(x)),
  fold: f => f(x),
  inspect: () => `Box(${x})`
});

const res1 = Box('squirrels')
			.map(s => s.substr(5))
			.map(s => s.toUpperCase());
const res2 = Box('squirrels')
			.map(s => s.substr(5).toUpperCase());
console.log(res1, res2); // Box(RELS) Box(RELS)
复制代码

显然 Box 满足第一个条件。注意这里的 s = > s.substr(5).toUpperCase() 其实本质上跟 g(f(x)) 是一样的,我们完全重新定义成下面这种形式,不要被形式迷惑:

const f = s => s.substr(5);
const g = s => s.toUpperCase();
const h = s => g(f(s));

const res = Box('squirrels')
			.map(h);
console.log(res); // Box(RELS)
复制代码

接下来我们看是否满足第二个条件:

const id = x => x;
const res1 = Box('crayons').map(id);
const res2 = id(Box('crayons'));
console.log(res1, res2); // Box(crayons) Box(crayons)
复制代码

显然也满足第二个条件。

15. 使用 of 方法将值放入Pointed Functor

pointed functor是具有 of 方法的functor, of 可以理解成使用一个初始值来填充functor。以 Box 为例说明:

const Box = x => ({
  map: f => Box(f(x)),
  fold: f => f(x),
  inspect: () => `Box(${x})`
});
Box.of = x => Box(x);

const res = Box.of(100);
console.log(res); // Box(100)
复制代码

这里再举个functor的例子,IO functor:

const R = require('ramda');

const IO = x => ({
  x, // here x is a function
  map: f => IO(R.compose(f, x)),
  fold: f => f(x) // get out x
});

IO.of = x => IO(x);
复制代码

IO是一个值为函数的容器,细心的话你会发现这就是前面的值为函数的 Box 容器。借助IO functor,我们可以纯函数式的处理一些IO操作了,因为读写操作就好像全部放入了队列一样,直到最后调用IO内部的函数时才会扣动扳机执行一系列操作,试一下:

const R = require('ramda');
const {IO} = require('./IO');

const fake_window = {
    innerWidth: '1000px',
    location: {
        href: "http://www.baidu.com/cpd/fe"
    }
};

const io_window = IO(() => fake_window);

const getWindowInnerWidth = io_window
.map(window => window.innerWidth)
.fold(x => x);

const split = x => s => s.split(x);

const getUrl = io_window
.map(R.prop('location'))
.map(R.prop('href'))
.map(split('/'))
.fold(x => x);

console.log(getWindowInnerWidth()); // 1000px
console.log(getUrl()); // [ 'http:', '', 'www.baidu.com', 'cpd', 'fe' ]
复制代码

16. Monad

functor可以将一个函数作用到一个包着的(这里“包着”意思是值存在于箱子内,下同)值上面:

Box(1).map(x => x + 1); // Box(2)
复制代码

applicative functor可以将一个包着的函数作用到一个包着的值上面:

const add = x => x + 1;
Box(add).ap(Box(1)); // Box(2)
复制代码

而monod可以将一个返回箱子类型的函数作用到一个包着的值上面,重点是作用之后包装层数不增加:

先看个 Box functor的例子:

const Box = x => ({
  map: f => Box(f(x)),
  fold: f => f(x),
  inspect: () => `Box(${x})`
});

const res = Box(1)
			.map(x => Box(x))
			.map(x => Box(x)); // Box(Box(Box(1)))
console.log(res); // Box([object Object])
复制代码

这里我们连续调用 map 并且 map 时传入的函数的返回值是箱子类型,显然这样会导致箱子的包装层数不断累加,我们可以给 Box 增加 join 方法来拆包装:

const Box = x => ({
  map: f => Box(f(x)),
  join: () => x,
  fold: f => f(x),
  inspect: () => `Box(${x})`
});

const res = Box(1)
			.map(x => Box(x))
			.join()
			.map(x => Box(x))
			.join();
console.log(res); // Box(1)
复制代码

这里定义 join 仅仅是为了说明拆包装这个操作,我们当然可以使用 fold 完成相同的功能:

const Box = x => ({
  map: f => Box(f(x)),
  join: () => x,
  fold: f => f(x),
  inspect: () => `Box(${x})`
});

const res = Box(1)
			.map(x => Box(x))
			.fold(x => x)
			.map(x => Box(x))
			.fold(x => x);
console.log(res); // Box(1)
复制代码

考虑到 .map(...).join() 的一般性,我们可以为 Box 增加一个方法 chain 完成这两步操作:

const Box = x => ({
  map: f => Box(f(x)),
  join: () => x,
  chain: f => Box(x).map(f).join(),
  fold: f => f(x),
  inspect: () => `Box(${x})`
});

const res = Box(1)
			.chain(x => Box(x))
			.chain(x => Box(x));
console.log(res); // Box(1)
复制代码

17. 柯里化

这个非常简单,直接举例,能看懂这些例子就明白柯里化了:

const modulo = dvr => dvd => dvd % dvr;

const isOdd = modulo(2); // 求奇数

const filter = pred => xs => xs.filter(pred);

const getAllOdds = filter(isOdd);

const res1 = getAllOdds([1, 2, 3, 4]);
console.log(res1); // [1, 3]

const map = f => xs => xs.map(f);

const add = x => y => x + y;

const add1 = add(1);
const allAdd1 = map(add1);

const res2 = allAdd1([1, 2, 3]);
console.log(res2); // [2, 3, 4]
复制代码

18. Applicative Functor

前面介绍的 Box 是一个functor,我们为其添加 ap 方法,将其升级成applicative functor:

const Box = x => ({
  ap: b2 => b2.map(x), // here x is a function
  map: f => Box(f(x)),
  fold: f => f(x),
  inspect: () => `Box(${x})`
});

const res = Box(x => x + 1).ap(Box(2));
console.log(res); // Box(3)
复制代码

这里 Box 内部是一个一元函数,我们也可以使用柯里化后的多元函数:

const add = x => y => x + y;

const res = Box(add).ap(Box(2));
console.log(res); // Box([Function])
复制代码

显然我们applicative functor上调用一次 ap 即可消掉一个参数,这里 res 内部存的是仍然是一个函数: y => 2 + y ,只不过消掉了参数 x 。我们可以连续调用 ap 方法:

const res = Box(add).ap(Box(2)).ap(Box(3));
console.log(res); // Box(5)
复制代码

稍加思考我们会发现对于applicative functor,存在下面这个恒等式:

F(x).map(f) = F(f).ap(F(x))

即在一个保存值 x 的functor上调用 map(f) ,恒等于在保存函数 f 的functor上调用 ap(F(x))

接着我们实现一个处理applicative functor的 工具 函数 liftA2

const liftA2 = (f, fx, fy) =>
	F(f).ap(fx).ap(fy);
复制代码

但是这里需要知道具体的functor类型 F ,因此借助于前面的恒等式,我们继续定义下面的一般形式 liftA2

const liftA2 = (f, fx, fy) =>
	fx.map(f).ap(fy);
复制代码

试一下:

const res1 = Box(add).ap(Box(2)).ap(Box(4));
const res2 = liftA2(add, Box(2), Box(4)); // utilize helper function liftA2

console.log(res1); // Box(6)
console.log(res2); // Box(6)
复制代码

当然我们也可以定义类似的 liftA3liftA4 等工具函数:

const liftA3 = (f, fx, fy, fz) =>
	fx.map(f).ap(fy).ap(fz);
复制代码

19. Applicative Functor举例

首先来定义 either

const Right = x => ({
  ap: e2 => e2.map(x), // declare as a applicative, here x is a function
  chain: f => f(x), // declare as a monad
  map: f => Right(f(x)),
  fold: (f, g) => g(x),
  inspect: () => `Right(${x})`
});

const Left = x => ({
  ap: e2 => e2.map(x), // declare as a applicative, here x is a function
  chain: f => Left(x), // declare as a monad
  map: f => Left(x),
  fold: (f, g) => f(x),
  inspect: () => `Left(${x})`
});

const fromNullable = x =>
    x != null ? Right(x) : Left(null); // [!=] will test both null and undefined

const either = {
  	Right,
  	Left,
  	of: x => Right(x),
  	fromNullable
};
复制代码

可以看出 either 既是monad又是applicative functor。

假设我们要计算页面上除了 headerfooter 之外的高度:

const $ = selector =>
	either.of({selector, height: 10}); // fake DOM selector

const getScreenSize = (screen, header, footer) =>
	screen - (header.height + footer.height);
复制代码

如果使用 monodchain 方法,可以这样实现:

const res = $('header')
	.chain(header =>
    	$('footer').map(footer =>
        	getScreenSize(800, header, footer)));
console.log(res); // Right(780)
复制代码

也可以使用 applicative 实现,不过首先需要柯里化 getScreenSize

const getScreenSize = screen => header => footer =>
	screen - (header.height + footer.height);

const res1 = either.of(getScreenSize(800))
	.ap($('header'))
	.ap($('footer'));
const res2 = $('header')
	.map(getScreenSize(800))
	.ap($('footer'));
const res3 = liftA2(getScreenSize(800), $('header'), $('footer'));
console.log(res1, res2, res3); // Right(780) Right(780) Right(780)
复制代码

20. Applicative Functor之List

本节介绍使用applicative functor实现下面这种模式:

for (x in xs) {
  for (y in ys) {
    for (z in zs) {
      // your code here
    }
  }
}
复制代码

使用applicative functor重构如下:

const {List} = require('immutable-ext');

const merch = () =>
	List.of(x => y => z => `${x}-${y}-${z}`)
	.ap(List(['teeshirt', 'sweater']))
	.ap(List(['large', 'medium', 'small']))
	.ap(List(['black', 'white']));
const res = merch();
console.log(res);
复制代码

21. 使用applicatives处理并发异步事件

假设我们要发起两次读数据库的请求:

const Task = require('data.task');

const Db = ({
  find: id =>
  	new Task((rej, res) =>
    	setTimeOut(() => {
      		console.log(res);
        	res({id: id, title: `Project ${id}`}) 
    	}, 5000))
});

const report = (p1, p2) =>
	`Report: ${p1.title} compared to ${p2.title}`;
复制代码

如果使用 monadchain 实现,那么两个异步事件只能顺序执行:

Db.find(20).chain(p1 =>
	Db.find(8).map(p2 =>
    	report(p1, p2)))
	.fork(console.error, console.log);
复制代码

使用applicatives重构:

Task.of(p1 => p2 => report(p1, p2))
.ap(Db.find(20))
.ap(Db.find(8))
.fork(console.error, console.log);
复制代码

22. [Task] => Task([])

假设我们准备读取一组文件:

const fs = require('fs');
const Task = require('data.task');
const futurize = require('futurize').futurize(Task);
const {List} = require('immutable-ext');

const readFile = futurize(fs.readFile);

const files = ['box.js', 'config.json'];
const res = files.map(fn => readFile(fn, 'utf-8'));
console.log(res);
// [ Task { fork: [Function], cleanup: [Function] },
//   Task { fork: [Function], cleanup: [Function] } ]
复制代码

这里 res 是一个 Task 数组,而我们想要的是 Task([]) 这种类型,类似 promise.all() 的功能。我们可以借助 traverse 方法使 Task 类型从数组里跳到外面:

[Task] => Task([])

实现如下:

const files = List(['box.js', 'config.json']);
files.traverse(Task.of, fn => readFile(fn, 'utf-8'))
  .fork(console.error, console.log);
复制代码

23. {Task} => Task({})

假设我们准备发起一组http请求:

const fs = require('fs');
const Task = require('data.task');
const {List, Map} = require('immutable-ext');

const httpGet = (path, params) =>
	Task.of(`${path}: result`);

const res = Map({home: '/', about: '/about', blog: '/blod'})
.map(route => httpGet(route, {}));
console.log(res);
// Map { "home": Task, "about": Task, "blog": Task }
复制代码

这里 res 是一个值为 TaskMap ,而我们想要的是 Task({}) 这种类型,类似 promise.all() 的功能。我们可以借助 traverse 方法使 Task 类型从 Map 里跳到外面:

{Task} => Task({})

实现如下:

Map({home: '/', about: '/about', blog: '/blod'})
.traverse(Task.of, route => httpGet(route, {}))
.fork(console.error, console.log);
// Map { "home": "/: result", "about": "/about: result", "blog": "/blod: result" }
复制代码

24. 类型转换

本节介绍一种functor如何转换成另外一种functor。例如将 either 转换成 Task

const {Right, Left, fromNullable} = require('./either');
const Task = require('data.task');

const eitherToTask = e =>
	e.fold(Task.rejected, Task.of);

eitherToTask(Right('nightingale'))
.fork(
	e => console.error('err', e),
  	r => console.log('res', r)
); // res nightingale

eitherToTask(Left('nightingale'))
.fork(
	e => console.error('err', e),
  	r => console.log('res', r)
); // err nightingale
复制代码

Box 转换成 Either

const {Right, Left, fromNullable} = require('./either');
const Box = require('./box');

const boxToEither = b =>
	b.fold(Right);

const res = boxToEither(Box(100));
console.log(res); // Right(100)
复制代码

你可能会疑惑为什么 boxToEither 要转换成 Right ,而不是 Left ,原因就是本节讨论的类型转换需要满足该条件:

nt(fx).map(f) == nt(fx.map(f))

其中 nt 是natural transform的缩写,即自然类型转换,所有满足该公式的函数均为自然类型转换。接着讨论 boxToEither ,如果前面转换成 Left ,我们看下是否还能满足该公式:

const boxToEither = b =>
	b.fold(Left);

const res1 = boxToEither(Box(100)).map(x => x * 2);
const res2 = boxToEither(Box(100).map(x => x * 2));
console.log(res1, res2); // Left(100) Left(200)
复制代码

显然不满足上面的条件。

再看一个自然类型转换函数 first

const first = xs =>
	fromNullable(xs[0]);

const res1 = first([1, 2, 3]).map(x => x + 1);
const res2 = first([1, 2, 3].map(x => x + 1));
console.log(res1, res2); // Right(2) Right(2)
复制代码

前面的公式表明,对于一个 functor ,先进行自然类型转换再 map 等价于先 map 再进行自然类型转换。

25. 类型转换举例

先看下 first 的一个用例:

const {fromNullable} = require('./either');

const first = xs =>
	fromNullable(xs[0]);

const largeNumbers = xs =>
	xs.filter(x => x > 100);

const res = first(largeNumbers([2, 400, 5, 1000]).map(x => x * 2));

console.log(res); // Right(800)
复制代码

这种实现没什么问题,不过这里将large numbers的每个值都进行了乘2的 map ,而我么最后的结果仅仅需要第一个值,因此借用自然类型转换公式我们可以改成下面这种形式:

const res = first(largeNumbers([2, 400, 5, 1000])).map(x => x * 2);

console.log(res); // Right(800)
复制代码

再看一个稍微复杂点的例子:

const {Right, Left} = require('./either');
const Task = require('data.task');

const fake = id => ({
  id,
  name: 'user1',
  best_friend_id: id + 1
}); // fake user infomation

const Db = ({
  find: id =>
  	new Task((rej, res) =>
    	res(id > 2 ? Right(fake(id)) : Left('not found')))
}); // fake database

const eitherToTask = e =>
	e.fold(Task.rejected, Task.of);
复制代码

这里我们模拟了一个数据库以及一些用户信息,并假设数据库中只能够查到 id 大于2的用户。

现在我们要查找某个用户的好朋友的信息:

Db.find(3) // Task(Right(user))
.map(either =>
    either.map(user => Db.find(user.best_friend_id))) // Task(Either(Task(Either)))
复制代码

如果这里使用 chain ,看一下效果如何:

Db.find(3) // Task(Right(user))
.chain(either =>
	either.map(user => Db.find(user.best_friend_id))) // Either(Task(Either))
复制代码

这样调用完之后也有有问题:容器的类型从 Task 变成了 Either ,这也不是我们想看到的。下面我们借助自然类型转换重构一下:

Db.find(3) // Task(Right(user))
.map(eitherToTask) // Task(Task(user))
复制代码

为了去掉一层包装,我们改用 chain

Db.find(3) // Task(Right(user))
.chain(eitherToTask) // Task(user)
.chain(user =>
	Db.find(user.best_friend_id)) // Task(Right(user))
.chain(eitherToTask)
.fork(
	console.error,
  	console.log
); // { id: 4, name: 'user1', best_friend_id: 5 }
复制代码

26. 同构(isomorphrism)

这里讨论的同构不是“前后端同构”的同构,而是一对满足如下要求的函数:

from(to(x)) == x
to(from(y)) == y

如果能够找到一对函数满足上述要求,则说明一个数据类型 x 具有与另一个数据类型 y 相同的信息或结构,此时我们说数据类型 x 和数据类型 y 是同构的。比如 String[char] 就是同构的:

const Iso = (to, from) =>({
  to,
  from
});

// String ~ [char]
const chars = Iso(s => s.split(''), arr => arr.join(''));

const res1 = chars.from(chars.to('hello world'));
const res2 = chars.to(chars.from(['a', 'b', 'c']));
console.log(res1, res2); // hello world [ 'a', 'b', 'c' ]
复制代码

这有什么用呢?我们举个例子:

const filterString = (str1, str2, pred) =>
  chars.from(chars.to(str1 + str2).filter(pred));

const res1 = filterString('hello', 'HELLO', x => x.match(/[aeiou]/ig));

console.log(res1); // eoEO

const toUpperCase = (arr1, arr2) =>
  chars.to(chars.from(arr1.concat(arr2)).toUpperCase());

const res2 = toUpperCase(['h', 'e', 'l', 'l', 'o'], ['w', 'o', 'r', 'l', 'd']);

console.log(res2); // [ 'H', 'E', 'L', 'L', 'O', 'W', 'O', 'R', 'L', 'D' ]
复制代码

这里我们借助 Arrayfilter 方法来过滤 String 中的字符;借助 StringtoUpperCase 方法来处理字符数组的大小写转换。可见有了同构,我们可以在两种不同的数据类型之间互相转换并调用其方法。


以上所述就是小编给大家介绍的《深入学习javascript函数式编程》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

About Face 3

About Face 3

Alan Cooper、Robert Reimann、David Cronin / John Wiley & Sons / 2007-5-15 / GBP 28.99

* The return of the authoritative bestseller includes all new content relevant to the popularization of how About Face maintains its relevance to new Web technologies such as AJAX and mobile platforms......一起来看看 《About Face 3》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试