深入学习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函数式编程》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

信息简史

信息简史

[美] 詹姆斯·格雷克 / 高博 / 人民邮电出版社 / 2013-10 / 69.00元

人类与信息遭遇的历史由来已久。詹姆斯•格雷克笔下的这段历史出人意料地从非洲的鼓语讲起(第1章)。非洲土著部落在尚未直接跨越到移动电话之前,曾用鼓声来传递讯息,但他们是如何做到的呢?后续章节进而讲述了这段历史上几个影响深远的关键事件,包括文字的发明(第2章)、罗伯特•考德里的第一本英语词典(第3章)、查尔斯•巴贝奇的差分机与爱达•拜伦的程序(第4章)、沙普兄弟的信号塔与摩尔斯电码(第5章)。 ......一起来看看 《信息简史》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

URL 编码/解码
URL 编码/解码

URL 编码/解码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具