第十二章:穿越类型之石
在我们的容器马戏团中,您已见证我们驯服了凶猛的函子,使其执行随心所欲的操作。您通过函数的应用见证了如何精妙掌控多重副作用,犹如同时驾驭数匹烈马。当容器通过融合化为无形时,我们目睹了它们在副作用副场中组合为一的奇观。最新近的,我们突破自然规律,在您眼前完成类型的变形。
接下来的表演将聚焦遍历技巧。我们将见证类型如同高空秋千艺术家般保持值不变地飞跃重组,像游乐园飓风飞椅般重构副作用效果。当容器像柔术演员般缠绕时,这个接口能理清它们的肢体。通过不同的顺序观察不同副作用。让我们披上彩裤吹响哨笛,正式启程。
类型之舞
进入异次元空间:
// readFile :: FileName -> Task Error String
// firstWords :: String -> String
const firstWords = compose(intercalate(' '), take(3), split(' '));
// tldr :: FileName -> Task Error String
const tldr = compose(map(firstWords), readFile);
map(tldr, ['file1', 'file2']);
// [Task('hail the monarchy'), Task('smash the patriarchy')]当我们读取文件得到无用的任务数组时,如何分解每个任务?更佳的方案是将《[Task Error String]》转换为《Task Error [String]》。这样单个异步任务就能承载结果集合,比零散的多个异步值更适合异步场景。
最后一个疑难案例:
// getAttribute :: String -> Node -> Maybe String
// $ :: Selector -> IO Node
// getControlNode :: Selector -> IO (Maybe (IO Node))
const getControlNode = compose(map(map($)), map(getAttribute('aria-controls')), $);这些《IO》渴望结合。虽想让它们亲密共舞,但夹杂其间的《Maybe》犹如毕业舞会监护人阻止接触。最佳解法是将它们的层级调整为《IO (Maybe Node)》。
类型风水学
可遍历接口由两大神器构成:《sequence》与《traverse》。
使用《sequence》重组类型:
sequence(List.of, Maybe.of(['the facts'])); // [Just('the facts')]
sequence(Task.of, new Map({ a: Task.of(1), b: Task.of(2) })); // Task(Map({ a: 1, b: 2 }))
sequence(IO.of, Either.of(IO.of('buckle my shoe'))); // IO(Right('buckle my shoe'))
sequence(Either.of, [Either.of('wing')]); // Right(['wing'])
sequence(Task.of, left('wing')); // Task(Left('wing'))我们的嵌套类型如同魔术师反穿外套般内外翻转。需注意《sequence》参数严格,其形态如下:
// sequence :: (Traversable t, Applicative f) => (a -> f a) -> t (f a) -> f (t a)
const sequence = curry((of, x) => x.sequence(of));第二参数必须是携带应用函子的可遍历容器。它将《t (f a)》转为《f (t a)》。首参数仅是类型构造器(即《of》方法),用于处理像《Left》这类抗拒映射的类型。
以《Either》类型演示实现:
class Right extends Either {
// ...
sequence(of) {
return this.$value.map(Either.of);
}
}若作为应用函子,简单映射构造器即可完成类型跳跃。
此处忽略《of》参数。针对《Left》这类场景需特别处理:
class Left extends Either {
// ...
sequence(of) {
return of(this);
}
}为保持结构一致性,《Left》等容器需构造器支持。强类型系统可自动推断外部类型。
副作用调度
类型层次决定语义差异:《[Maybe a]》是可能值的集合,《Maybe [a]》是集合可能缺失。同理《Either Error (Task Error a)》表征客户端校验,《Task Error (Either Error a)》对应服务端校验。
// fromPredicate :: (a -> Bool) -> a -> Either e a
// partition :: (a -> Bool) -> [a] -> [Either e a]
const partition = f => map(fromPredicate(f));
// validate :: (a -> Bool) -> [a] -> Either e [a]
const validate = f => traverse(Either.of, fromPredicate(f));通过选用《map》或《traverse》获得不同行为:《partition》保留有效数据,《validate》返回首个错误项或全量数据。
通过《List.traverse》解析《validate》实现:
traverse(of, fn) {
return this.$value.reduce(
(f, a) => fn(a).map(b => bs => bs.concat(b)).ap(f),
of(new List([])),
);
}在列表上运行含《(f, a) => fn(a).map(b => bs => bs.concat(b)).ap(f)》缩减函数的《reduce》。
- 《reduce(..., ...)》,
注意签名:《reduce :: [a] -> (f -> a -> f) -> f -> f》,首参来自《$value》的链式调用。
- 《of(new List([]))》,
种子值为《Either e [a]》类型并贯穿最终结果。
- 《fn :: Applicative f => a -> f a》,
本例中《fn》即《fromPredicate(f) :: a -> Either e a》。
fn(a) :: Either e a
- 《.map(b => bs => bs.concat(b))》,
当《Right》时返回带闭包的新《Right》。若《Left》则直接返回。
fn(a).map(b => bs => bs.concat(b)) :: Either e ([a] -> [a])
- .《ap(f)》,
应用闭包至《f》《Either e [a]》类型。根据《f》类型执行添加或保持操作。
fn(a).map(b => bs => bs.concat(b)).ap(f) :: Either e [a]
仅用六行代码通过《of》《map》《ap》实现通用遍历转换,印证了高阶抽象在类型系统支持下构建通用代码的威力。
类型圆舞曲
重构初始案例:
// readFile :: FileName -> Task Error String
// firstWords :: String -> String
const firstWords = compose(intercalate(' '), take(3), split(' '));
// tldr :: FileName -> Task Error String
const tldr = compose(map(firstWords), readFile);
traverse(Task.of, tldr, ['file1', 'file2']);
// Task(['hail the monarchy', 'smash the patriarchy']);用《traverse》驯服异步《Task》群集,同步结果数组。这类似《Promise.all()》但适配任意可遍历类型,避免重复造轮子。
最终案例优化(与闭包无关):
// getAttribute :: String -> Node -> Maybe String
// $ :: Selector -> IO Node
// getControlNode :: Selector -> IO (Maybe Node)
const getControlNode = compose(chain(traverse(IO.of, $)), map(getAttribute('aria-controls')), $);使用《chain(traverse(IO.of, $))》替代《map(map($))》,通过链式调用压平《IO》结构。
无律不立
在敲击退格键前,请理解这些定律保障代码可靠性。良好架构通过合理约束引导最优解。
无律接口只是间接层。数学结构通过特性保护数据并支持接口替换。
让我们探析定律体系。
恒等律
const identity1 = compose(sequence(Identity.of), map(Identity.of));
const identity2 = Identity.of;
// test it out with Right
identity1(Either.of('stuff'));
// Identity(Right('stuff'))
identity2(Either.of('stuff'));
// Identity(Right('stuff'))用《Identity》函子执行《sequence》转换的内外翻转等价原始状态。范畴论中函子范畴以自然变换为态射,《Identity》即恒等态射。参考组合类型的处理方式:
组合律
const comp1 = compose(sequence(Compose.of), map(Compose.of));
const comp2 = (Fof, Gof) => compose(Compose.of, map(sequence(Gof)), sequence(Fof));
// Test it out with some types we have lying around
comp1(Identity(Right([true])));
// Compose(Right([Identity(true)]))
comp2(Either.of, Array)(Identity(Right([true])));
// Compose(Right([Identity(true)]))确保函子组合交换的等价性。可用quickcheck等工具验证。
基于此律可实现遍历融合优化。
自然性
const natLaw1 = (of, nt) => compose(nt, sequence(of));
const natLaw2 = (of, nt) => compose(sequence(of), map(nt));
// test with a random natural transformation and our friendly Identity/Right functors.
// maybeToEither :: Maybe a -> Either () a
const maybeToEither = x => (x.$value ? new Right(x.$value) : new Left());
natLaw1(Maybe.of, maybeToEither)(Identity.of(Maybe.of('barlow one')));
// Right(Identity('barlow one'))
natLaw2(Either.of, maybeToEither)(Identity.of(Maybe.of('barlow one')));
// Right(Identity('barlow one'))先重组再应用自然变换等效先映射再重组。
衍生的结论:
traverse(A.of, A.of) === A.of;也为性能优化奠定基础。
总结
可遍历是强大的类型编排工具,能优雅调整结构消弭融合障碍。下章将探索函数式编程的终极利器:幺半群统合万物。
练习
给定以下元素:
// httpGet :: Route -> Task Error JSON
// routes :: Map Route Route
const routes = new Map({ '/': '/', '/about': '/about' });开始练习!
使用可遍历接口将《getJsons》类型改为 Map Route Route → Task Error (Map Route JSON) // getJsons :: Map Route Route -> Map Route (Task Error JSON) const getJsons = map(httpGet);
定义验证函数:
// validate :: Player -> Either String Player
const validate = player => (player.name ? Either.of(player) : left('must have name'));开始练习!
结合可遍历接口与《validate》,更新《startGame》 确保仅在所有玩家有效时启动 // startGame :: [Player] -> [Either Error String] const startGame = compose(map(map(always('game started!'))), map(validate));
文件系统工具:
// readfile :: String -> String -> Task Error String
// readdir :: String -> Task Error [String]开始练习!
使用可遍历重组并压平《Task》与《Maybe》嵌套 // readFirst :: String -> Task Error (Maybe (Task Error String)) const readFirst = compose(map(map(readfile('utf-8'))), map(safeHead), readdir);