函子02-either monad

3.9k words

书接上回,Maybe已经可以正确处理数据了。但是Maybe遇到意外分支时只返回了null,开发者不清楚是哪个环节出现了问题。 所以需要一个函子来保留意外分支的上下文信息.

🍐 Either 函子

Either 相较与 Maybe 函子,保留了错误分支的上下文环境。Maybe 遇到错误分支时返回null,而Either会持有当前发生错误的分支信息。

🌰 Either 核心实现

Either实现依靠Nothing, Nothing是一个不对持有值进行任何处理的函子。Maybe 函子上能正常执行的函数,在Nothing不会执行。基于这个特性,在遇到意外分支时,即可使用 Nothing 保存当前的意外分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/** 用于接受意外分支的函子 */
class Nothing {
constructor(value) {
this.value = value
}

// 不做任何处理,返回自身
map(fn) {
return this
}

static of(value) {
return new this(value)
}
}

const val = new Nothing('hello').map((str) => `say: ${str}`) // Nothing { value: "hello" }

🌰 Either 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/** Some是个基础函子,实战中可替换为其他 */
class Some {
constructor(value) {
this.value = value
}

map(fn) {
return Some.of(fn(this.value))
}

static of(value) {
return new this(value)
}
}

/** 单词首字母大写 */
const capitalCase = (val) => {
try {
return new Some(val).map((str) => `${str[0].toUpperCase()}${str.substring(1)}`)
} catch (err) {
return new Nothing(err)
}
}

// Some { value: "result: Hello" }
console.log(capitalCase('hello').map((str) => `result: ${str}`))

// Nothing { value: TypeError: Cannot read properties of undefined (reading "0") }
console.log(capitalCase(undefined).map((str) => `result: ${str}`))

这里 Nothing 很好地捕获到了错误的位置。

🍐 Either 简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Either {
constructor(value) {
this.value = value
}

map(fn) {
try {
return Either.of(fn(this.value))
} catch (err) {
return new Nothing(err)
}
}

static of(value) {
return new this(value)
}
}

/** 使用 Either 来处理逻辑 */
const capitalCase2 = (val) => {
return new Either(val).map((str) => `${str[0].toUpperCase()}${str.substring(1)}`)
}

// 正常返回 Either { value: "result: Hello" }
console.log(capitalCase2('hello').map((str) => `result: ${str}`))

// 捕获错误 Nothing { value: TypeError: Cannot read properties of undefined (reading "0") }
console.log(capitalCase2(undefined).map((str) => `result: ${str}`))

🍐 Monad 函子

具有 chain 方法的 Pointed函子可称为 Monad函子, 用于解决多重map调用值嵌套问题。

! 多重map调用

以下示例中,在第一个MayBe函子的map调用中返回一个新的函子做为值。这情况很常见在实际业务开发中,嵌套情况只会更复杂。

1
2
3
4
5
6
const val = new MayBe('hello').map((str) => new MayBe(str).map((str) => str.toUpperCase()))

console.log(val) // MayBe { value: MayBe { value: "HELLO" } }

// 🙅🏻‍♀️ 不优雅
console.log(val.value.value) // "HELLO"

💡 使用 join 结果问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class MayBe {
/** start MayBe */
static of(value) {
return new this(value)
}
constructor(value) {
this.value = value
}
map(fn) {
return MayBe.of(this.isNothing() ? null : fn(this.value))
}
isNothing() {
return this.value === null || this.value === undefined
}
/** end MayBe */

// 如果值存在返回值,反之返回一个空的函子
join() {
return this.isNothing() ? MayBe.of(null) : this.value
}
}

// 对于结果使用 join 进行解套
const val = new MayBe('hello').map((str) => new MayBe(str).map((str) => str.toUpperCase()).join())
console.log(val) // MayBe { value: "HELLO" }

/** 💡 更多 `join` 使用场景 */
const num = new MayBe(new MayBe(30).map((n) => n + 3))
console.log(num.join().map((n) => n + 200)) // MayBe { value: 233 }

💡 使用 chain

join 可以解套单个函子的嵌套问题,但多个函子都要执行一次join。这部分的逻辑可以封装到chain方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class MayBe {
static of(value) {
return new this(value)
}

constructor(value) {
this.value = value
}

map(fn) {
return MayBe.of(this.isNothing() ? null : fn(this.value))
}

isNothing() {
return this.value === null || this.value === undefined
}

// 如果值存在返回值,反之返回一个空的函子
join() {
return this.isNothing() ? MayBe.of(null) : this.value
}

// 默认执行一次join
chain(fn) {
return this.map(fn).join()
}
}

// 更简洁了
const num = new MayBe(30).chain((n) => new MayBe(n + 3).map((n) => n + 200))
console.log(num) // MayBe { value: 233 }