Skip to content

第十二章:穿越类型之石

在我们的容器马戏团中,您已见证我们驯服了凶猛的函子,使其执行随心所欲的操作。您通过函数的应用见证了如何精妙掌控多重副作用,犹如同时驾驭数匹烈马。当容器通过融合化为无形时,我们目睹了它们在副作用副场中组合为一的奇观。最新近的,我们突破自然规律,在您眼前完成类型的变形

接下来的表演将聚焦遍历技巧。我们将见证类型如同高空秋千艺术家般保持值不变地飞跃重组,像游乐园飓风飞椅般重构副作用效果。当容器像柔术演员般缠绕时,这个接口能理清它们的肢体。通过不同的顺序观察不同副作用。让我们披上彩裤吹响哨笛,正式启程。

类型之舞

进入异次元空间:

js
// 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]》。这样单个异步任务就能承载结果集合,比零散的多个异步值更适合异步场景。

最后一个疑难案例:

js
// 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》重组类型:

js
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》参数严格,其形态如下:

js
// 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》类型演示实现:

js
class Right extends Either {
  // ...
  sequence(of) {
    return this.$value.map(Either.of);
  }
}

若作为应用函子,简单映射构造器即可完成类型跳跃。

此处忽略《of》参数。针对《Left》这类场景需特别处理:

js
class Left extends Either {
  // ...
  sequence(of) {
    return of(this);
  }
}

为保持结构一致性,《Left》等容器需构造器支持。强类型系统可自动推断外部类型。

副作用调度

类型层次决定语义差异:《[Maybe a]》是可能值的集合,《Maybe [a]》是集合可能缺失。同理《Either Error (Task Error a)》表征客户端校验,《Task Error (Either Error a)》对应服务端校验。

js
// 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》实现:

js
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》。

  1. 《reduce(..., ...)》,

注意签名:《reduce :: [a] -> (f -> a -> f) -> f -> f》,首参来自《$value》的链式调用。

  1. 《of(new List([]))》,

种子值为《Either e [a]》类型并贯穿最终结果。

  1. 《fn :: Applicative f => a -> f a》,

本例中《fn》即《fromPredicate(f) :: a -> Either e a》。

fn(a) :: Either e a

  1. 《.map(b => bs => bs.concat(b))》,

当《Right》时返回带闭包的新《Right》。若《Left》则直接返回。

fn(a).map(b => bs => bs.concat(b)) :: Either e ([a] -> [a])

  1. .《ap(f)》,

应用闭包至《f》《Either e [a]》类型。根据《f》类型执行添加或保持操作。

fn(a).map(b => bs => bs.concat(b)).ap(f) :: Either e [a]

仅用六行代码通过《of》《map》《ap》实现通用遍历转换,印证了高阶抽象在类型系统支持下构建通用代码的威力。

类型圆舞曲

重构初始案例:

js
// 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()》但适配任意可遍历类型,避免重复造轮子。

最终案例优化(与闭包无关):

js
// 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》结构。

无律不立

在敲击退格键前,请理解这些定律保障代码可靠性。良好架构通过合理约束引导最优解。

无律接口只是间接层。数学结构通过特性保护数据并支持接口替换。

让我们探析定律体系。

恒等律

js
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》即恒等态射。参考组合类型的处理方式:

组合律

js
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等工具验证。

基于此律可实现遍历融合优化

自然性

js
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'))

先重组再应用自然变换等效先映射再重组。

衍生的结论:

js
traverse(A.of, A.of) === A.of;

也为性能优化奠定基础。

总结

可遍历是强大的类型编排工具,能优雅调整结构消弭融合障碍。下章将探索函数式编程的终极利器:幺半群统合万物

练习

给定以下元素:

js
// 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);


定义验证函数:

js
// 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));


文件系统工具:

js
// 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);