# 第 11 章: 再转换一次，就很自然

接下去，本文将结合实际日常编程应用，讨论 *Natural Transformations* 。Natural Transformations 恰巧是范畴论的支柱，是利用数学知识分析、重构代码不可或缺之技巧。事先申明，因作者知识水平所限，本文如有纰漏不实，还望海涵。好了，让我们开始吧。

## 令人生厌的嵌套

我想要提出一个关于嵌套的问题。但不是那种马上会唤起老母亲整理打扫天性的那种问题，而是我们马上会在之后的章节中遇到的问题。在任何情况下，我所谓的*嵌套*是将不同的两个类型套在同一个值上，某种意义上，看上去就像一个全新的类型。

```js
Right(Maybe('b'));

IO(Task(IO(1000)));

[Identity('bee thousand')];
```

现在，我们用一些精心制作的例证来说明仿佛我们已经能够摆脱这些常见的嵌套情形。但是实际上我们编码的时候，不同的类型就像耳机线一样乱成一团，仿佛一种恶魔召唤的邪恶产物。如果我们编码的时候不是一丝不苟地组织正在使用的类型，最后代码会比猫咪咖啡厅的披头族更加难以理解。

## 一场情景喜剧

```js
// getValue :: Selector -> Task Error (Maybe String)
// postComment :: String -> Task Error Comment
// validate :: String -> Either ValidationError String

// saveComment :: () -> Task Error (Maybe (Either ValidationError (Task Error Comment)))
const saveComment = compose(
  map(map(map(postComment))),
  map(map(validate)),
  getValue('#comment'),
);
```

让类型声明形同虚设的坏家伙们都在这里了。容我稍微解释一下这一段代码。一开始，用`getValue('#comment')`获取用户输入，这是一个`action`，从一个`element`中获取文本。但是可能会因为字符串值不存在而获取失败，从而返回`Task Error(Maybe String)`。在这之后，我们需要`map`所有`Task`和`Maybe`传递文本到`validate`。最后，我们不是得到`ValiadtionError`或者想要的`String`。然后，从过层层 map，把当前`Task Error(Maybe(Either ValidationError String))`中的`String`传入`postComment`，最后返回`Task`的结果。 简直乱得骇人。一大堆抽象类型乱得像一幅业余表现主义的烂作，还混杂着波洛克式的多态，蒙德里安风的大统一。对于这种常见的情况，有许多解决方法。把这些类型编好弄成一个巨大的容器，分类后再`join`，将其同化，解构，等等。在下一章的重点，就在于通过 *Natural Transformations* 将他们同化。

## 全都很自然

*Natural Transformations*是“functor 之间的态射”，就是，一个操作容器本身的函数。类型上来说，它是个函数`(Functor f, Functor g) => f a -> g a`。让这个函数特殊的是，在任何情况下，都不能得知 functor 中的内容。就像两个党派交换机密文件，都会把东西封在马尼拉纸信封里，戳上“最高机密”。这是一个结构上的操作。functor 外壳的变化。形式上，*Natural Transformations* 作用于如下功能：

