内容简介:大家都知道普通函数是这样的:如果借助Array,可以这样实现:
大家都知道 JavaScript
可以作为 面向对象
或者 函数式
编程语言来使用,一般情况下大家理解的 函数式编程
无非包括 副作用
、 函数组合
、 柯里化
这些概念,其实并不然,如果往深了解学习会发现 函数式编程
还包括非常多的高级特性,比如 functor
、 monad
等。国外课程网站 egghead
上有个教授(名字叫Frisby)基于 JavaScript
讲解的 函数式编程
非常棒,主要介绍了 box
、 semigroup
、 monoid
、 functor
、 applicative functor
、 monad
、 isomorphism
等函数式编程相关的高级主题内容。整个课程大概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, '')); 复制代码
Box
式 moneyToFloat
:
const moneyToFloat = str => Box(str) .map(s => s.replace(/\$/g, '')) .fold(r => parseFloat(r)); 复制代码
我们这里使用 Box
重构了 moneyToFloat
, Box
擅长的地方就在于将嵌套表达式转成一个一个的 map
,这里虽然不是很复杂,但却是一种好的实践方式。
命令式 percentToFloat
:
const percentToFloat = str => { const replaced = str.replace(/\%/g, ''); const number = parseFloat(replaced); return number * 0.01; }; 复制代码
Box
式 percentToFloat
:
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
稍微麻烦点,因为该函数有两条数据流,不过我们可以借助闭包:
Box
式 applyDiscount
:
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" 复制代码
如果我们在 moneyToFloat
和 percentToFloat
中不进行拆箱(即 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)" 复制代码
这里我们暂且不实现 Right
的 fold
,而是先来实现 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
完全忽略了传入的数据转换函数,保持容器内部数据原样。有了 Right
和 Left
,我们可以对程序数据流进行分支控制。考虑到程序中经常会存在异常,因此容器通常都是未知类型 RightOrLeft
。
接下来我们实现 Right
和 Left
容器的 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})` }); 复制代码
测试一下 Right
和 Left
的 fold
方法:
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
,因此导致箱子套了两层,最后需要进行两次拆箱。为了解决这种箱子套箱子的问题,我们可以给 Right
和 Left
增加一个方法 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
方法满足结合律。比如 Array
和 String
:
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
安装 immutable
和 immutable-ext
。 immutable-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
就是这样的,比如 Box
和 Right
:
Box(3).fold(x => x); // 3 Right(3).fold(e => e, x => x); // 3 复制代码
没错,不过 fold
的本质就是拆箱。前面对 Box
和 Right
类型拆箱是将其值取出来;而现在对集合拆箱则是为了将集合的汇总结果取出来。而将一个集合中的多个值汇总成一个值就需要传入初始值 Sum.empty()
。因此当你看到 fold
时,应该看成是为了从一个类型中取值出来,而这个类型可能是一个仅含一个值的类型(比如 Box
, Right
),也可能是一个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
以及一系列数据转换函数,如果不调用 fork
, Task
中的 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) 复制代码
当然我们也可以定义类似的 liftA3
, liftA4
等工具函数:
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。
假设我们要计算页面上除了 header
和 footer
之外的高度:
const $ = selector => either.of({selector, height: 10}); // fake DOM selector const getScreenSize = (screen, header, footer) => screen - (header.height + footer.height); 复制代码
如果使用 monod
的 chain
方法,可以这样实现:
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}`; 复制代码
如果使用 monad
的 chain
实现,那么两个异步事件只能顺序执行:
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
是一个值为 Task
的 Map
,而我们想要的是 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' ] 复制代码
这里我们借助 Array
的 filter
方法来过滤 String
中的字符;借助 String
的 toUpperCase
方法来处理字符数组的大小写转换。可见有了同构,我们可以在两种不同的数据类型之间互相转换并调用其方法。
以上所述就是小编给大家介绍的《深入学习javascript函数式编程》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 深入理解 JavaScript 函数
- 【4】JavaScript 基础深入——函数、回调函数、IIFE、理解this
- 深入理解 Java 函数式编程,第 5 部分: 深入解析 Monad
- [译] 深入理解 JavaScript 回调函数
- 重读《深入理解ES6》—— 函数
- 深入理解 Java 函数式编程,第 1 部分: 函数式编程思想概论
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。