JavaScript 函数式编程(三)

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

内容简介:以下内容主要参考自假设有个函数,可以接收一个来自用户输入的数字字符串。我们需要对其预处理一下,去除多余空格,将其转换为数字并加一,最后返回该值对应的字母。代码大概长这样...因缺思厅,这代码嵌套的也太紧凑了,看多了“老阔疼”,赶紧重构一把...

以下内容主要参考自 Professor Frisby Introduces Composable Functional JavaScript

JavaScript 函数式编程(三)

4.1.容器(Box)

假设有个函数,可以接收一个来自用户输入的数字字符串。我们需要对其预处理一下,去除多余空格,将其转换为数字并加一,最后返回该值对应的字母。代码大概长这样...

const nextCharForNumStr = (str) =>
  String.fromCharCode(parseInt(str.trim()) + 1)

nextCharForNumStr(' 64 ') // "A"
复制代码

因缺思厅,这代码嵌套的也太紧凑了,看多了“老阔疼”,赶紧重构一把...

JavaScript 函数式编程(三)
const nextCharForNumStr = (str) => {
  const trimmed = str.trim()
  const number = parseInt(trimmed)
  const nextNumber = number + 1
  return String.fromCharCode(nextNumber)
}

nextCharForNumStr(' 64 ') // 'A'
复制代码

很显然,经过之前内容的熏(xi)陶(nao),一眼就可以看出这个修订版代码很不 Pointfree...

为了这些只用一次的中间变量还要去想或者去查翻译,也是容易“老阔疼”,再改再改~

JavaScript 函数式编程(三)
const nextCharForNumStr = (str) => [str]
  .map(s => s.trim())
  .map(s => parseInt(s))
  .map(i => i + 1)
  .map(i => String.fromCharCode(i))

nextCharForNumStr(' 64 ') // ['A']
复制代码

这次借助数组的 map 方法,我们将必须的4个步骤拆分成了4个小函数。

这样一来再也不用去想中间变量的名称到底叫什么,而且每一步做的事情十分的清晰,一眼就可以看出这段代码在干嘛。

我们将原本的字符串变量 str 放在数组中变成了 [str],这里就像放在一个容器里一样。

代码是不是感觉好 door~~ 了?

JavaScript 函数式编程(三)

不过在这里我们可以更进一步,让我们来创建一个新的类型 Box。我们将同样定义 map 方法,让其实现同样的功能。

const Box = (x) => ({
  map: f => Box(f(x)),        // 返回容器为了链式调用
  fold: f => f(x),            // 将元素从容器中取出
  inspect: () => `Box(${x})`, // 看容器里有啥
})

const nextCharForNumStr = (str) => Box(str)
  .map(s => s.trim())
  .map(i => parseInt(i))
  .map(i => i + 1)
  .map(i => String.fromCharCode(i))
  .fold(c => c.toLowerCase()) // 可以轻易地继续调用新的函数

nextCharForNumStr(' 64 ') // a
复制代码

此外创建一个容器,除了像函数一样直接传递参数以外,还可以使用静态方法 of

函数式编程一般约定,函子有一个 of 方法,用来生成新的容器。

Box(1) === Box.of(1)
复制代码

其实这个 Box 就是一个函子(functor),因为它实现了 map 函数。当然你也可以叫它 Mappable 或者其他名称。

不过为了保持与范畴学定义的名称一致,我们就站在巨人的肩膀上不要再发明新名词啦~(后面小节的各种奇怪名词也是来源于数学名词)。

functor 是实现了 map 函数并遵守一些特定规则的容器类型。

那么这些特定的规则具体是什么咧?

** 1. 规则一:**

fx.map(f).map(g) === fx.map(x => f(g)(x))
复制代码

这其实就是函数组合...

** 2. 规则二:**

const id = x => x

fx.map(id) === id(fx)
复制代码
JavaScript 函数式编程(三)

4.2.Either / Maybe

JavaScript 函数式编程(三)

假设现在有个需求:获取对应颜色的十六进制的 RGB 值,并返回去掉 # 后的大写值。

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

