R·ex / Zeng


音游狗、安全狗、攻城狮、业余设计师、段子手、苦学日语的少年。

从连接器开始的一系列旅程

注意:本文发布于 2267 天前,文章中的一些内容可能已经过时。

偶然认识的连接器

最近有小伙伴在群里问一些关于 Underscore.jsfnull 函数的问题,我并没用过这个函数,于是就去翻文档,于是就发现了一个之前没见过的词:combinator。其实光看英文“连接器”大概能知道是带有“函数组合”的意思,并且文档里写到这个函数是个高阶函数,于是我就大概知道是怎么回事了。在我看来,combinator 其实跟之前见过的 decorator装饰器)差不多。

对装饰器的第一印象

第一次见到装饰器,是在 Coding.net 的后端代码中,Spring 自带的 @RequestMapping 等一系列注解Annotations)。

@RequestMapping("/owners/{ownerId}")
public class RelativePathUriTemplateController {
    @RequestMapping(value = "/pets/{petId}", method = RequestMethod.GET)
    public void findPet(
        @PathVariable String ownerId,
        @PathVariable String petId,
        Model model
    ) {
        // implementation omitted
  }
}

非常好理解,无非是把不同的请求映射到不同的函数,将里面的参数提取出来之类的操作。后来我在写 PHP 的时候,也有了这个需求,但并没有在 PHP 中找到长成这模样的东西,Laravel 之前也只是使用了 User#postApply 来表示 POST /user/apply,其它类似的框架和库都是用的其它方法(例如在专门的地方手动注册路由与函数的绑定),感觉都不如 @RequestMapping 优雅。

装饰器模式

在查找资料的时候,发现在设计模式中有一种专门的“装饰器模式”,网上的文章虽然可以读懂,但是术语和例子对初学者太不友好了。我个人是这样理解的:

在编码时,会遇到需要“通过父类派生出一堆子类”的场景,例如之前写游戏的时候,SceneBase 是所有场景的父类,可以派生出 SceneTitleSceneMusicSelect 等子类。但不是所有情况下这么写都合适,例如父类是 CarMaker,做所有的车,现在需要一个可以做宝马车的 BMWCarMaker、做丰田车的 ToyotaCarMaker,以及红车的 RedCarMaker、蓝车的 BlueCarMaker,有时候我还需要红色的宝马车 RedBMWCarMaker……这样写下去是不划算的。装饰器就可以看成是一个个装饰,这样我需要制作红色宝马车,只需要这么写:

public class CarMaker {}
// 此处省略许多行……
CarMaker redBMWMaker = Red(BMW(new CarMaker()));

按照这样理解,那其实装饰器一开始的出现并不是为了解决什么 Request Mapping 的问题,而是为了让程序更易于扩展。

Wrapper 的概念

这又不得不让我联想起来写 React 的经历。为了让一个路由只有登录后可见,未登录则跳转到登录页面,我从同事那儿学到了一个方法,就是新写一个函数叫 loginWrapper,只需要将该函数包在组件外面,即可实现这样的效果。后来我在 React Router 的文档中也看到了这个写法:Redirects (Auth),页面内查找“Private Route”即可。

稍微想一想,就会发现,Wrapper 不光可以用于判断是否登录,还可以判断是否有权限、为函数添加新功能等等,甚至可以多个 Wrapper 一层套一层,这样组合是相当自由的:

// 当然一般来说最好不要这么写,容易被打……
return Red(BMW(Large(Smart(Funny(XXX(CarMaker))))));

复合函数与高阶函数

上面这段代码的写法,看起来超像数学中的 复合函数

[math](h \circ g \circ f)(x) = h(g(f(x)))[/math]

中间空心的小圆圈(Ring Operator)就是将函数连接起来的东西。真用代码来实现的话,JavaScript 代码也不是特别麻烦:

const ring = (g, f) => x => g(f(x));
const f = x => x + 1;
const g = x => x * x;
const h = x => x - 2;
console.log(ring(h, ring(g, f))(10)); // 119

当然还有一些更好的写法,可以直接写成类似于 ring(f, g, h)(x) 这样更适合程序员阅读的样子,不过不管怎么写,ring 函数本身就是一个高阶函数——接受函数作为参数,返回一个新函数。

管道

前几天刷推,发现有人发了这样一个 提案,打算为 JavaScript 添加“管道”功能。当然,“管道”并不是什么新概念,最常见的地方当然是在 Linux 等 Unix-like 的系统中了,例如下面这条命令可以查看当前有多少名称带有 php-fpm 的进程在运行:

ps aux | grep php-fpm | wc -l

在开发中,我曾经也用过类似的东西,是 Vue 1.0 中的过滤器:

Vue.filter('wrap', function (value, begin, end) {
    return begin + value + end;
});
<!-- 'hello' => 'before hello after' -->
<span v-text="message | wrap 'before' 'after'"></span>

当然,很多函数式编程的语言都是自带管道功能的(其实 JavaScript 一开始的目标是成为一门函数式编程语言的,但是加了太多非函数式语言的功能)。不过即使这个功能加入到了 JavaScript 中,让代码写起来更加精简的同时,看起来可能会更奇怪,例如提案中的 Demo:

import Lazy from 'lazy.js';

getAllPlayers()
    .filter(p => p.score > 100)
    .sort()
|> _ => Lazy(_)
    .map(p => p.name)
    .take(5)
|> _ => renderLeaderboard('#my-div', _);

反正在这个缩进下,我第一眼并没看懂在干啥……所以人们对于这个提案褒贬不一,是很正常的嘛。

ES7 中的装饰器

思维发散了那么久了,突然想起来 ECMAScript 2016 中也有一个 装饰器这里 是一个个人认为写的比较好的中文教程,推荐大家看一看。

文章以外的故事

其实除了本文的内容以外,MonadPromise 跟连接器也有几分相似,但并不是同一个东西。鉴于我水平有限,Monad 还暂时理解不能,就没法写出来了;当然也鉴于篇幅所限,Promise 就不写了,网上关于它的教程已经太多了。

Disqus 加载中……如未能加载,请将 disqus.com 和 disquscdn.com 加入白名单。

这是我们共同度过的

第 3071 天