![natural transformation diagram](https://1559349009-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M4muAGLNCRVf0FD1jja%2Fuploads%2Fgit-blob-2141f24fac101e0cafa5e11ee75d7d2c85edf0ef%2Fnatural_transformation.png?alt=media)

或者代码:

```js
// nt :: (Functor f, Functor g) => f a -> g a
compose(map(f), nt) === compose(nt, map(f));
```

两者都体现了同样的东西：我们可以先自然变换再`map`，或者先`map`再自然变换，结果都是一样的。顺便，在 [free theorem](https://llh911001.gitbook.io/mostly-adequate-guide-chinese/ch7#free-as-in-theorem) 中，也提到，自然变换不仅局限于函数和类型（以及 functor）。

## 有原则的类型转换

作为程序员，我们对类型转换十分熟悉。我们经常把`String`转换成`Boolean`，或者`Integer` 转换成`Float`（虽然`JavaScript`只有`Number`）。这些类型之间的区别很简单，这些类型都是一些代数容器，我们手头有一些可以运用的理论。

以下是一些例子：

```js
// idToMaybe :: Identity a -> Maybe a
const idToMaybe = x => Maybe.of(x.$value);

// idToIO :: Identity a -> IO a
const idToIO = x => IO.of(x.$value);

// eitherToTask :: Either a b -> Task a b
const eitherToTask = either(Task.rejected, Task.of);

// ioToTask :: IO a -> Task () a
const ioToTask = x => new Task((reject, resolve) => resolve(x.unsafePerform()));

// maybeToTask :: Maybe a -> Task () a
const maybeToTask = x => (x.isNothing ? Task.rejected() : Task.of(x.$value));

// arrayToMaybe :: [a] -> Maybe a
const arrayToMaybe = x => Maybe.of(x[0]);
```

看到了吗？这些只是从一个 functor 换到了另一个。在转换过程中的数据丢失是被允许的，只要要被`map`的数据在转换（shape shift shuffle）中不会消失即可。这就是重点，`map`必须以定义执行，就算在转换之后。

从另一个角度来看，转换的是某个作用（effects）。由此可以认为，`ioToTask`就是从同步到异步的转换，和`arrayToMaybe`是从非确定性到可能失败。要注意的是，在`JavaScript` 里，并不能把异步变到同步，无法实现`taskToIO`方法，那是个"超自然"变换。

## 方法狂

假设要使用一些其他类型的方法（feature），比如对一个`List`进行`sortBy`。*Natural Transformations* 提供了变换为目标类型的巧妙方式，只要该类型支持`map`操作。

```js
// arrayToList :: [a] -> List a
const arrayToList = List.of;

const doListyThings = compose(sortBy(h), filter(g), arrayToList, map(f));
const doListyThings_ = compose(sortBy(h), filter(g), map(f), arrayToList); // law applied
```

捏一把鼻子，颠三下魔杖，放进`arrayToList`，当当当当，`[a]`变成了`List a`，甚至还可以对其`sortBy`。

此外，如`doListyThings_`所示，将映射操作`map(f)`移到 *Natural Transformations* 的左边，更加容易实现函数的优化或者复合。

## 同构的 JavaScript

当把一个值前后怎么转换都不会丢失任何数据时，可称之为 *同构*（isomorphic） 。看上去挺高大上，不过就是“保持相同的数据”而已。如果两个类型在 *Natural Transformations* 中既可以`to`也可以`from` ，就被称之为是 *同构* ，证明如下：

```js
// promiseToTask :: Promise a b -> Task a b
const promiseToTask = x => new Task((reject, resolve) => x.then(resolve).catch(reject));

// taskToPromise :: Task a b -> Promise a b
const taskToPromise = x => new Promise((resolve, reject) => x.fork(reject, resolve));

const x = Promise.resolve('ring');
taskToPromise(promiseToTask(x)) === x;

const y = Task.of('rabbit');
promiseToTask(taskToPromise(y)) === y;
```

证毕。

`Promise`与`Task`是 *同构 Isomorphic* 。也可以实现方法`listToArray`使之与`arrayToList`构成 *同构Isomorphism*。反之，与 `arrayToMaybe`无法 *同构 Isomorphism*，因为转换时有数据丢失。

```js
// maybeToArray :: Maybe a -> [a]
const maybeToArray = x => (x.isNothing ? [] : [x.$value]);

// arrayToMaybe :: [a] -> Maybe a
const arrayToMaybe = x => Maybe.of(x[0]);

const x = ['elvis costello', 'the attractions'];

// 不 isomorphic
maybeToArray(arrayToMaybe(x)); // ['elvis costello']

// 但是是一个 natural transformation
compose(arrayToMaybe, map(replace('elvis', 'lou')))(x); // Just('lou costello')
// ==
compose(map(replace('elvis', 'lou'), arrayToMaybe))(x); // Just('lou costello')
```

这就是 *Natural Transformations*，然而，因为两边的`map`都得到相同的结果。行文到本章的一半，似乎已经将 *Isomorphic* 说的差不多了，但是可别被这些表象所迷惑，真正的 *Isomorphic* 比我们想象的要广泛，有用的多。话不多说，让我们继续。

## 更加宽泛的定义

这些结构函数绝不仅仅局限于类型转换。 以下是一些不同的例子：

```js
reverse :: [a] -> [a]

join :: (Monad m) => m (m a) -> m a

head :: [a] -> a

of :: a -> f a
```

Natural Transformations 的法则同样适用于上述方法。其中可能让你有点疑惑的是，`head :: [a] -> a`可以被看做成`head :: [a] -> Identity a`。在其中的任何地方，都可以插入`Identity`，因为`a`与`Identity a`为 Isomorphic。（看，我说过 *Isomorphic* 用途很广泛吧）

## 实现单层嵌套的方法

回到之前的场景。在其中试试 *Natural Transformations*，使调用中的各个类型统一化，从而可以将函数`join`起来。

```js
// getValue :: Selector -> Task Error (Maybe String)
// postComment :: String -> Task Error Comment
// validate :: String -> Either ValidationError String

// saveComment :: () -> Task Error Comment
const saveComment = compose(
  chain(postComment),
  chain(eitherToTask),
  map(validate),
  chain(maybeToTask),
  getValue('#comment'),
);
```

这里具体的操作仅仅加入了`chain(maybeToTask)`和`chain(eigherToTack)`。都是同样的效果。在`join`时很自然的将`Task`的 functor 转换到另一个`Task`的 functor。就像窗边的尖刺驱鸟器，从源头扼杀了嵌套。就像他们巴黎（city of the light）人说的：“Mieux vaut prévenir que guérir” - 花一英镑去治疗不如花一盎司用于预防。

## 总结

*Natural Transformations* 是操作 functor 的方法。在范畴学中是非常重要的概念，特别是采用多种抽象化机制时，就会用的更多。上述例子，仅仅局限于几个具体的应用中。如上文所言，只要转换类型时,确保可组合性，即可以实现所需要的不同作用 effects。同时可以解决嵌套问题，虽然会将类型同化到最低的共同母类（lowest common denominator），在实际应用中，一般是作用最易变的函子（通常是`Task` ）（functor with most volatile effects）。

这种连续而冗长的类型是实现的代价 - 从以太中召唤而来。当然，隐式作用（implicit effects）更有潜在风险，所以也算是合理的方案。不过,如果想归并大量类型，还需要更多的工具。之后，我们将通过 *Traverable* 讨论重组类型。

[Chapter 12: Traversing the Stone](https://llh911001.gitbook.io/mostly-adequate-guide-chinese/ch12)

## 练习

写一个 natural transformation 将`Either b a`转换到`Maybe a`

```js
// eitherToMaybe :: Either b a -> Maybe a
const eitherToMaybe = undefined
```

***

```js
// eitherToTask :: Either a b -> Task a b
const eitherToTask = either(Task.rejected, Task.of)
```

使用`eitherToTask`, 简化`findNameById`方法, 去掉嵌套的`Either`。

```js
// findNameById :: Number -> Task Error (Either Error User)
const findNameById = compose(
  map(map(prop('name'))),
  findUserById
)
```

***

友情提示，以下方法可用于解练习题

```hs
split :: String -> String -> [String]
intercalate :: String -> [String] -> String
```

写出 String 和 \[Char]的 Isomorphic。

```js
// strToList :: String -> [Char]
const strToList = undefined

// listToStr :: [Char] -> String
const listToStr = undefined
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://llh911001.gitbook.io/mostly-adequate-guide-chinese/ch11.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