const redColor = findColor('red')
  .slice(1)
  .toUpperCase() // FF4444

const greenColor = findColor('green')
  .slice(1)
  .toUpperCase()
// Uncaught TypeError:
// Cannot read property 'slice' of undefined
复制代码

以上代码在输入已有颜色的 key 值时运行良好,不过一旦传入其他颜色就会报错。咋办咧?

暂且不提条件判断和各种奇技淫巧的错误处理。咱们来先看看函数式的解决方案~

函数式将错误处理抽象成一个 Either 容器,而这个容器由两个子容器 RightLeft 组成。

// Either 由 Right 和 Left 组成

const Left = (x) => ({
  map: f => Left(x),            // 忽略传入的 f 函数
  fold: (f, g) => f(x),         // 使用左边的函数
  inspect: () => `Left(${x})`,  // 看容器里有啥
})

const Right = (x) => ({
  map: f => Right(f(x)),        // 返回容器为了链式调用
  fold: (f, g) => g(x),         // 使用右边的函数
  inspect: () => `Right(${x})`, // 看容器里有啥
})

// 来测试看看~
const right = Right(4)
  .map(x => x * 7 + 1)
  .map(x => x / 2)

right.inspect() // Right(14.5)
right.fold(e => 'error', x => x) // 14.5

const left = Left(4)
  .map(x => x * 7 + 1)
  .map(x => x / 2)

left.inspect() // Left(4)
left.fold(e => 'error', x => x) // error
复制代码

可以看出 RightLeft 相似于 Box

  • 最大的不同就是 fold 函数,这里需要传两个回调函数,左边的给 Left 使用,右边的给 Right 使用。
  • 其次就是 Leftmap 函数忽略了传入的函数(因为出错了嘛,当然不能继续执行啦)。

现在让我们回到之前的问题来~

const fromNullable = (x) => x == null
  ? Left(null)
  : Right(x)

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

findColor('green')
  .map(c => c.slice(1))
  .fold(
    e => 'no color',
    c => c.toUpperCase()
  ) // no color
复制代码

从以上代码不知道各位读者老爷们有没有看出使用 Either 的好处,那就是可以放心地对于这种类型的数据进行任何操作,而不是在每个函数里面小心翼翼地进行参数检查。

4.3. Chain / FlatMap / bind / >>=

假设现在有个 json 文件里面保存了端口,我们要读取这个文件获取端口,要是出错了返回默认值 3000。

// config.json
{ "port": 8888 }

// chain.js
const fs = require('fs')

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

const result = getPort()
复制代码

so easy~,下面让我们来用 Either 来重构下看看效果。

const fs = require('fs')

const Left = (x) => ({ ... })
const Right = (x) => ({ ... })

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, c => c.port)
复制代码

啊,常规操作,看起来不错哟~

不错你个蛇头...!

以上代码有个 bug ,当 json 文件写的有问题时,在 JSON.parse 时会出错,所以这步也要用 tryCatch 包起来。

但是,问题来了...

返回值这时候可能是 Right(Right('')) 或者 Right(Left(e)) (想想为什么不是 Left(Right('')) 或者 Left(Left(e)))

也就是说我们现在得到的是两层容器,就像俄罗斯套娃一样...

要取出容器中的容器中的值,我们就需要 fold 两次...!(若是再多几层...)

JavaScript 函数式编程(三)

因缺思厅,所以聪明机智的函数式又想出一个新方法 chain~,其实很简单,就是我知道这里要返回容器了,那就不要再用容器包了呗。

...

const Left = (x) => ({
  ...
  chain: f => Left(x) // 和 map 一样,直接返回 Left
})

const Right = (x) => ({
  ...
  chain: f => f(x),   // 直接返回,不使用容器再包一层了
})

const tryCatch = (f) => { ... }

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

其实这里的 LeftRight 就是单子(Monad),因为它实现了 chain 函数。

monad 是实现了 chain 函数并遵守一些特定规则的容器类型。

在继续介绍这些特定规则前,我们先定义一个 join 函数:

// 这里的 m 指的是一种 Monad 实例
const join = m => m.chain(x => x)
复制代码
  1. 规则一:
