原文地址:https://mostly-adequate.gitbooks.io/mostly-adequate-guide/
前十章中文翻译:https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/
说明:
- 编程/计算机基础较弱,理解不到原作者的深度,翻译一定会有问题,尽量在自己成长的同时保持更新。
- 英语也弱,try my best,也是保持更新/修复。
- 只讲重点,不会像前十章翻译那样直译,觉得无关紧要的引用或叙述,统统做总结简述。
第11章 自然转换
诅咒嵌套
此嵌套指:两种以上的类型将一个值包裹起来,如下
Right(Maybe('b'));
IO(Task(IO(1000)));
[Identity('bee thousand')];
一个情景喜剧
// 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'),
);
有多种方案解决这个常见的问题:将多重类型组合成一个巨大的容器、排序和join、同质化、解构等等。这章我们集中在通过自然转换同质化它们。
纯天然
自然转换是函子间的态射,意即:作用在容器上的函数。它是一个函数:(Functor f, Functor g) => f a -> g a
。特别之处在于,无论如何,我们不能偷看函子的内容。把它想象成高级机密的交换。这是一个结构上的操作,一个函子的变装。正式来说:自然转换是使以下成立的任意函数。
用代码表示如下:
// nt :: (Functor f, Functor g) => f a -> g a
compose(map(f), nt) === compose(nt, map(f));
示意图和代码都说明了同一件事情,先自然转换然后map或者先map然后再自然转换,可以得到同样的结果。附带说明,这来自自然定理,只不过自然转换(和函子)没有限制函数的类型。
坚持原则的类型转换
程序员都很熟悉类型转换。我们将Strings转换成Boolens,Integers转换成Floats(JS中仅有Numbers)。不同之处在于,这里我们在处理代数容器,并且用一些理论去处理。
先看一些例子
// 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]);
看出来没?将一个函子转换成另一个函子,我们可以在过程中丢失一些信息,只要保证没有在形式转换中丢失将要map的value即可。这就是关键点:根据我们的定义,map必须可以持续下去,甚至是在转换结束之后,还可以继续。
从转换效果的角度观察,ioToTask可以认为是同步到异步的转换,arrayToMaybe是非确定性到可能失败。注意在JS中我们不能从异步转换到同步,所以我们不能写taskToIO - 那将是超自然转换。
Feature Envy
假设我们需要再List上使用其他类型的特性,比如sortBy。自然转换提供一个很好的方式转换成目标类型。
// 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
同构Javascript
当可以完全的来回调用而不丢失任何信息,这就是同构。
如果我们可以提供自然转换作为证据,我们就可以说两种类型是同构的。
// 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是同构的,我们也可以写一个listToArray实现arrayToList显示他们是同构。作为一个反例,arrayToMaybe不是同构,因为它丢失了信息。
// 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'];
// not isomorphic
maybeToArray(arrayToMaybe(x)); // ['elvis costello']
// but is a natural transformation
compose(arrayToMaybe, map(replace('elvis', 'lou')))(x); // Just('lou costello')
// ==
compose(map(replace('elvis', 'lou'), arrayToMaybe))(x); // Just('lou costello')
它们确实是自然转换,因为在任何一方map都会产出同样的结果。
更广泛的定义
这些结构化的函数并不限制通过任何方式进行类型转换。
这有几个不同的例子。
reverse :: [a] -> [a]
join :: (Monad m) => m (m a) -> m a
head :: [a] -> a
of :: a -> f a
自然转换规则也支持这些函数。