内容简介:大家都知道普通函数是这样的:如果借助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 部分: 函数式编程思想概论
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Book of CSS3
Peter Gasston / No Starch Press / 2011-5-13 / USD 34.95
CSS3 is the technology behind most of the eye-catching visuals on the Web today, but the official documentation can be dry and hard to follow. Luckily, The Book of CSS3 distills the heady technical la......一起来看看 《The Book of CSS3》 这本书的介绍吧!