join(m.map(join)) === join(join(m))
复制代码
  1. 规则二:
// 这里的 M 指的是一种 Monad 类型
join(M.of(m)) === join(m.map(M.of))
复制代码

这条规则说明了 map 可被 chainof 所定义。

m.map(f) === m.chain(x => M.of(f(x)))
复制代码

也就是说 Monad 一定是 Functor

Monad 十分强大,之后我们将利用它处理各种副作用。但别对其感到困惑, chain 的主要作用不过将两种不同的类型连接( join )在一起罢了。

JavaScript 函数式编程(三)

4.4.半群(Semigroup)

定义一:对于非空集合 S,若在 S 上定义了二元运算 ○,使得对于任意的 a, b ∈ S,有 a ○ b ∈ S,则称 {S, ○} 为广群。

定义二:若 {S, ○} 为广群,且运算 ○ 还满足结合律,即:任意 a, b, c ∈ S,有 (a ○ b) ○ c = a ○ (b ○ c),则称 {S, ○} 为半群。

举例来说,JavaScript 中有 concat 方法的对象都是半群。

// 字符串和 concat 是半群
'1'.concat('2').concat('3') === '1'.concat('2'.concat('3'))

// 数组和 concat 是半群
[1].concat([2]).concat([3]) === [1].concat([2].concat([3]))
复制代码

虽然理论上对于 <Number, +> 来说它符合半群的定义:

  • 数字相加返回的仍然是数字(广群)
  • 加法满足结合律(半群)

但是数字并没有 concat 方法

没事儿,让我们来实现这个由 <Number, +> 组成的半群 Sum。

const Sum = (x) => ({
  x,
  concat: ({ x: y }) => Sum(x + y), // 采用解构获取值
  inspect: () => `Sum(${x})`,
})

Sum(1)
  .concat(Sum(2))
  .inspect() // Sum(3)
复制代码

除此之外, <Boolean, &&> 也满足半群的定义~

const All = (x) => ({
  x,
  concat: ({ x: y }) => All(x && y), // 采用解构获取值
  inspect: () => `All(${x})`,
})

All(true)
  .concat(All(false))
  .inspect() // All(true)
复制代码

最后,让我们对于字符串创建一个新的半群 First,顾名思义,它会忽略除了第一个参数以外的内容。

const First = (x) => ({
  x,
  concat: () => First(x), // 忽略后续的值
  inspect: () => `First(${x})`,
})

First('blah')
  .concat(First('yoyoyo'))
  .inspect() // First('blah')
复制代码

咿呀哟?是不是感觉这个半群和其他半群好像有点儿不太一样,不过具体是啥又说不上来...?

这个问题留给下个小节。在此先说下这玩意儿有啥用。

const data1 = {
  name: 'steve',
  isPaid: true,
  points: 10,
  friends: ['jame'],
}
const data2 = {
  name: 'steve',
  isPaid: false,
  points: 2,
  friends: ['young'],
}
复制代码

假设有两个数据,需要将其合并,那么利用半群,我们可以对 name 应用 First,对于 isPaid 应用 All,对于 points 应用 Sum,最后的 friends 已经是半群了...

const Sum = (x) => ({ ... })
const All = (x) => ({ ... })
const First = (x) => ({ ... })

const data1 = {
  name: First('steve'),
  isPaid: All(true),
  points: Sum(10),
  friends: ['jame'],
}
const data2 = {
  name: First('steve'),
  isPaid: All(false),
  points: Sum(2),
  friends: ['young'],
}

const concatObj = (obj1, obj2) => Object.entries(obj1)
  .map(([ key, val ]) => ({
    // concat 两个对象的值
    [key]: val.concat(obj2[key]),
  }))
  .reduce((acc, cur) => ({ ...acc, ...cur }))

concatObj(data1, data2)
/*
  {
    name: First('steve'),
    isPaid: All(false),
    points: Sum(12),
    friends: ['jame', 'young'],
  }
*/
复制代码

4.5.幺半群(Monoid)

幺半群是一个存在单位元(幺元)的半群。

半群我们都懂,不过啥是单位元?

单位元:对于半群 <S, ○>,存在 e ∈ S,使得任意 a ∈ S 有 a ○ e = e ○ a

举例来说,对于数字加法这个半群来说,0就是它的单位元,所以 <Number, +, 0> 就构成一个幺半群。同理:

<Number, *>
<Boolean, &&>
<Boolean, ||>
<Number, Min>
<Number, Max>

那么 <String, First> 是幺半群么?

显然我们 并不能 找到这样一个单位元 e 满足

First(e).concat(First('steve')) === First('steve').concat(First(e))

这就是上一节留的小悬念,为何会感觉 First 与 Sum 和 All 不太一样的原因。

格叽格叽,这两者有啥具体的差别么?

其实看到幺半群的第一反应应该是 默认值或初始值 ,例如 reduce 函数的第二个参数就是传入一个初始值或者说是默认值。

// sum
const Sum = (x) => ({
  ...
  empty: () => Sum(0), // 单位元
})

const sum = xs => xs.reduce((acc, cur) => acc + cur, 0)

sum([1, 2, 3])  // 6
sum([])         // 0,而不是报错!

// all
const All = (x) => ({
  ...
  empty: () => All(true), // 单位元
})

const all = xs => xs.reduce((acc, cur) => acc && cur, true)

all([true, false, true]) // false
all([])                  // true,而不是报错!

// first
const First = (x) => ({ ... })

const first = xs => xs.reduce(acc, cur) => acc)

first(['steve', 'jame', 'young']) // steve
first([])                         // boom!!!
复制代码

从以上代码可以看出幺半群比半群要 安全 得多,

4.6.foldMap

1.套路

在上一节中幺半群的使用代码中,如果传入的都是幺半群实例而不是原始类型的话,你会发现其实都是一个套路...

const Monoid = (x) => ({ ... })

const monoid = xs => xs.reduce(
    (acc, cur) => acc.concat(cur),  // 使用 concat 结合
    Monoid.empty()                  // 传入幺元
)

monoid([Monoid(a), Monoid(b), Monoid(c)]) // 传入幺半群实例
复制代码

所以对于思维高度抽象的函数式来说,这样的代码肯定是需要继续重构精简的~

2.List、Map

在讲解如何重构之前,先介绍两个炒鸡常用的不可变数据结构: ListMap

顾名思义,正好对应原生的 ArrayObject

3.利用 List、Map 重构

因为 immutable 库中的 ListMap 并没有 empty 属性和 fold 方法,所以我们首先扩展 List 和 Map~

import { List, Map } from 'immutable'

const derived = {
  fold (empty) {
    return this.reduce((acc, cur) => acc.concat(cur), empty)
  },
}

List.prototype.empty = List()
List.prototype.fold = derived.fold

Map.prototype.empty = Map({})
Map.prototype.fold = derived.fold

// from https://github.com/DrBoolean/immutable-ext
复制代码

这样一来上一节的代码就可以精简成这样:

List.of(1, 2, 3)
  .map(Sum)
  .fold(Sum.empty())     // Sum(6)

List().fold(Sum.empty()) // Sum(0)

Map({ steve: 1, young: 3 })
  .map(Sum)
  .fold(Sum.empty())     // Sum(4)

Map().fold(Sum.empty())  // Sum(0)
复制代码

4.利用 foldMap 重构

注意到 mapfold 这两步操作,从逻辑上来说是一个操作,所以我们可以新增 foldMap 方法来结合两者。

import { List, Map } from 'immutable'

const derived = {
  fold (empty) {
    return this.foldMap(x => x, empty)
  },
  foldMap (f, empty) {
    return empty != null
      // 幺半群中将 f 的调用放在 reduce 中,提高效率
      ? this.reduce(
          (acc, cur, idx) =>
            acc.concat(f(cur, idx)),
          empty
      )
      : this
        // 在 map 中调用 f 是因为考虑到空的情况
        .map(f)
        .reduce((acc, cur) => acc.concat(cur))
  },
}

List.prototype.empty = List()
List.prototype.fold = derived.fold
List.prototype.foldMap = derived.foldMap

Map.prototype.empty = Map({})
Map.prototype.fold = derived.fold
Map.prototype.foldMap = derived.foldMap

// from https://github.com/DrBoolean/immutable-ext
复制代码

所以最终版长这样:

List.of(1, 2, 3)
  .foldMap(Sum, Sum.empty()) // Sum(6)
List()
  .foldMap(Sum, Sum.empty()) // Sum(0)

Map({ a: 1, b: 3 })
  .foldMap(Sum, Sum.empty()) // Sum(4)
Map()
  .foldMap(Sum, Sum.empty()) // Sum(0)
复制代码

4.7.LazyBox

下面我们要来实现一个新容器 LazyBox

顾名思义,这个容器很懒...

虽然你可以不停地用 map 给它分配任务,但是只要你不调用 fold 方法催它执行(就像 deadline 一样),它就死活不执行...

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

const result = LazyBox(() => ' 64 ')
  .map(s => s.trim())
  .map(i => parseInt(i))
  .map(i => i + 1)
  .map(i => String.fromCharCode(i))
  // 没有 fold 死活不执行

result.fold(c => c.toLowerCase()) // a
复制代码

4.8.Task

1.基本介绍

有了上一节中 LazyBox 的基础之后,接下来我们来创建一个新的类型 Task。

首先 Task 的构造函数可以接收一个函数以便延迟计算,当然也可以用 of 方法来创建实例,很自然的也有 mapchainconcatempty 等方法。

与众不同的是它有个 fork 方法(类似于 LazyBox 中的 fold 方法,在 fork 执行前其他函数并不会执行),以及一个 rejected 方法,类似于 Left ,忽略后续的操作。

import Task from 'data.task'

const showErr = e => console.log(`err: ${e}`)
const showSuc = x => console.log(`suc: ${x}`)

Task
  .of(1)
  .fork(showErr, showSuc) // suc: 1

Task
  .of(1)
  .map(x => x + 1)
  .fork(showErr, showSuc) // suc: 2

// 类似 Left
Task
  .rejected(1)
  .map(x => x + 1)
  .fork(showErr, showSuc) // err: 1

Task
  .of(1)
  .chain(x => new Task.of(x + 1))
  .fork(showErr, showSuc) // suc: 2
复制代码

2.使用示例

接下来让我们做一个发射飞弹的程序~

const lauchMissiles = () => (
  // 和 promise 很像,不过 promise 会立即执行
  // 而且参数的位置也相反
  new Task((rej, res) => {
    console.log('lauchMissiles')
    res('missile')
  })
)

// 继续对之前的任务添加后续操作(duang~给飞弹加特技!)
const app = lauchMissiles()
  .map(x => x + '!')

// 这时才执行(发射飞弹)
app.fork(showErr, showSuc)
复制代码

3.原理意义

上面的代码乍一看好像没啥用,只不过是把待执行的代码用函数包起来了嘛,这还能吹上天?

还记得前面章节说到的副作用么?虽然说使用纯函数是没有副作用的,但是日常项目中有各种必须处理的副作用。

所以我们将有副作用的代码给包起来之后,这些新函数就都变成了纯函数,这样我们的整个应用的代码都是纯的~,并且在代码真正执行前( fork 前)还可以不断地 compose 别的函数,为我们的应用不断添加各种功能,这样一来整个应用的代码流程都会十分的简洁漂亮。

JavaScript 函数式编程(三)

4.异步嵌套示例

以下代码做了 3 件事:

  1. 读取 config1.json 中的数据
  2. 将内容中的 8 替换成 6
  3. 将新内容写到 config2.json 中
import fs from 'fs'

const app = () => (
  fs.readFile('config1.json', 'utf-8', (err, contents) => {
    if (err) throw err

    const newContents = content.replace(/8/g, '6')

    fs.writeFile('config2.json', newContents, (err, _) => {
      if (err) throw err

      console.log('success!')
    })
  })
)
复制代码

让我们用 Task 来改写一下~

import fs from 'fs'
import Task from 'data.task'

const cfg1 = 'config1.json'
const cfg2 = 'config2.json'

const readFile = (file, enc) => (
  new Task((rej, res) =>
    fs.readFile(file, enc, (err, str) =>
      err ? rej(err) : res(str)
    )
  )
)

const writeFile = (file, str) => (
  new Task((rej, res) =>
    fs.writeFile(file, str, (err, suc) =>
      err ? rej(err) : res(suc)
    )
  )
)

const app = readFile(cfg1, 'utf-8')
  .map(str => str.replace(/8/g, '6'))
  .chain(str => writeFile(cfg2, str))

app.fork(
  e => console.log(`err: ${e}`),
  x => console.log(`suc: ${x}`)
)
复制代码

代码一目了然,按照线性的先后顺序完成了任务,并且在其中还可以随意地插入或修改需求~

4.9.Applicative Functor

1.问题引入

Applicative Functor 提供了让不同的函子(functor)互相应用的能力。

为啥我们需要函子的互相应用?什么是互相应用?

先来看个简单例子:

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

add(Box.of(2))(Box.of(3)) // NaN

Box(2).map(add).inspect() // Box(y => 2 + y)
复制代码

现在我们有了一个容器,它的内部值为局部调用(partially applied)后的函数。接着我们想让它应用到 Box(3) 上,最后得到 Box(5) 的预期结果。

说到从容器中取值,那肯定第一个想到 chain 方法,让我们来试一下:

Box(2)
  .chain(x => Box(3).map(add(x)))
  .inspect() // Box(5)
复制代码

成功实现~,BUT,这种实现方法有个问题,那就是单子(Monad)的 执行顺序 问题。

我们这样实现的话,就必须等 Box(2) 执行完毕后,才能对 Box(3) 进行求值。假如这是两个异步任务,那么完全无法并行执行。

别慌,吃口药~

2.基本介绍

下面介绍下主角: ap ~:

const Box = (x) => ({
  // 这里 box 是另一个 Box 的实例,x 是函数
  ap: box => box.map(x),
  ...
})

Box(add)
  // Box(y => 2 + y) ,咦?在哪儿见过?
  .ap(Box(2))
  .ap(Box(3)) // Box(5)
复制代码

运算规则

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

// 这就是为什么
Box(2).map(add) === Box(add).ap(Box(2))
复制代码

3.Lift 家族

由于日常编写代码的时候直接用 ap 的话模板代码太多,所以一般通过使用 Lift 家族系列函数来简化。

// F 该从哪儿来?
const fakeLiftA2 = f => fx => fy => F(f).ap(fx).ap(fy)

// 应用运算规则转换一下~
const liftA2 = f => fx => fy => fx.map(f).ap(fy)

liftA2(add, Box(2), Box(4)) // Box(6)

// 同理
const liftA3 = f => fx => fy => fz => fx.map(f).ap(fy).ap(fz)
const liftA4 = ...
...
const liftAN = ...
复制代码

4.Lift 应用

  • 例1
// 假装是个 jQuery 接口~
const $ = selector =>
  Either.of({ selector, height: 10 })

const getScreenSize = screen => head => foot =>
  screen - (head.height + foot.height)

liftA2(getScreenSize(800))($('header'))($('footer')) // Right(780)
复制代码
  • 例2
// List 的笛卡尔乘积
List.of(x => y => z => [x, y, z].join('-'))
  .ap(List.of('tshirt', 'sweater'))
  .ap(List.of('white', 'black'))
  .ap(List.of('small', 'medium', 'large'))
复制代码
  • 例3
const Db = ({
  find: (id, cb) =>
    new Task((rej, res) =>
      setTimeout(() => res({ id, title: `${id}`}), 100)
    )
})

const reportHeader = (p1, p2) =>
  `Report: ${p1.title} compared to ${p2.title}`

Task.of(p1 => p2 => reportHeader(p1, p2))
  .ap(Db.find(20))
  .ap(Db.find(8))
  .fork(console.error, console.log) // Report: 20 compared to 8

liftA2
  (p1 => p2 => reportHeader(p1, p2))
  (Db.find(20))
  (Db.find(8))
  .fork(console.error, console.log) // Report: 20 compared to 8
复制代码

4.10.Traversable

1.问题引入

import fs from 'fs'

// 详见 4.8.
const readFile = (file, enc) => (
  new Task((rej, res) => ...)
)

const files = ['a.js', 'b.js']

// [Task, Task],我们得到了一个 Task 的数组
files.map(file => readFile(file, 'utf-8'))
复制代码

然而我们想得到的是一个包含数组的 Task([file1, file2]) ,这样就可以调用它的 fork 方法,查看执行结果。

为了解决这个问题,函数式编程一般用一个叫做 traverse 的方法来实现。

files
  .traverse(Task.of, file => readFile(file, 'utf-8'))
  .fork(console.error, console.log)
复制代码

traverse 方法第一个参数是创建函子的函数,第二个参数是要应用在函子上的函数。

2.实现

其实以上代码有 bug ...,因为数组 Array 是没有 traverse 方法的。没事儿,让我们来实现一下~

Array.prototype.empty = []

// traversable
Array.prototype.traverse = function (point, fn) {
  return this.reduce(
    (acc, cur) => acc
      .map(z => y => z.concat(y))
      .ap(fn(cur)),
    point(this.empty)
  )
}
复制代码

看着有点儿晕?

不急,首先看代码主体是一个 reduce ,这个很熟了,就是从左到右遍历元素,其中的第二个参数传递的就是幺半群(monoid)的单位元(empty)。

再看第一个参数,主要就是通过 applicative functor 调用 ap 方法,再将其执行结果使用 concat 方法合并到数组中。

所以最后返回的就是 Task([foo, bar]) ,因此我们可以调用 fork 方法执行它。

4.11.自然变换(Natural Transformations)

1.基本概念

自然变换就是一个函数,接受一个函子(functor),返回另一个函子。看看代码熟悉下~

const boxToEither = b => b.fold(Right)
复制代码

这个 boxToEither 函数就是一个自然变换(nt),它将函子 Box 转换成了另一个函子 Either

那么我们用 Left 行不行呢?

答案是不行!

因为自然变换不仅是将一个函子转换成另一个函子,它还满足以下规则:

nt(x).map(f) == nt(x.map(f))
复制代码
JavaScript 函数式编程(三)

举例来说就是:

const res1 = boxToEither(Box(100))
  .map(x => x * 2)
const res2 = boxToEither(
  Box(100).map(x => x * 2)
)

res1 === res2 // Right(200)
复制代码

即先对函子 a 做改变再将其转换为函子 b ,是等价于先将函子 a 转换为函子 b 再做改变。

显然, Left 并不满足这个规则。所以任何满足这个规则的函数都是 自然变换

2.应用场景

1.例1:得到一个数组小于等于 100 的最后一个数的两倍的值

const arr = [2, 400, 5, 1000]
const first = xs => fromNullable(xs[0])
const double = x => x * 2
const getLargeNums = xs => xs.filter(x => x > 100)

first(
  getLargeNums(arr).map(double)
)
复制代码

根据自然变换,它显然和 first(getLargeNums(arr)).map(double) 是等价的。但是后者显然性能好得多。

再来看一个更复杂一点儿的例子:

2.例2:找到 id 为 3 的用户的最好的朋友的 id

// 假 api
const fakeApi = (id) => ({
  id,
  name: 'user1',
  bestFriendId: id + 1,
})

// 假 Db
const Db = {
  find: (id) => new Task(
    (rej, res) => (
      res(id > 2
        ? Right(fakeApi(id))
        : Left('not found')
      )
    )
  )
}
复制代码
// Task(Either(user))
const zero = Db.find(3)

// 第一版
// Task(Either(Task(Either(user)))) ???
const one = zero
  .map(either => either
    .map(user => Db
      .find(user.bestFriendId)
    )
  )
  .fork(
    console.error,
    either => either // Either(Task(Either(user)))
      .map(t => t.fork( // Task(Either(user))
        console.error,
        either => either
            .map(console.log), // Either(user)
      ))
  )
复制代码
JavaScript 函数式编程(三)

这是什么鬼???

肯定不能这么干...

// Task(Either(user))
const zero = Db.find(3)

// 第二版
const two = zero
  .chain(either => either
    .fold(Task.rejected, Task.of) // Task(user)
    .chain(user => Db
      .find(user.bestFriendId) // Task(Either(user))
    )
    .chain(either => either
      .fold(Task.rejected, Task.of) // Task(user)
    )
  )
  .fork(
    console.error,
    console.log,
  )
复制代码

第二版的问题是多余的嵌套代码。

// Task(Either(user))
const zero = Db.find(3)

// 第三版
const three = zero
  .chain(either => either
    .fold(Task.rejected, Task.of) // Task(user)
  )
  .chain(user => Db
    .find(user.bestFriendId) // Task(Either(user))
  )
  .chain(either => either
    .fold(Task.rejected, Task.of) // Task(user)
  )
  .fork(
    console.error,
    console.log,
  )
复制代码

第三版的问题是多余的重复逻辑。

// Task(Either(user))
const zero = Db.find(3)

// 这其实就是自然变换
// 将 Either 变换成 Task
const eitherToTask = (e) => (
  e.fold(Task.rejected, Task.of)
)

// 第四版
const four = zero
  .chain(eitherToTask) // Task(user)
  .chain(user => Db
    .find(user.bestFriendId) // Task(Either(user))
  )
  .chain(eitherToTask) // Task(user)
  .fork(
    console.error,
    console.log,
  )

// 出错版
const error = Db.find(2) // Task(Either(user))
  // Task.rejected('not found')
  .chain(eitherToTask)
  // 这里永远不会被调用,被跳过了
  .chain(() => console.log('hey man'))
  ...
  .fork(
    console.error, // not found
    console.log,
  )
复制代码

4.12.同构(Isomorphism)

同构是在数学对象之间定义的一类映射,它能揭示出在这些对象的属性或者操作之间存在的关系。

简单来说就是两种不同类型的对象经过变形,保持结构并且不丢失数据。

具体怎么做到的呢?

其实同构就是一对儿函数: tofrom ,遵守以下规则:

to(from(x)) === x
from(to(y)) === y
复制代码

这其实说明了这两个类型都能够无损地保存同样的信息。

1. 例如 String[Char] 就是同构的。

// String ~ [Char]
const Iso = (to, from) => ({ to, from })

const chars = Iso(
  s => s.split(''),
  c => c.join('')
)

const str = 'hello world'

chars.from(chars.to(str)) === str
复制代码

这能有啥用呢?

const truncate = (str) => (
  chars.from(
    // 我们先用 to 方法将其转成数组
    // 这样就能使用数组的各类方法
    chars.to(str).slice(0, 3)
  ).concat('...')
)

truncate(str) // hel...
复制代码

2. 再来看看最多有一个参数的数组 [a]Either 的同构关系

// [a] ~ Either null a
const singleton = Iso(
  e => e.fold(() => [], x => [x]),
  ([ x ]) => x ? Right(x) : Left()
)

const filterEither = (e, pred) => singleton
  .from(
    singleton
      .to(e)
      .filter(pred)
  )

const getUCH = (str) => filterEither(
  Right(str),
  x => x.match(/h/ig)
).map(x => x.toUpperCase())

getUCH('hello') // Right(HELLO)

getUCH('ello') // Left(undefined)
复制代码

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

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

快速转行做产品经理

快速转行做产品经理

李三科 / 华中科技大学出版社 / 2018-6-1 / 39.90

互联网已经进入以产品为中心的时代,不懂技术一样做高薪产品经理。本书将满足你转行、就业、加薪的愿望。 . 作者李三科,互联网资深产品经理。2011年离开传统销售行业进入互联网行业工作,从对产品经理的工作一无所知,到成长为一名年薪几十万的资深产品经理,他对产品经理职业有着深刻的理解,也积累了丰富的学习、工作经验。本书以作者亲身经历为线索,讲解学习产品经理相关知识和工作方法的经验,同时介绍求......一起来看看 《快速转行做产品经理》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

在线进制转换器
在线进制转换器

各进制数互转换器

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具