wangzengdi's Blog

Functional Programming

0%

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

Generate & Deploy together

1
$ hexo d -g

More info: Deployment

Hexo 自动化部署

自动化部署工作流:

本地开发使用 hexo-source 分支,开发完成后,推导 github 仓库,会触发自动化部署,自动部署在 master 分支上生成最新的文档并部署。

原理简介

在 Vue2 中,我们编写的 单文件组件(SCF,也即 Single-File Components),其实是 vue 自创的前端领域语言(类比各互联网厂的小程序开发语言,也是各自造(抄)的领域语言),形式如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<template>
<div class="title" @click="onClick">{{ title }}</div>
</template>

<script>
export default {
props: {
// 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
propA: Number,
// 多个可能的类型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
},

data() {
return {
title: 'hello, vue',
}
},

methods: {
onClick() {
console.log('click');
}
}
}
</script>

<style>
.title {
color: red;
}
</style>

其中 <script> ... </script> 标签内包含了一个使用 JavaScript 编写的、 Vue 组件的配置对象,配置对象中包含 props、data、methods 等内容。

从上面的示例可以看出,原始的 Vue 配置对象,只能够对 props 进行有限的类型检查,不能对 data、methods 等其他配置项做类型声明和类型检查。并且在对 props 进行类型声明时,只能使用下列原生构造函数:String Number Boolean Array Object Date Function Symbol 来做类型校验,不能进行更具体、深入的类型声明,可以说是非常鸡肋。

对一个对象进行 TypeScript 类型声明是比较困难的,而对 class 进行 TypeScript 类型声明是相对简单、自然的:我们可以很方便地对 class 的属性和方法添加类型,还可以通过 extends 继承父类的类型。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Animal {
name: string;

constructor(theName: string) {
this.name = theName;
}

move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}

class Horse extends Animal {
constructor(name: string) {
super(name);
}

move(distanceInMeters = 45) {
console.log("Galloping...");
super.move(distanceInMeters);
}
}

那么我们是否可以使用 class 来写 Vue 组件呢?这就是 Decorators (装饰器)

本质上,装饰器就是一个函数。函数能做的事情,装饰器都能做。函数可以做任何类型的数据转换,装饰器也是一样的。

有了装饰器,我们就可以使用 class 和 TypeScript 来开发 Vue 组件;然后在运行时,使用装饰器将我们定义的 class 转换为 Vue 引擎能够识别的原生的 Vue 配置对象,如下所示。

开发时:

1
2
3
4
5
6
7
8
9
10
import { Vue, Component, Prop } from 'vue-property-decorator'

@Component
export default class YourComponent extends Vue {
@Prop({ default: 'default value' }) readonly title!: string

message: string = 'hello, world';

onClick(): void {}
}

运行时,@Component 将自定义的组件 YourComponent 转换为 Vue 配置对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default {
props: {
title: {
default: 'default value',
},
},

data() {
message: 'hello, world',
},

methods: {
onClick() {}
}
}

Vue2 使用 TypeScript 开发,原理就是这么简单。下面来详细讲一下 Vue2 使用 TypeScript 开发原理。

依赖包介绍

Vue2 使用 TypeScript 开发,依赖三个库:

  • vue-property-decorator: 提供所有的装饰器;开发时会直接用到的库;依赖 vue-class-component 和 vue;
  • vue-class-component: 提供 @Component 装饰器 和 基本的装饰器工厂函数 createDecorator;开发时不会直接用到;依赖 vue;vue-property-decorator 中暴露的 @Component,其实是 vue-class-component 提供的;vue-property-decorator 中其他的装饰器,都是由 createDecorator 创建的;
  • vue: Vue 基础库,提供 Vue 的类型声明,vue-property-decorator 中暴露的 Vue,其实就是 vue 中的 Vue 类;

我们来具体看一下这三个库对外暴露的接口:

vue-property-decorator:

1
2
3
4
5
6
7
8
9
10
import Vue from 'vue'
import Component, { mixins } from 'vue-class-component'

export { Component, Vue, mixins as Mixins }

export { Emit } from './decorators/Emit'
export { Prop } from './decorators/Prop'
export { Ref } from './decorators/Ref'
export { Watch } from './decorators/Watch'
// ...

vue-class-component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import Vue, { ComponentOptions } from 'vue'
import { VueClass } from './declarations'
import { componentFactory, $internalHooks } from './component'

export { createDecorator, VueDecorator, mixins } from './util'

function Component <V extends Vue>(options: ComponentOptions<V> & ThisType<V>): <VC extends VueClass<V>>(target: VC) => VC
function Component <VC extends VueClass<Vue>>(target: VC): VC
function Component (options: ComponentOptions<Vue> | VueClass<Vue>): any {
if (typeof options === 'function') {
return componentFactory(options)
}
return function (Component: VueClass<Vue>) {
return componentFactory(Component, options)
}
}

Component.registerHooks = function registerHooks (keys: string[]): void {
$internalHooks.push(...keys)
}

export default Component

vue

1
2
3
4
5
6
7
8
9
10
11
import { Vue } from "./vue";
import "./umd";

export default Vue;

export {
CreateElement,
VueConstructor
} from "./vue";

// ...

通过展示三个库对外暴露接口的 index.ts 文件,我们就可以很清晰看到它们暴露的内容,以及依赖关系。

我们再来看一下如何使用 装饰器、class 和 TypeScript 来写 Vue 组件:

1
2
3
4
5
6
7
8
9
10
import { Vue, Component, Prop } from 'vue-property-decorator'

@Component
export default class YourComponent extends Vue {
@Prop({ default: 'default value' }) readonly title!: string

message: string = 'hello, world';

onClick(): void {}
}

YourComponent extends Vue 就可以继承 Vue 上面所有的属性和方法,方法包括实例方法和静态方法,YourComponent 可以直接通过 this 调用 Vue 上的实例方法,在开发时,编辑器也会自动提示。我们来看一下 Vue 的类型声明:

下面是 Vue 实例上的属性和方法的声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
export interface Vue {
readonly $el: Element;
readonly $options: ComponentOptions<Vue>;
readonly $parent: Vue;
readonly $root: Vue;
readonly $children: Vue[];
readonly $refs: { [key: string]: Vue | Element | (Vue | Element)[] | undefined };
readonly $slots: { [key: string]: VNode[] | undefined };
readonly $scopedSlots: { [key: string]: NormalizedScopedSlot | undefined };
readonly $isServer: boolean;
readonly $data: Record<string, any>;
readonly $props: Record<string, any>;
readonly $ssrContext: any;
readonly $vnode: VNode;
readonly $attrs: Record<string, string>;
readonly $listeners: Record<string, Function | Function[]>;

$mount(elementOrSelector?: Element | string, hydrating?: boolean): this;
$forceUpdate(): void;
$destroy(): void;
$set: typeof Vue.set;
$delete: typeof Vue.delete;
$watch(
expOrFn: string,
callback: (this: this, n: any, o: any) => void,
options?: WatchOptions
): (() => void);
$watch<T>(
expOrFn: (this: this) => T,
callback: (this: this, n: T, o: T) => void,
options?: WatchOptions
): (() => void);
$on(event: string | string[], callback: Function): this;
$once(event: string | string[], callback: Function): this;
$off(event?: string | string[], callback?: Function): this;
$emit(event: string, ...args: any[]): this;
$nextTick(callback: (this: this) => void): void;
$nextTick(): Promise<void>;
$createElement: CreateElement;
}

下面是 Vue 的静态属性和静态方法,也即 Vue 构造函数上的属性和方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
export interface VueConstructor<V extends Vue = Vue> {
new <Data = object, Methods = object, Computed = object, PropNames extends string = never>(options?: ThisTypedComponentOptionsWithArrayProps<V, Data, Methods, Computed, PropNames>): CombinedVueInstance<V, Data, Methods, Computed, Record<PropNames, any>>;
// ideally, the return type should just contain Props, not Record<keyof Props, any>. But TS requires to have Base constructors with the same return type.
new <Data = object, Methods = object, Computed = object, Props = object>(options?: ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>): CombinedVueInstance<V, Data, Methods, Computed, Record<keyof Props, any>>;
new (options?: ComponentOptions<V>): CombinedVueInstance<V, object, object, object, Record<keyof object, any>>;

extend<Data, Methods, Computed, PropNames extends string = never>(options?: ThisTypedComponentOptionsWithArrayProps<V, Data, Methods, Computed, PropNames>): ExtendedVue<V, Data, Methods, Computed, Record<PropNames, any>>;
extend<Data, Methods, Computed, Props>(options?: ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>): ExtendedVue<V, Data, Methods, Computed, Props>;
extend<PropNames extends string = never>(definition: FunctionalComponentOptions<Record<PropNames, any>, PropNames[]>): ExtendedVue<V, {}, {}, {}, Record<PropNames, any>>;
extend<Props>(definition: FunctionalComponentOptions<Props, RecordPropsDefinition<Props>>): ExtendedVue<V, {}, {}, {}, Props>;
extend(options?: ComponentOptions<V>): ExtendedVue<V, {}, {}, {}, {}>;

nextTick<T>(callback: (this: T) => void, context?: T): void;
nextTick(): Promise<void>
set<T>(object: object, key: string | number, value: T): T;
set<T>(array: T[], key: number, value: T): T;
delete(object: object, key: string | number): void;
delete<T>(array: T[], key: number): void;

directive(
id: string,
definition?: DirectiveOptions | DirectiveFunction
): DirectiveOptions;
filter(id: string, definition?: Function): Function;

component(id: string): VueConstructor;
component<VC extends VueConstructor>(id: string, constructor: VC): VC;
component<Data, Methods, Computed, Props>(id: string, definition: AsyncComponent<Data, Methods, Computed, Props>): ExtendedVue<V, Data, Methods, Computed, Props>;
component<Data, Methods, Computed, PropNames extends string = never>(id: string, definition?: ThisTypedComponentOptionsWithArrayProps<V, Data, Methods, Computed, PropNames>): ExtendedVue<V, Data, Methods, Computed, Record<PropNames, any>>;
component<Data, Methods, Computed, Props>(id: string, definition?: ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>): ExtendedVue<V, Data, Methods, Computed, Props>;
component<PropNames extends string>(id: string, definition: FunctionalComponentOptions<Record<PropNames, any>, PropNames[]>): ExtendedVue<V, {}, {}, {}, Record<PropNames, any>>;
component<Props>(id: string, definition: FunctionalComponentOptions<Props, RecordPropsDefinition<Props>>): ExtendedVue<V, {}, {}, {}, Props>;
component(id: string, definition?: ComponentOptions<V>): ExtendedVue<V, {}, {}, {}, {}>;

use<T>(plugin: PluginObject<T> | PluginFunction<T>, options?: T): VueConstructor<V>;
use(plugin: PluginObject<any> | PluginFunction<any>, ...options: any[]): VueConstructor<V>;
mixin(mixin: VueConstructor | ComponentOptions<Vue>): VueConstructor<V>;
compile(template: string): {
render(createElement: typeof Vue.prototype.$createElement): VNode;
staticRenderFns: (() => VNode)[];
};

observable<T>(obj: T): T;

util: {
warn(msg: string, vm?: InstanceType<VueConstructor>): void;
};

config: VueConfiguration;
version: string;
}

装饰器将 vue class 转换为配置对象的过程

下面我们讲一下 vue 的装饰器是如何将 vue class 形式的组件,转换为 vue 引擎能识别的传统的配置对象的。

装饰器定义

装饰器的定义如下:

装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

简单示例如下:

1
2
3
4
5
6
7
8
9
function red(target) {
target.color = 'red';
}

@red
class MyComponent {
}

MyComponent.color // 'red'

我们定义了一个 red 装饰器,作用是给被装饰类添加一个值为 'red' 的 color 属性。

我们看到,装饰器本质上就是一个函数,可以对数据做一定的转换(修饰)。

我们还可以利用装饰器工厂,来提高装饰器的复用性,例如我们想要一个可以给组件添加任意颜色的装饰器,则可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function addColor(color) {
return (target) {
target.color;
}
}

@addColor('red')
class MyComponent1 {
}
MyComponent1.color // 'red'

@addColor('blue')
class MyComponent2 {
}
MyComponent2.color // 'blue'

装饰器工厂,说白了,就是一个生成装饰器的函数

装饰器的概念,就简单介绍到这里。

装饰器的执行顺序

在TypeScript中,装饰器的执行顺序为:

  1. 类成员装饰器,一般按书写顺序从前往后执行;
  2. 类装饰器;

关于装饰器调用顺序,网上几乎所有中文资料都是错的!

因此对于下面使用装饰器的 Vue class 组件:

1
2
3
4
5
6
7
8
9
10
11
import { Vue, Component, Prop, Ref } from 'vue-property-decorator'

@Component
export default class YourComponent extends Vue {
@Ref readonly elementRef!: HTMLElement;
@Prop({ default: 'default value' }) readonly title!: string

message: string = 'hello, world';

onClick(): void {}
}

会先执行类属性上的装饰器,如 @Ref、@Prop,这两个装饰器,会将被修饰的属性收集到对应类型的数组中(refs 数组,props 数组),然后是方法的装饰器,最后会执行 @Component ,对刚才装饰器收集的属性、方法,以及为修饰的属性、方法做分类处理,最终转成 Vue 引擎能识别的、原始的 vue 组件配置对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export default {
props: {
title: {
default: 'default value',
},
},

data() {
message: 'hello, world',
},

methods: {
onClick() {}
},

computed: {
elementRef: {
cache: false,
get(this: Vue) {
return this.$refs['elementRef']
},
}
}

使用 vue-property-decorator 装饰器,将 vue class 转换为 vue 配置对象的过程,就解释完了。

参考

Lerna 本身是一个非常好的 monorepo (单仓库-多项目)管理工具,当经常会因为时间久了不用,而导致生疏,因此总结一些常用的 Lerna 操作,以备后续用到时参考。

本文整理了一些 nvm 不常用、但非常有用的命令。主要分为以下几大类:

  • reinstall-packages Reinstall global npm packages contained in to current version

由于使用 npm install -g 安装全局 npm 包,是安装到当前使用的版本的 node 下面,在安装新版本的 node 或者 切换 node 版本后,之前安装的全局 npm 包往往就不生效了,比如,切换 node 版本后,yarn 命令失效。

这时可以用 nvm reinstall-packages <version> 来将之前 node 下面所有的全局 npm 包都重新安装到当前使用的 node 版本中。

若想查看当前 node 版本中都安装了哪些全局 npm 包,可以使用 npm ls -g 来查看。

VSCode 入门简单、操作方便、扩展灵活的特点,让 VSCode 在编程届(尤其是前端)越来越流行;

而相比之下,Emacs 学习和维护成本都很高,导致用户群较小,开发和维护面临后继无人的局面。Emacs 相对于 VSCode 的优势已经不明显。Emacs 的优势:快捷键、magit 等,在 VSCode 上也都有相应的实现,因此从 Emacs 迁移到 VSCode 的成本也没有想象的那么大。

而缺点却比较多:

  • 出问题时,提示不友好,不知道错在哪里;
  • 各种小问题不断,每个小问题可能都要查半天,时间成本比较高;
  • Emacs 配置比较复杂,需要学习 elisp 语言;

鉴于上述几点,本人也要从使用了多年的 Emacs 迁移到 VSCode 了。在此想到了几句话:唯一不变的是变化,选择比努力更重要。

本文将是本人学习 VSCode 总结的备忘录,后面遇到之前用过、总结过、但不熟悉的地方,可以快速查找到解决方法。主要是快捷键

macOS 快捷键

名词解释

  • Explorer: 左侧边栏;
  • Editor: 编辑区;
  • Editor Group: 编辑组,一个编辑组可以包含多个打开的文件。可以使用 CMD + \ 来分割编辑组;

各区域切换

快捷键 解释
CMD + 0 左侧边栏
CMD + 1, CMD + 2, CMD + 3… 聚焦 1st,2nd,3rd editor group
CMD + B 切换左侧边栏是否可见

设置

本文整理了一些 npm 不常用、但非常有用的命令。主要分为以下几大类:

  • 查看 npm 源(registry) 上的 npm 包信息

    • npm view
  • 开发 npm 包时,用到的命令

    • npm publish
    • npm pack
    • npm link & npm unlink
    • npm set-script
  • 查看已安装的 npm 包的本地信息

    • npm ls
    • npm edit
    • npm explore
    • npm explain | npm why
  • 浏览器查看 npm 包信息

    • npm docs
    • npm repo
  • 使用 npm 包开发其他项目时,用到的命令

    • npm outdated
    • npm update
  • 其他 npm 命令

    • npm config
    • npm bin
    • npm completion

查看 npm 源(registry) 上的 npm 包信息

  • npm view: 最为灵活、最为强大的 查看 npm 包信息的命令;

由于 npm view 太灵活,所以需要单独一小节进行讲解。

npm view

查看 npm 源上包的信息

1
2
3
npm view [<@scope>/]<name>[@<version>] [<field>[.<subfield>]...]

aliases: info, show, v

查看包的基本信息

1
npm view <pkg>

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
› npm view ramda

ramda@0.27.1 | MIT | deps: none | versions: 52
A practical functional library for JavaScript programmers.
https://ramdajs.com/

dist
.tarball: http://r.npm.sankuai.com/ramda/download/ramda-0.27.1.tgz
.shasum: 66fc2df3ef873874ffc2da6aa8984658abacf5c9

maintainers:
- aromano <aromano@preemptsecurity.com>
- bradcomp <notpmoc84@hotmail.com>
- ...

dist-tags:
0.2.0: 0.2.0 es-rc: 0.24.1-es.rc3 latest: 0.27.1

published 9 months ago by davidchambers <dc@davidchambers.me>

更加灵活的查看包的详细信息

1
npm view <pkg>@<version> <field>[.<subfield>]...

查看包的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
› npm view http-server dependencies
{
'basic-auth': '^1.0.3',
colors: '^1.4.0',
corser: '^2.0.1',
ecstatic: '^3.3.2',
'http-proxy': '^1.18.0',
minimist: '^1.2.5',
opener: '^1.5.1',
portfinder: '^1.0.25',
'secure-compare': '3.0.1',
union: '~0.5.0'
}

查看包的最新版本

1
2
› npm view ramda version
0.27.1

查看包发布的所有版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
› npm view ramda versions
[
'0.1.0', '0.1.1', '0.1.2',
'0.1.4', '0.1.5', '0.2.0',
'0.2.1', '0.2.2', '0.2.3',
'0.2.4', '0.3.0', '0.4.0',
'0.4.1', '0.4.2', '0.4.3',
'0.5.0', '0.6.0', '0.7.0',
'0.7.1', '0.7.2', '0.8.0',
'0.9.0', '0.9.1', '0.10.0',
'0.11.0', '0.12.0', '0.13.0',
'0.14.0', '0.15.0', '0.15.1',
'0.16.0', '0.17.0', '0.17.1',
'0.18.0', '0.19.0', '0.19.1',
'0.20.0', '0.20.1', '0.21.0',
'0.22.0', '0.22.1', '0.23.0',
'0.24.0', '0.24.1-es.rc1', '0.24.1-es.rc2',
'0.24.1-es.rc3', '0.24.1', '0.25.0',
'0.26.0', '0.26.1', '0.27.0',
'0.27.1'
]

查看指定版本范围内的包的发布情况

查看自 0.25.0 以来,所有版本 ramda 包的信息

1
npm view ramda@'>=0.25.0'

查看 npm 包官网地址

1
npm view ramda homepage

查看 npm 包仓库地址

1
npm view ramda repository.url

通过 shell 脚本,组合出灵活的查询命令

可以通过一些 Shell 脚本轻松查看有关依赖项的信息。例如,要查看有关ronn所依赖的opts版本的所有数据,如下所示:

1
npm view opts@$(npm view ronn dependencies.opts)

更多 npm view 的高级用法,可以查看 npm view 官方文档

开发 npm 包涉及的命令

  • npm publish: 将包发布到 npm 源(registry) 上去;

  • npm pack: 打包;

  • npm link: 将当前 package 文件夹软链到全局 npm 环境中;

  • npm unlink: 断开全局软链时,其实就是 npm uninstall 的别名;

  • npm set-script: 在 package.json 的 "scripts" 字段中设置任务;

npm publish – 发包

将包发布到 npm 源(registry) 上去。

模拟发包

1
npm publish --dry-run

npm publish --dry-run 只是模拟发包,并不会真正发包。会将发包过程中的所有信息都打印出来,用作发包前进行信息确认。也可以使用 npm pack --dry-run

发布的包中包含的文件

要查看软件包中将包含的内容,请运行 npx npm-packlist。默认情况下,所有文件都包括在内,但以下情况除外:

  • 始终包含与软件包安装和分发相关的某些文件。例如,package.jsonREADME.mdLICENSE 等。

  • 如果 package.json 中有一个 "files" 列表字段,则仅包含 "files" 指定的文件。(如果指定了目录,则将遵循相同的忽略规则,以递归方式遍历目录并包含目录的内容。)

  • 如果存在 .gitignore.npmignore 文件,则其中的被忽略文件以及所有子目录都将从软件包中排除。如果两个文件都存在,则将忽略 .gitignore,而仅使用 .npmignore

    .npmignore 文件遵循与 .gitignore 文件相同的模式规则

  • 需要特别注意的是:如果文件匹配某些模式,则除非明确将其添加到 package.json 的 “files” 列表中,否则将永远不会将其包括在内,或者在 .npmignore.gitignore 文件中使用 ! ,来强制包含需要发布的文件

    例如,npm 发包,默认是会忽略 .npmrc 文件的,如果我确实需要将 .npmrc 包含进发布的包中,则需要使用在 .npmignore.gitignore 中写入规则 !.npmrc!.npmrc 表示强制包含 .npmrc

  • 符号链接永远不会包含在 npm 软件包中。

有关已发布的软件包中包含的内容以及如何构建该软件包的详细信息,请查看 开发者须知

npm pack – 打包

1
npm pack [[<@scope>/]<pkg>...] [--dry-run]

一般会用到 npm pack --dry-run ,看一下即将发布的包的打包情况。加了 --dry-run 会后,命令不会真正的执行,只是把之间结果打印出来,以供调试使用。

本地调试利器

将当前 package 文件夹软链到全局 npm 环境中。开发某个 npm 包时,本地调试非常好用。避免了在开发过程中不断重复发包的困扰。

npm link 分两步:

  • 软链当前的 package;

  • 使用软链过的 package;

如下所示:

1
2
3
4
cd ~/projects/node-redis    # go into the package directory
npm link # creates global link
cd ~/projects/node-bloggy # go into some other package directory.
npm link redis # link-install the package

当本地调试完成,想要断开全局软链时,运行下面命令:

1
npm unlink <pkg> -g

npm unlink 会将 从全局的 npm 环境中移除(断开软链)。

注:npm unlink 其实是 npm uninstall 的别名,所以运行 npm unlink <pkg> -g 等价于 npm uninstall <pkg> -g

npm set-script

在 package.json 的 "scripts" 字段中设置任务。

1
npm set-script [<script>] [<command>]

如果开发的 npm 包在安装时,需要动态修改项目中 package.json 文件的 "scripts" ,则可以使用该命令进行设置。

示例:

1
npm set-script start "http-server ."
1
2
3
4
5
6
7
{
"name": "my-project",
"scripts": {
"start": "http-server .",
"test": "some existing value"
}
}

"scripts" 中添加 "start" 脚本。

查看已安装的 npm 包的本地信息

  • npm ls: 列出安装的 packages,或者指定 package 的依赖树;

  • npm edit: 使用默认的编辑器打开当前项目中指定的 npm 包;

  • npm explore: 进入指定的被安装的 npm 包的目录;

  • npm explain | npm why: 解释 packages 被安装的原因。主要是把指定包的依赖链条打印出来;

npm ls

列出安装的 packages。

1
npm ls [<pkg> -g]

打印出当前项目已安装的首层 packages

1
npm ls

所有的依赖以依赖树的形式打印出来

1
npm ls -all

当使用 --all 时,会将所有的依赖以依赖树的形式打印出来。

打印全局安装的 packages

1
npm ls -g

打印指定 packages 的安装情况

1
npm ls name@version-range

npm ls name@version-range 可以以结构树的形式打印出指定 package 在项目中的安装情况。

也可以使用 npm explain 查看指定的 package 为什么会被安装,作用相当于 npm ls ,只不过展示的顺序是反向的。

1
npm explain name

npm edit

使用默认的编辑器,直接打开当前项目已经安装的 package 的文件夹。可以省去手动到 node_modules 中查找 package 的麻烦。

1
npm edit <pkg>

例如:

1
npm edit prettier

会打开当前项目下的 ./node_modules/prettier 目录。

指定默认的编辑器,修改 $EDITOR 变量

1
echo export EDITOR="emacsclient -t" >> ~/.zshrc

npm explore

在命令行中,进入指定的被安装的 package 文件夹中。

1
npm explore <pkg> [ -- <command>]

示例:

  1. 进入当前项目中某 package 的目录
1
2
3
4
5
// 进入当前项目中安装的 prettier 目录
npm explore prettier

pwd
// project/node_modules/prettier
  1. 进入全局安装的某 package 目录
1
2
3
4
5
// 进入全局安装的 prettier 目录
npm explore prettier -g

pwd
// /Users/wangzengdi/.nvm/versions/node/v14.15.4/lib/node_modules/prettier

npm explain | npm why

解释已安装的 packages,被安装的原因。

会将当前指定包的依赖关系打印出来(哪些包依赖的指定的包),可以用于解释为什么一个依赖为什么会被安装多次。

1
2
3
npm explain <folder | specifier>

alias: why

例如:

1
2
3
4
5
6
7
8
9
10
11
12
› npm explain eslint

eslint@7.25.0 dev
node_modules/eslint
dev eslint@"^7.25.0" from the root project
peer eslint@">= 4.12.1" from babel-eslint@10.1.0
node_modules/babel-eslint
dev babel-eslint@"^10.1.0" from the root project
peer eslint@">=7.0.0" from eslint-config-prettier@8.3.0
node_modules/eslint-config-prettier
dev eslint-config-prettier@"^8.3.0" from the root project
...

表示当前项目在 devDependencies 中安装了 7.25.0 版本的 eslint,安装的位置为 node_modules/eslint 。后面的部分表明是哪些包的依赖,导致了 eslint 的安装。

浏览器查看 npm 包信息

  • npm docs | npm home: 在浏览器中打开 npm 包官网;

  • npm repo: 在浏览器中打开 npm 包仓库地址;

npm docs

在浏览器中打开指定 npm 包的官方网站。

1
2
3
npm docs [pakname]

aliases: home

例如

1
2
3
4
5
# 会打开 react 官网: https://reactjs.org/
npm docs react

# 会打开 ramda 官网: https://ramdajs.com/
npm docs ramda

npm 的官方网站一般写在 package.json 中的 “homepage” 字段中。

npm repo

在浏览器中打开指定 npm 包的仓库(一般为 github 仓库)地址。

1
npm repo [<pkgname> [<pkgname> ...]]

例如

1
2
# 会打开 ramda github 仓库: https://github.com/ramda/ramda
npm repo ramda

使用 npm 包开发其他项目时,用到的命令

npm outdated vs npm update

npm outdated

查看项目中是否存在过期的 packages,或者指定的 packages 是否过期

1
npm outdated [<pkg> ...]

举例

1
2
3
› npm outdated
Package Current Wanted Latest Location Depended by
semver-regex 3.1.2 3.1.2 4.0.0 node_modules/semver-regex fe.cli

在某一项目下运行 npm outdated,便可以列出当前项目中所有已经过期的依赖。

如上所示,semver-regex 已经过期了,当前版本为 3.1.2,最新版本为 4.0.0

想要查看 package 的详细信息,可以使用npm view <pkg>,如查看 semver-regex: npm view semver-regex

查看全局已经过期的 package

1
npm outdated -g

npm update & npm upgrade

更新 packages,npm update 会将 package 更新到当前已发布的 最新版本

1
2
3
npm update [-g] [<pkg>...]

aliases: up, upgrade

在上文中,我们已经介绍了使用 npm outdated 查看过期的 packages。本段将介绍如何使用 npm update 更新过期的 packages

更新当前项目中所有过期的项目

1
npm update

更新全局的过期项目

1
npm update -g

更新指定的 packages

1
npm update pkg1 pkg2 ...

其他 npm 命令

  • npm config: npm 配置

  • npm bin: 打印 npm 可执行命令 bin 的文件夹

  • npm completion: npm 补全脚本

npm config

npm 配置。用于列出当前 npm 环境的配置信息,或管理 npm 配置文件,一般是 .npmrc。

列出当前的 npm 配置

1
2
3
4
npm config list

# 列出详细配置
npm config list -l

npm bin

打印 npm 可执行命令 bin 的文件夹

1
npm bin [-g | --global]

npm completion

npm 补全命令脚本, 可以通过下列命令将命令补全脚本注入到 .bashrc 或 .zshrc 中,这样即可以在终端的任何地方使用。

1
2
npm completion >> ~/.bashrc
npm completion >> ~/.zshrc

Hexo 踩坑指南

安装主题

我选用 next 主题,简洁、素雅

安装指南:

1
2
$ cd hexo
$ git clone https://github.com/theme-next/hexo-theme-next themes/next

favicon 配置

需要在 themes/next/_config.yml 中对 favicon 进行配置,我的配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ---------------------------------------------------------------
# Site Information Settings
# ---------------------------------------------------------------

# To get or check favicons visit: https://realfavicongenerator.net
# Put your favicons into `hexo-site/source/` (recommend) or `hexo-site/themes/next/source/images/` directory.

# Default NexT favicons placed in `hexo-site/themes/next/source/images/` directory.
# And if you want to place your icons in `hexo-site/source/` root directory, you must remove `/images` prefix from pathes.

# For example, you put your favicons into `hexo-site/source/images` directory.
# Then need to rename & redefine they on any other names, otherwise icons from Next will rewrite your custom icons in Hexo.
favicon:
small: /favicon.ico
medium: /favicon.ico
apple_touch_icon: /images/apple-touch-icon-next.ico
safari_pinned_tab: /logo.svg
#android_manifest: /images/manifest.json
#ms_browserconfig: /images/browserconfig.xml

关于锚点失效问题

这是 hexo-renderer-markdown-it 的一个 bug,但作者不想修复,认为是其他库的坑,所以我们要参考这个 issue ,手动到 node_modules 中修改该库,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
'use strict';

module.exports = function (data, options) {
var MdIt = require('markdown-it');
var cfg = this.config.markdown;
var opt = (cfg) ? cfg : 'default';
var parser = (opt === 'default' || opt === 'commonmark' || opt === 'zero') ?
new MdIt(opt) :
new MdIt(opt.render);

parser.use(require('markdown-it-named-headings')) // 只需要添加这行代码

if (opt.plugins) {
parser = opt.plugins.reduce(function (parser, pugs) {
return parser.use(require(pugs));
}, parser);
}

if (opt.anchors) {
parser = parser.use(require('./anchors'), opt.anchors);
}

return parser.render(data.text);
};

自动化修复以上问题的脚本

写了一个自动化脚本,git pull 下我的博客备份后,自动安装、修复上述问题。

运行工程目录下的 ./hexo-init.sh,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

npm i hexo-cli -g

yarn

git clone https://github.com/theme-next/hexo-theme-next themes/next

# 修复锚点不生效问题
cp ./init_source/hexo-renderer-markdown-it/lib/renderer.js ./node_modules/hexo-renderer-markdown-it/lib/renderer.js

# 自动配置 favicon
cp ./init_source/next/_config.yml ./themes/next/_config.yml

Git pull 下来的 Hexo 工程,hexo deploy 后将整个项目都 push 上去了

每次 git pull 备份的工程后,需要清理一下 hexo 工程,具体方法如下:

1
2
$ cd adispring.github.io
rm -rf .git .deploy_git hexo clean

(又名 “代数 JavaScript 规范”)

logo

该项目规定了通用代数数据结构的互操作性:

dependencies

概览

代数是遵循一定法则的、具有封闭性的,一系列值及一系列操作的集合。

每个 Fantasy Land 代数是一个单独的规范。一个代数可能依赖于其他必需实现的代数。

术语

  1. “值”:任何 JavaScript 值,包括下面定义的结构的任何值。

  2. “等价”:对给定值的等价性的恰当定义。这个定义应该保证两个值可以在其对应的抽象的程序中,能够安全地进行交换。例如:

    • 当两个列表对应的索引上的值都相等时,它们是等价的。
    • 当两个普通的 JavaScript 对象所有键值对都相等时,它们(作为字典)是等价的。
    • 当两个 promises 生成相等的值时,它们是等价的。
    • 当两个函数给定相同的输入,产生相同的输出时,它们是等价的。

类型签名符号

本文档使用的类型签名符号如下所述:[^1]

  • :: _“是 xx 的成员”。
    • e :: t 读作:“表达式 e 是类型 t 的成员”。
    • true :: Boolean - “true 是类型 Boolean 的成员”。
    • 42 :: Integer, Number - “42 是类型 IntegerNumber 的成员”。
  • 新类型可以通过类型构造函数创建。
    • 类型构造函数可以接受零或多个类型参数。
    • Array 是一个接受单个参数的类型构造函数。
    • Array String 代表包含字符串的数组的类型。后面每个都是 Array String 类型的:[]['foo', 'bar', 'baz']
    • Array (Array String) 代表包含字符串的数组的数组的类型。后面每个都是 Array (Array String) 类型的:[][[], []][[], ['foo'], ['bar', 'baz']]
  • 小写字母代表类型变量。
    • 类型变量可以接受任何类型,除非受到类型约束的限制(参见下面的胖箭头)。
    • -> (箭头) 函数类型的构造函数
    • -> 是一个 中缀 构造函数,它接受两个类型参数,左侧参数为输入的类型,右侧参数为输出的类型。
    • -> 的输入类型可以通过一组类型创建出来,该函数接受零个或多个参数。其语法是:(<input-types>) -> <output-type>,其中 <input-types> 包含零个或多个 “逗号-空格” (, )分开的类型表示,对于一元函数,圆括号也可以省略。
    • String -> Array String 是一种接受一个 String 并返回一个 Array String 的函数的类型。
    • String -> Array String -> Array String 是一种函数类型,它接受一个 String 并返回一个函数,返回的函数接受一个 Array String 并返回一个 Array String
    • (String, Array String) -> Array String 是一种函数类型,它接受一个 StringAray String 作为参数,并返回一个 Array String
    • () -> Number 是一种不带输入参数,返回 Number 的函数类型。
  • ~> (波浪形箭头) 方法类型的构造函数。
    • 当一个函数是一个对象(Object)的属性时,它被称为方法。所有方法都有一个隐含的参数类型 - 它是属性所在对象的类型。
    • a ~> a -> a 是一种对象中方法的类型,它接受 a 类型的参数,并返回一个 a 类型的值。
  • => (胖箭头) 表示对类型变量的约束。
    • a ~> a -> a(参见上面的波浪形箭头)中,a 可以为任意类型。半群 a => a ~> a -> a 会添加一个约束,使得类型 a 现在必须满足该半群的类型类。满足类型类意味着,须合法地实现该类型类指定所有函数/方法。

例如:

1
2
3
4
5
6
traverse :: Applicative f, Traversable t => t a ~> (TypeRep f, a -> f b) -> f (t b)
'------' '--------------------------' '-' '-------------------' '-----'
' ' ' ' '
' ' - type constraints ' ' - argument types ' - return type
' '
'- method name ' - method target type

[^1]: 更多相关信息,请参阅 Sanctuary 文档中的 Types 部分。

前缀方法名

为了使数据类型与 Fantasy Land 兼容,其值必须具有某些属性。这些属性都以 fantasy-land/ 为前缀。例如:

1
2
//  MyType#fantasy-land/map :: MyType a ~> (a -> b) -> MyType b
MyType.prototype['fantasy-land/map'] = ...

在本文中,不使用前缀的名称,只是为了减少干扰。

为了方便起见,你可以使用 fantasy-land 包:

1
2
3
4
5
6
7
8
9
var fl = require('fantasy-land')

// ...

MyType.prototype[fl.map] = ...

// ...

var foo = bar[fl.map](x => x + 1)

类型表示 (JavaScript 中的构造函数?)

某些行为是从类型成员的角度定义的。而另外一些行为不需要类型成员。因此,某些代数需要一个类型来提供值层面上的表示(具有某些属性)。例如,Identity 类型可以提供 Id 作为其类型表示:Id :: TypeRep Identity

如果一个类型提供了类型表示,那么这个类型的每个成员都必须有一个指向该类型表示的 contructor 属性。

代数

Setoid

  1. a.equals(a) === true (自反性)
  2. a.equals(b) === b.equals(a) (对称性)
  3. 如果 a.equals(b) 并且 b.equals(a),则 a.equals(c) (传递性)

equals 方法

1
equals :: Setoid a => a ~> a -> Boolean

具有 Setoid 的值必须提供 equals 方法。equals 方法接受一个参数:

a.equals(b)
  1. b 必须是相同 Setoid 的值
    1. 如果 b 不是相同的 Setoid,则 equals 的行为未指定(建议返回 false)。
    2. equals 必须返回一个布尔值(truefalse)。

Ord

实现 Ord 规范的值还必须实现 Setoid 规范。

  1. a.lte(b)b.lte(a) (完全性)
  2. 如果 a.lte(b)b.lte(a),则 a.equals(b) (反对称性)
  3. 如果 a.lte(b)b.lte(c),则 a.lte(c) (传递性)

lte 方法

1
lte :: Ord a => a ~> a -> Boolean

具有 Ord 的值必须提供 lte 方法。lte 方法接受一个参数:

`a.lte(b)`
  1. b 必须是相同 Ord 的值。
    1. 如果 b 不是相同的 Ord,则 lte 的行为未指定 (建议返回 false)。
  2. lte 必须返回布尔值(truefalse)。

Semigroupoid

  1. a.compose(b).compose(c) === a.compose(b.compose(c)) (结合性)

compose 方法

1
compose :: Semigroupoid c => c i j ~> c j k -> c i k

具有 Semigoupoid 的值必须提供 compose 组合方法。compose 方法接受一个参数:

a.compose(b)
  1. b 必须返回相同 Semigroupoid 规范。
    1. 如果 b 不是相同的 Semigroupoid,compose 的行为未指定。
  2. compose 必须返回相同 Semigroupoid 的值。

Category

实现范畴规范的值还必须实现半群规范。

  1. a.compose(C.id()) 等价于 a (右同一性)
  2. C.id().compose(a) 等价于 a (左同一性)

id 方法

1
id :: Category c => () -> c a a

具有范畴的值必须在其类型表示中提供一个 id 函数。

C.id()

给定值 c,可以通过 contructor 属性来访问其类型表示:

c.constructgor.id()
  1. id 必须返回相同范畴的值。

Semigroup

  1. a.concat(b).concat(c) 等价于 a.concat(b.concat(c)) (结合性)

concat 方法

1
concat :: Semigroup a => a ~> a -> a

具有 Semigroup 的值必须提供 concat 方法。concat 方法接受一个参数:

s.concat(b)
  1. b 必须是相同 Semigroup 的值
    1. 如果 b 不是相同的 Semigroup,则 concat 的行为未指定。
  2. concat 必须返回相同 Semigroup 的值。

Monoid

实现 Monoid 规范的值还必须实现 Semigroup 规范

  1. m.concat(M.empty()) 等价于 m (右结合性)
  2. M.empty().concat(m) 等价于 m (左结合性)

empty 方法

1
empty :: Monoid m => () -> m

具有 Monoid 的值必须在其类型表示上提供 empty 方法:

M.empty()

给定值 m,可以通过 constructor 属性来访问其类型表示:

m.constructor.empty()
  1. empty 必须返回相同 Monoid 的值。

Group

实现 Group 规范的值还必须实现 Monoid 规范。

  1. g.concat(g.invert()) 等价于 g.constructor.empty() (右反转性??)
  2. g.invert().concat(g) 等价于 g.constructor.empty() (左翻转性??)

invert 方法

1
invert :: Group g => g ~> () -> g

具有 Semigroup 的值必须提供 invert 方法。invert 方法接受零个参数:

g.invert()
  1. invert 必须返回相同 Group 的值。

Filterable

  1. v.filter(x => p(x) && q(x)) 等价于 v.filter(p).filter(q) (分配性)
  2. v.filter(x => true) 等价于 v (同一性)
  3. v.filter(x -> false) 等价于 w.filter(x => false),如果 vw 具有相同的 Filterable 值 (湮灭??)

filter 方法

1
filter :: Filterable f => f a ~> (a -> Boolean) -> f a

具有 Filterable 的值必须提供 filter 方法。filter 方法接受一个参数:

v.filter(p)
  1. p 必须是一个函数。

    1. 如果 p 不是函数,则 filter 的行为未指定。
    2. p 必须返回 turefalse。如果返回任何其它值,filter 的行为未指定。
  2. filter 必须返回相同 Filterable 的值。

Functor

  1. u.map(a => a) 等价于 u (同一性)
  2. u.map(x => f(g(x))) 等价于 u.map(g).map(f) (组合性)

map 方法

1
map :: Functor f => f a ~> (a -> b) -> f b

具有 Functor 的值必须提供 map 方法。map 方法接受一个参数:

u.map(f)
  1. f 必须是一个函数,

    1. 如果 f 不是函数,则 map 的行为未指定。
    2. f 可以返回任何值。
    3. f 返回值的任何部分都不应该被检查(??)。
  2. map 必须返回相同 Functor 的值。

Contravariant

  1. u.contramap(a => a) 等价于 u (同一性)
  2. u.contramap(x => f(g(x))) 等价于 u.contramap(f).contramap(g) (组合性)

contramap 方法

1
contramap :: Contravariant f => f a ~> (b -> a) -> f b

具有 Contravariant 的值必须提供 contramap 方法。contramap 方法接受一个参数:

u.contramap(f)
  1. f 必须是一个函数,
    1. 如果 f 不是函数,则 contramap 的行为未指定。
    2. f 可以返回任何值。
    3. f 返回值的任何部分都不应该被检查(??)。
  2. contramap 必须返回相同 Contravariant 的值。

Apply

实现 Apply 规范的值还必须实现 Functor 规范。

  1. v.ap(u.ap(a.map(f => g => x => f(g(x))))) 等价于 v.ap(u).ap(a) (组合型),推导过程??

ap 方法

1
ap :: Apply f => f a ~> f (a -> b) -> f b

具有 Apply 的值必须提供 ap 方法。ap 方法接受一个参数:

a.ap(b)
  1. b 必须是一个函数的 Apply
    1. 如果 b 不代表函数,则 ap 的行为未指定。
    2. b 必须与 a 具有相同的 Apply。
  2. a 可以是任意值的 Apply。(??)
  3. ap 必须能将 Apply b 内的函数应用于 Apply a 的值上
    1. 函数返回值的任何部分都不应该被检查。
  4. ap 返回的 Apply 必须与 ab 的相同。

Applicative

实现 Applicative 规范的值还必须实现 Apply 规范。

  1. v.ap(A.of(x => x)) 等价于 v (同一性)
  2. A.of(x).ap(A.of(f)) 等价于 A.of(f(x)) (同态性, homomorphism)
  3. A.of(y).ap(u) 等价于 u.ap(A.of(f => f(y))) (交换性)

of 方法

1
of :: Applicative f => a -> f a

具有 Applicative 的值必须在其类型表示中提供 of 函数。of 函数接受一个参数:

F.of(a)

给定值 f,可以通过 contructor 属性访问其类型表示:

f.contructor.of(a)
  1. of 必须提供相同的 Applicative
    1. a 的任何部分都不应该被检查

Alt

实现 Alt 规范的值还必须实现 Functor 规范。

  1. a.alt(b).alt(c) 等价于 a.alt(b.alt(c)) (结合性)
  2. a.alt(b).map(f) 等价于 a.map(f).alt(b.map(f)) (分配性) (看起来像乘法,有什么实际用途呢?)

alt 方法

1
alt :: Alt f => f a ~> f a -> f a

具有 Alt 的值必须提供 alt 方法。alt 方法接受一个参数:

a.alt(b)
  1. b 必须是相同 Alt 的值
    1. 如果 b 不是相同的 Alt,则 alt 的行为未指定。
    2. ab 可以包含相同类型的任何值。
    3. ab 包含值的任何部分都不应该被检查。
  2. alt 必须返回相同 Alt 的值。

Plus

实现 Plus 规范的值还必须实现 Alt 规范。

  1. x.alt(A.zero()) 等价于 x (右同一性)
  2. A.zero().alt(x) 等价于 x (左同一性)
  3. A.zero().map(f) 等价于 A.zero() (湮灭??)

zero 方法

1
zero :: Plus f => () -> f a

具有 Plus 的值必须在其类型表示中提供 zero 函数:

A.zero()

给定值 x,可以通过 contructor 属性访问其类型表示:

x.contructor.zero()

zero 必须返回相同 Plus 的值。

Alternative

实现 Alternative 规范的值还必须实现 ApplicativePlus 规范。

  1. x.ap(f.alt(g)) 等价于 x.ap(f).alt(x.ap(g)) (分配性)
  2. x.ap(A.zero()) 等价于 A.zero() (湮灭)

Foldable

u.reduce 等价于 u.reduce((acc, x) => acc.concat([x]), []).reduce

reduce 方法

1
reduce :: Foldable f => f a ~> ((b, a) -> b, b) -> b

具有 Foldable 的值必须在其类型表示中提供 reduce 函数。reduce 函数接受两个参数:

u.reduce(f, x)
  1. f 必须是一个二元函数
    1. 如果 f 不是函数,则 reduce 的行为未指定。
    2. f 的第一个参数类型必须与 x 的相同。
    3. f 的返回值类型必须与 x 的相同。
    4. f 返回值的任何部分都不应该被检查。
  2. x 是归约的初始累积值
    1. x 的任何部分都不应该被检查

Traversable

实现 Traversable 规范的值还必须实现 FunctorFoldable 规范。

  1. 对于任意 tt(u.traverse(F, x => x)) 等价于 u.traverse(G, t) ,因为 t(a).map(f) 等价于 t(a.map(f)) (自然性)
  2. 对于任意 Applicative Fu.traverse(F, F.of) 等价于 F.of(u) (同一性)
  3. u.traverse(Compose, x => new Compose(x)) 等价于 new Compose(u.traverse(F, x => x).map(x => x.traverse(G, x => x))),对下面定义的 Compose 和 任意 Applicatives FG 都适用 (组合性)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var Compose = function(c) {
this.c = c;
};

Compose.of = function(x) {
return new Compose(F.of(G.of(x)));
};

Compose.prototype.ap = function(f) {
return new Compose(this.c.ap(f.c.map(u => y => y.ap(u))))
};

Compose.prototype.map = function(f) {
return new Compose(this.c.map(y => y.map(f)));
};

traverse 方法

1
traverse :: Applicative f, Traversable t => t a ~> (TypeRep f, a -> f b) -> f (t b)

具有 Traversable 的值必须提供 traverse 函数。traverse 函数接受两个参数:

u.traverse(A, f)
  1. A 必须是一个 Applicative 的类型表示。
  2. f 必须是一个返回值的函数
    1. 如果 f 不是函数,则 traverse 的行为未指定。
    2. f 必须返回类型表示为 A 的值。
  3. traverse 必须返回类型表示为 A 的值。

Chain

实现 Chain 规范的值还必须实现 Apply 规范。

  1. m.chain(f).chain(g) 等价于 m.chain(x => f(x).chain(g)) (结合性)

chain 方法

1
chain :: Chain m => m a ~> (a -> m b) -> m b

具有 Chain 的值必须提供 chain 函数。chain 函数接受一个参数:

m.chain(f)
  1. f 必须是一个返回值的函数
    1. 如果 f 不是函数,则 chain 的行为未指定。
    2. f 必须返回相同 Chain 的值。
  2. chain 必须返回相同 Chain 的值。

ChainRec

实现 ChainRec 规范的值还必须实现 Chain 规范。

  1. M.chainRec((next, done, v) => p(v) ? d(v).map(done) : n(v).map(next), i) 等价于 function step(v) { return p(v) ? d(v) : n(v).chain(step); }(i) (等价性)
  2. M.chainRec(f, i) 栈的用量必须是 f 自身栈用量的常数倍。

chainRec 方法

1
chainRec :: ChainRec m => ((a -> c), b -> c, a) -> m b

具有 ChainRec 的值必须在其类型表示中提供 chainRec 函数。chainRec 函数接受两个参数:

M.chainRec(f, i)

给定值 m,可以通过 contructor 属性访问其类型表示:

m.constructor.chainRec(f, i)
  1. f 必须是一个返回值的函数
    1. 如果 f 不是函数,则 chainRec 的行为未指定。
    2. f 接受三个参数 nextdonevalue
      1. next 是一个函数,其接受一个与 i 类型相同的参数,可以返回任意值
      2. done 也是一个函数,其接受一个参数,并返回一个与 next 返回值类型相同的值
      3. value 是一个与 i 类型相同的值。
    3. f 必须返回一个相同 ChainRec 的值,其中包含的是从 donenext 返回的值。
  2. chainRec 必须返回一个相同 ChainRec 的值,其中包含的值的类型与 done 的参数类型相同。

Monad

实现 Monad 规范的值还必须实现 ApplicativeChain 规范。

  1. M.of(a).chain(f) 等价于 f(a) (左同一性)
  2. m.chain(M.of) 等价于 m (右同一性)

Extend

实现 Extend 规范的值还必须实现 Functor 规范。

  1. w.extend(g).extend(f) 等价于 w.extend(\_w => f(\_w.extend(g)))

extend 方法

1
extend :: Extend w => w a ~> (w a -> b) -> w b

具有 Extend 的值必须提供 extend 函数。extend 函数接受一个参数:

w.extend(f)
  1. f 必须是一个返回值的函数,
    1. 如果 f 不是函数,则 extend 的行为未指定。
    2. f 必须返回一个 v 类型的值,其中 vw 中包含的某个变量 v (??)
    3. f 返回值的任何部分都不应该被检查。
  2. extend 必须返回相同 Extend 的值。

Comonad

实现 Comonad 规范的值还必须实现 Extend 规范。

  1. w.extend(_w => _w.extract()) 等价于 w (左同一性)
  2. w.extend(f).extract() 等价于 f(w) (右同一性)

extract 方法

具有 Comonad 的值必须提供 extract 函数。extract 函数接受零个参数:

w.extract()
  1. extract 必须返回一个 v 类型的值,其中 vw 中包含的某个变量 v (??)
    1. v 必须与在 extend 中的 f 返回的类型相同。

Bifunctor

实现 Bifunctor 规范的值还必须实现 Functor 规范。

  1. p.bimap(a => a, b => b) 等价于 p (同一性)
  2. p.bimap(a => f(g(a)), b => h(i(b))) 等价于 p.bimap(g, i).bimap(f, h) (组合性)

bimap 方法

1
bimap :: Bifunctor f => f a c ~> (a -> b, c -> d) -> f b d

具有 Bifunctor 的值必须提供 bimap 函数。bimap 函数接受两个参数:

c.bimap(f, g)
  1. f 必须是一个返回值的函数,
    1. 如果 f 不是函数,则 bimap 的行为未指定。
    2. f 可以返回任意值
    3. f 返回值的任何部分都不应该被检查。
  2. g 必须是一个返回值的函数,
    1. 如果 g 不是函数,则 bimap 的行为未指定。
    2. g 可以返回任意值
      3.g 返回值的任何部分都不应该被检查。
  3. bimap 必须返回相同 Bifunctor 的值。

Profunctor

实现 Profunctor 规范的值还必须实现 Functor 规范。

  1. p.promap(a => a, b => b) 等价于 p (同一性)
  2. p.promap(a => f(g(a)), b => h(i(b))) 等价于 p.promap(f, i).promap(g, h) (组合性)

promap 方法

1
promap :: Profunctor p => p b c ~> (a -> b, c -> d) -> p a d
  1. f 必须是一个返回值的函数,
    1. 如果 f 不是函数,则 promap 的行为未指定。
    2. f 可以返回任意值
    3. f 返回值的任何部分都不应该被检查。
  2. g 必须是一个返回值的函数,
    1. 如果 g 不是函数,则 promap 的行为未指定。
    2. g 可以返回任意值
    3. g 返回值的任何部分都不应该被检查。
  3. promap 必须返回相同 Profunctor 的值。

推导

当创建满足多个代数的数据类型是,作者可以选择实现某些方法,然后推导出剩余的方法。推导:

  • equals 可以由 lte 推导出:

    1
    function(other) { retrun this.lte(other) && other.lte(this) }
  • map 可以由 apof 推导出:

    1
    function(f) { return this.ap(this.of(f))}
  • map 可以由 chainof 推导出:

    1
    function(f) { return this.chain(a => this.of(f(a))); }
  • map 可以由 bimap 推导出 (??):

    1
    function(f) { return this.bimap(a => a, f); }
  • map 可以由 promap 推导出:

    1
    function(f) { return this.promap(a => a, f); }
  • ap 可以由 chain 推导出:

    1
    function(m) { return m.chain(f => this.map(f)); }
  • reduce 可以由下列推导出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function(f, acc) {
    function Const(value) {
    this.value = value;
    }
    Const.of = function(\_) {
    return new Const(acc);
    }
    Const.prototype.map = function(\_) {
    return this;
    }
    Const.prototype.ap = function(b) {
    return new Const(f(b.value, this.value));
    }
    return this.traverse(x => new Const(x), Const.of).value;
    }
  • map 的推导如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function(f) {
    function Id(value) {
    this.value = value;
    }
    Id.of = function(x) {
    return new Id(x);
    }
    Id.prototype.map = function(f) {
    return new Id(f(b.value));
    }
    Id.prototype.ap = function(b) {
    return new Id(this.value(b.value));
    }
    return this.traverse(x => Id.of(f(x)), Id.of).value;
    }
  • filter 可以由 ofchainzero 推导出:

    1
    2
    3
    4
    function(pred) {
    var F = this.constructor;
    return this.chain(x => pred(x) ? F.of(x) : F.zero());
    }
  • filter 还可以由 concatofzeroreduce

    1
    2
    3
    4
    function(pred) {
    var F = this.constructor;
    return this.reduce((f, x) => pred(x) ? f.concat(F.of(x)) : f, F.zero());
    }

注意

  1. 如果实现的方法和规则不止一种,应该选择一种实现,并为其他用途提供包装。
  2. 我们不鼓励重载特定的方法。那样会很容易造成崩溃和错误的行为。
  3. 建议对未指定的行为抛出异常。
  4. sanctuary-identity 中提供了一个实现了许多方法的 Id 容器。

备选方案

此外,还存在一个 Static Land 规范,其思想与 Fantasy Land 完全相同,但是是基于静态方法而非实例方法。

目标

我们编写 Ramda 的目的是,用比原生 JavaScript 更好的方式进行编程。给定数据如下:

1
2
3
4
5
6
7
// `projects` 是一个以下形式的对象类型的数组
// {codename: 'atlas', due: '2013-09-30', budget: 300000,
// completed: '2013-10-15', cost: 325000},
//
// `assignments` 是将工程名映射到员工名的对象类型的数组,如下所示:
// {codename: 'atlas', name: 'abby'},
// {codename: 'atlas', name: 'greg'},

我们想按以下形式进行编程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var employeesByProjectName = R.pipe(
R.propEq('codename'),
R.flip(R.filter)(assignments),
R.map(R.prop('name'))
);
var onTime = R.filter(proj => proj.completed <= proj.due);
var withinBudget = R.filter(proj => proj.cost <= proj.budget);
var topProjects = R.converge(R.intersection, [onTime, withinBudget]);
var bonusEligible = R.pipe(
topProjects,
R.map(R.prop('codename')),
R.map(employeesByProjectName),
R.flatten,
R.uniq
);

console.log(bonusEligible(projects));
// Live version at https://codepen.io/adispring/pen/WdQjXL?editors=0012
// 译者注:原文用的 ramda@0.22.1 版本比较旧了,converge 第二个之后的函数未加中括号
// 本文采用 ramda@0.25.0

这段代码是一段 “函数式” 的 pipeline。它是由模块化、可组合的函数构建而成,这些函数拼接在一起形成长长的管道,然后我们可以从管道入口传入待处理的数据。上面的每个 var 变量声明都代表一个单输入单输出的函数。每个函数的输出结果在管道中继续传递下去。

这些函数对数据进行转换并将转换结果传给下一个函数。需要注意的是,这些函数都不会改变输入参数的值。

Ramda 的目标是让这种风格的编码在 JavaScript 中更容易些。这就是它的目的,我们的设计决策都是由这个目标驱动的。还有一个唯二值得关注的点:简洁(Simplicity)。我们追求的是简洁(Simple),而不是简单(容易,Easy)。如果你没有看过 Rich Hickey 的 “Simple Made Easy”,你应该花点时间看看。简洁,意味着不要将独立的功能点耦合或纠缠到一起。Ramda 努力坚持这个原则。(单一职责原则)

座右铭

Ramda 自认为是 “一个实用的 JavaScript 函数式编程库”。什么意思呢?

在本文接下来的部分,我们将这句话的解释分成几部分,并在下文中分别讨论每部分在 Ramda 中的含义。

为 JavaScript 编程人员而设计

有些惊讶?

Ramda 是为编程人员设计的库。它不是一个学术试验品。它是为一线人员构建系统而准备的,它必须能运行,并且是良好、高效地运行。

我们尽量描述清楚函数的作用,以确保不会因误解而发生意外。不过,我们做的一些事情可能会让很多学术同仁感到惊讶。但只要日常的(工业界)编程人员理解我们,我们就愿意冒这个风险。例如,Ramda 的 is 函数可以用来代替 isArrayisNumberisFunction 等函数。Ramda 版本的类型判断函数接受一个构造函数和一个对象:

1
2
3
is(Number, 42); //=> true
is(Function, 42); //=> false
is(Object, {}); //=> true

这也适用于自定义的构造函数。如果 Square 的原型链上包含 Rectangle,则可以这样做:

1
is(Rectangle, new Square(4)); //=> true

但这也可能引起学术界同仁的疑惑:

1
is(Object, 42); //=> false

现实世界的编程人员知道这是完全正确的。字符串、布尔值和数字是原生类型,但它们不是对象。然而学者们可能会坚持,认为包装过的 Number 类型继承自 Object,类比 Square/Rectangle ,也应该返回 true。当然,他们可以那么认为… 在他们自己的库里。这些函数对一线的编程人员才是最有用的。(译者注:Ramda 作者可能被学术界 Nerd 们的絮叨伤害过…)

命令式实现

我们并没有非得以函数式的方式实现 Ramda 的函数。许多我们提出的构造,像 folds、maps、filters 只能通过递归进行函数式实现。但由于 JavaScript 并没有对递归进行优化;我们不能用优雅的递归算法来编写这些函数。相反,我们诉诸于丑陋的、命令式的 while 循环。我们来编写令人讨厌的代码,以便(Ramda)用户可以编写更优雅的代码。Ramda 的实现绝不应该被认为是如何编写函数式代码的指导。(译者注:为了效率和实用性的考虑,Ramda 底层实现其实是命令式的)

虽然我们从 Haskell、ML 和 LISP(及其变种的函数式部分)等函数式语言中获得很多灵感,但 Ramda 从不试图实现这些语言的任何部分。

Ramda 也没有试图简单地以函数式的方式重写原生 API。机械的生搬硬套没有任何意义。当我们实现 map 函数时,我们既不用非得遵循 Array.prototype.map 的 ECMAScript 规范,也没有囿于已有的实现。我们可以自由地为我们的库定义每个函数的功能,它是如何工作的,确切的参数顺序,它会不会更改输入参数等(永远不会!),返回什么,以及它会抛出什么类型的错误等。换句话说,API 是我们自己的。我们确实受到了函数式编程的传统的限制,但如果在 JavaScript 中使用某些东西需要做出妥协,我们可以做出任何被认为实用的选择。(译者注:总之,我们对 Ramda 有绝对的掌控权)

作为一个库

Ramda 是一个库,一个工具包,或者类比 Underscore ,是一个辅助开发工具。它不是一个决定如何构建应用程序结构的框架(如 React)。相反,它只是一组函数,旨在使之前描述的可组合函数风格的编程更容易一些。这些函数并没有决定你的工作流程。例如,你不必为了使用过滤器而传递 where 函数的结果。

为什么不使用…

Ramda 不可避免的会与 UnderscoreLodash 做对比;其所提供的函数在功能和函数名称会有重叠。但是,Ramda 不会成为这些库的替代品。即使有一个神奇的参数顺序调整机制,它仍然不是一个简单的替代品。Ramda 有自身的优势、专注于不同领域。请记住,如果这些库能够很容易地按我们想要的方式进行编程,那么就不需要 Ramda 了。

当我们开始编写该库时,主要的函数式编程库有:

  • Oliver Steele 的 Functional Javascript, 这是首次使用令人难以置信的方式,展示真的可以在 JavaScript 中用函数式的方式编程。但它也只是个玩具,用生产环境中不想要的技巧进行Hack。

  • Reg Braithwaite 的 allong.es,这本书已经出来了,并且这个鲜为人知的库已经可以用了。但这个库自称是 Underscore 或 Lodash 的伴侣,虽然做得很好,但它似乎只是一个支持这本书的最小代码集合,而不是一个完整的库。

  • Michael Fogus 的 Lemonad 是一个具有前瞻性的实验品,也许是这里面最有趣的一个,它的一些函数在其他 JavaScript 库中是没有的。但它似乎只是一个 playground,基于此,该库基本上被废弃了。

  • 当然还有一些大块头,比如 Jeremy Ashkenas 的 Underscore 和 John-David Dalton 的 Lodash。这些库的广泛使用,显示了大量的 JavaScript 开发人员不再害怕函数式构造。它们非常受欢迎,已经包含了许多我们想要的工具。

那么为什么我们不使用 Underscore/Lodash 呢?答案很简单。对于我们想要的编程形式,它们犯了一些根本性的错误:它们传递参数的顺序是错误的。

这听起来很可笑,甚至无足轻重,但是对于这种编程风格来说确实 必不可少。为了构建简单可组合的函数,我们需要能正确协同工作的工具。其中最重要的是自动柯里化。为了能正确地进行柯里化,我们必须确保最经常变化的参数 – 通常是数据 – 放到最后。

差别很简单。假设我们有这样一个可用的函数:

1
var add = function(a, b) {return a + b;};

并且我们想要一个函数,可以计算一篮子水果的总价格,例如:

1
2
3
4
5
var basket = [
{item: 'apples', per: .95, count: 3, cost: 2.85},
{item: 'peaches', per: .80, count: 2, cost: 1.60},
{item: 'plums', per: .55, count: 4, cost: 2.20}
];

我们想要这样写:

1
var sum = reduce(add, 0);

并且这样使用:

1
var totalCost = compose(sum, pluck('cost'));

这就是我们想要的效果。注意看 sumtotalCost 是如此的简洁。使用 Underscore 写一个计算总价的函数并不难,但不会如此简洁。

1
2
3
4
5
6
var sum = function(list) {
return _.reduce(list, add, 0);
};
var totalCost = function(basket) {
return sum(_.pluck(basket, 'cost'));
};

在 Lodash 中可能的实现如下:

1
2
3
4
5
var sum = function(list) {
return _.reduce(list, add, 0);
};
var getCosts = _.partialRight(_.pluck, 'cost');
var totalCost = _.compose(sum, getCosts);

或者跳过中间变量:

1
2
3
4
var sum = function(list) {
return _.reduce(list, add, 0);
};
var totalCost = _.compose(sum, .partialRight(_.pluck, 'cost'));

虽然这已经非常接近我们想要的效果,但是跟 Ramda 版本的相比,还是有差距的:

1
2
var sum = R.reduce(add, 0);
var total = R.compose(sum, R.pluck('cost'));

在 Ramda 中实现这种风格的秘诀非常简单:我们将函数参数放在第一位,数据参数放到最后,并且将每个函数都柯里化。

来看一下 pluck。Ramda 有一个 pluck 函数,它和 Underscore 及 Lodash 中的 pluck 函数的功能差不多。这些函数接受一个字符串属性名和一个列表;返回由列表元素的属性值组成的列表。但 Underscore 和 Lodash 要求先提供列表,Ramda 希望最后传入列表。当你加入柯里化时,区别非常明显:

1
R.pluck('cost'); //=> function :: [Object] -> [costs]

通过简单地暂时不传列表参数给 pluck,我们得到一个新函数:接受一个列表,并从新提供的列表中提取 cost 属性值。

重申一下,就是这个简单的区别,将数据参数放到最后的自动柯里化函数让这两种风格变得不同:

1
2
3
4
5
6
var sum = function(list) {
return _.reduce(list, add, 0);
};
var total = function(basket) {
return sum(_.pluck(basket, 'cost'));
};
1
2
var sum = R.reduce(add, 0);
var total = R.compose(sum, R.pluck('cost'));

这就是我们开始编写一个新库的原因。

设计选择

接下来的问题是我们想要一个什么类型的库。我们当然知道我们想要一个简洁而又不怪异的 API。但是,这里仍然有一个悬而未决的问题:需要怎样确定 API 的适用广度和深度。

API 的广度,仅仅指它想要覆盖多少不同类型的功能。有两百个函数的 API 比只有十个函数的 API 适用范围要广得多。与大多数其他库一样,我们对其广度(适用范围)没有特别的限制。我们添加有用的函数,而不用担心库的规模的增大会导致崩溃。

一个库的深度,可以衡量它的函数们在独立使用时,可以提供多少种的不同的方式。(关于它们如何组合,是另一个完全不同的问题)在这里,我们走向了与 Underscore 及 Lodash 完全不同的方向。因为 JavaScript 不会去检查参数的类型和数量,所以编写根据传入确切参数(参数的类型和数量)而具有多种不同行为的单个函数是相当容易的。Underscore 和 Lodash 使用这种方法让它们的函数更灵活。例如,在 Lodash 中,pluck 不仅可以作用在 list 上,还可以作用在 object 和 string 上。从这个意义上讲,Lodash 是一个相当有深度的 API。Ramda 试图保持相对较浅的深度,原因如下:

Lodash 提供的功能如下:

1
_.pluck('abc', propertyName);

其将字符串拆分成由单字母字符串组成的数组,然后返回从每个字符串中提取的指定属性形成的数组。想找个这样的合适的应用场景是非常困难的:

1
_.pluck('abc', 'length'); //=> [1, 1, 1]

如果你真的想要一个元素为 1 ,且对应字符串中的每个字母的列表,下面这段代码比我的 Ramda 解法要短一些:

1
map(always(1), split('', 'abc'));

但这貌似没什么用,因为唯一另外一个属性是有意义的:

1
_.pluck('abc', '0'); //=> ['a', 'b', 'c']

如果 pluck 不存在,下面这样也是可以的:

1
'abc'.split(''); //=> ['a', 'b', 'c']

所以在字符串上操作并没多大用处。之所以将其(字符串)包含进来,可能是因为所有属于 Lodash “集合” 类的函数都应该能同时适用于数组、对象和字符串;这只是一个一致性问题。(令人失望的是,Lodash 没有打算扩展到其他实际的集合中去,比如 Map 和 Set)我们已经理解了 pluck 是如何在数组上工作的。它涵盖的另一种类型是对象,如下所示:

1
2
3
4
5
6
var flintstones1 = {
A: {name: 'fred', age: 30},
B: {name: 'wilma', age: 28},
C: {name: 'pebbles', age: 2}
};
_.pluck(flintstones1, 'age'); //=> [30, 28, 2]

可以创建一个对象,flintstones2 ,且以下结果为 true

1
_.isEqual(flintstones1, flintstones2); //=> true

但下面结果却为 false

1
_.pluck(flintstones1, 'age'); == _.pluck(flintstones2, 'age'); //=> false;

下面是一种可能的情况:

1
2
3
4
5
6
var flintstones2 = {
B: {name: 'wilma', age: 28},
A: {name: 'fred', age: 30},
C: {name: 'pebbles', age: 2}
};
_.pluck(flintstones2, 'age'); //=> [28, 30, 2]

问题在于,根据规范,对象 keys 的迭代顺序是依赖于实现的;通常它们按照添加到对象中的顺序进行迭代。。

在写本文时,我提交了一个关于这个问题的 issue。在最好的情况下,只有通过记录问题才能解决问题。但这个问题实在影响深远。如果你想统一列表和对象的行为,你将会不断遇到这个问题,除非你实现一个(非常慢的!)统一的顺序对 Object 属性进行迭代。

在 Ramda 中,pluck 只作用于列表。它接受一个属性名和一个列表,并返回一个相同长度的新列表。仅此而已。这个 API 深度很浅。(译者注:适用范围不太广)。

你可以将其看作特点,也可以看作是缺点。以 Lodash 的 filter 为例: 它接受一个数组、对象或字符串作为第一个集合(参数),然后接受一个函数、对象、字符串或者空作为它的回调,并且还需要一个对象或空作为它的 this 参数。你将一次获得 3 * 4 * 2 = 24 个函数!这要么是一个很大的问题,要么增加了从中找到一个你真正想要的方案的难度,增加了太多复杂性。决定权在于你。

在 Ramda 中,我们认为这种风格会增加不必要的复杂性。我们发现简单的函数签名对于维持简洁是至关重要的。如果我们需要函数既能作用于列表,又能作用于对象,我们会创建各自独立的函数(译者注:一般情况下会这样,但也有特例,比如 map)。如果有一个参数我们偶尔会用到,我们不会创建一个可选参数,而是创建两个函数。尽管这扩大了 API 的规模,但是它们保持了一至的浅度。

API 的增长

有一个我们已经意识到的危险,一个可以用三个字母拼出来的危险:“PHP”。我们不希望我们的 API 变成一个不可持续的、功能不一致的怪物。这是真正的威胁,没有强制性的规范来确定我们应该或不应该包含什么。

我们一直在努力;我们不希望包含一个貌似有用的函数。

为了避免变成 “PHP” 风格的庞然大物,我们专注于几件事情。首先,API 为王。虽然我们想要函数实现尽可能优雅,但我们为了即使是轻微的 API 性能改进,而牺牲了大量优雅的实现。我们试图执行严格的一致性标准。例如:像 somethingBy 这样的 Ramda 函数,以标准的方式看,与 somethingWith 函数是不同的。如 issue 65 所述,我们

使用 xxBy 来表示单一属性的比较,无论是对象的自然属性还是合成属性;使用 xxWith 表示更具一般性的函数。

一些使用这种方式的函数的例子包括max / min / sort / uniq / difference。

函数式

JavaScript 是一门多范式语言。你可以编写简单的命令式代码,面对对象的代码,或函数式代码。原始命令式的代码非常直白、简单。有很多库可以帮助你将 JavaScript 作为面向对象的语言使用。但是将 JavaScript 作为函数式语言使用的库非常少。Ramda 帮忙填补了这个空缺。

如前所述,我们当然不是第一个。其他库通过各种不同方式让人们可以在 JavaScript 中进行函数式编程(FP)。在我看来,将函数式世界与 JavaScript 结合最成功的可能是 allong.es。但它不是一个流行的库,与 UnderscoreLodash 这些库不在一个级别上(就流行程度而言);并且它有一个与 Ramda 不同的目标:它被设计为一种教学工具,一本书的演示库。

Ramda 正在尝试一些不同的东西。它的目标是成为一个能进行日常实际工作的实用的函数式库。

我们从头开始构建这个函数式库,使用了许多其他函数式语言通用的技术,以对 JavaScript 有意义的方式对这些技术进行移植。我们并没有试图弥合与面向对象世界之间的鸿沟,或者复制每一种函数式语言的每一个特性。实际上,我们甚至没有试图复制单一函数式语言的每个特性。它仍然是 JavaScript,甚至还继承了 JavaScript 缺陷。

函数式特性

那么,在广阔的函数式编程领域里,哪些部分是我们想要保留的,又有哪些不在我们的考虑范围呢?下面列出了函数式编程的一些主要(不完整)特性:

  • 一等函数
  • 高阶函数
  • 词法闭包
  • 引用透明
  • 数据不可变
  • 模式匹配
  • 惰性求值
  • 高效递归(TCO)
  • 同像性(Homoiconic)

前几个特性都已经内置在 JavaScript 中了。JavaScript 中的函数是一等公民,意味着我们可以像使用字符串、数字或对象等,对其引用或传递。我们还可以将函数作为参数传递给其他函数,并返回全新的函数,所以 JavaScript 中包含高阶函数。因为返回函数可以访问其在创建时的上下文中的所有变量,所以我们也在语言中构建出了词法闭包。

除此之外,上面列出其他的特性都没有自动包含在 JavaScript 中。有的可以轻易实现,有的只能部分或很难实现,有的则超出了语言的当前能力。

Ramda 可以确保在不会导致你的代码出问题的情况下,帮助实现(管理)上面的其他一些特性。例如,Ramda 不会改变你的输入数据。永远也不会!如果使用 append 将元素添加到列表的末尾,则会返回包含添加元素的新列表。你的原始列表保持不变。所以,由于 Ramda 不会尝试强行改变不可变的客户端数据,它可以很容易的与不可变数据一起工作。

另一方面,Ramda 强制要求引用透明。这个概念的意思是:可以在不改变整个程序行为的情况下,将表达式替换为其对应的计算值。对于 Ramda 来说,这意味着 Ramda 不会在应用程序中存储内部状态,也不会引用任何全局变量或者内部状态可以变的闭包。简言之,当你使用相同的值调用 Ramda 函数时,总会得到相同的结果。

在撰写本文时,正在讨论 Ramda 的惰性求值问题。一些库如 Lazy.jsLz.js ,表明在 JavaScript 中进行惰性求值是可行的。Transducer 提供了一种模拟惰性求值的方法。Ramda 正在努力增强自己这方面的能力。但这是一个巨大的改变,并不会很快实现。

Ramda 还会考虑加入一定程度的模式匹配,但不会像 Erlang 或 Haskell 这样的语言中的那么强大或方便。我们并没有看到会改变语言语法的宏,所以我们最多可以做一些类似于 Reg Braithwaite 所描述的东西。但是这至少在某种程度上讲是一种模式匹配的技术。

其他特性都超出了 Ramda 的能力。虽然有 trampolining 技术可以让你在不使用尾递归优化工具的情况下获得递归的一些好处,但是它们由于侵入性太强而不能被普遍使用。所以 Ramda 内部没有使用太多递归,也没有提供任何帮助来实现有效的递归。好消息是它将会被提到下一版语言规范的计划中去。

然后是 同像性(homoiconicity) – 某些语言(LISP、Prolog)的特性:程序的语法可以用一种在自身语言中易于理解和修改的数据结构表示的。这远远超出了 JavaScript 当前的能力,甚至超出了 Ramda 的梦想。

组合性

Ramda 的目标之一是,允许用户使用小的可组合函数,这是函数式编程的关键。

函数式编程通常涉及一些少量常见的数据结构,以及搭配操作它们的大量函数。这就是 Ramda 的工作原理。

简言之,Ramda 主要进行列表操作。但 JavaScript 没有列表的实现;最接近的模拟是 Array(数组)。这是 Ramda 使用的最基本的数据结构。我们不关心 JavaScript 数组的一些深层次可能的性质。我们忽略稀疏数组。如果你传了一个这样的数组给 Ramda,有可能会得到意想不到的结果。你需要传给 Ramda 以 Array 实现的列表。(如果这对你没有意义,不用担心;这是人们使用 JavaScript 数组的标准方式,你必须非常努力,才能创建出不寻常的情况(译者注:错误的情况))。

许多 Ramda 函数接受列表并且返回列表。这些函数都很容易组合。

1
2
3
4
5
6
// :: [Comment] -> [Number]  
var userRatingForComments = R.compose(
R.pluck('rating'), // [User] -> [Number]
R.map(R.propOf(users)), // [String] -> [User]
R.pluck('username') // [Comment] -> [String]
);

Ramda 还包含 pipe 函数,它跟 compose 功能相同,但顺序是反的;我个人觉得它更可读一些:

1
2
3
4
5
6
// :: [Comment] -> [Number]  
var userRatingForComments = R.pipe(
R.pluck('username'), // [Comment] -> [String]
R.map(R.propOf(users)), // [String] -> [User]
R.pluck('rating') // [User] -> [Number]
);

当然,组合可以作用于任何类型。如果下一个函数接受当前函数返回的类型,那么一切都应该没问题。

为了让其工作,Ramda 的函数必须具有足够小的规模。这与 Unix 的哲学不谋而合:大型的工具应该由小工具构建而成,每个工具做且只做一件事情。Ramda 的函数也是如此。理想情况下,这意味着以这些函数为基础的系统的复杂性只是问题自身固有的复杂性,而不是由库增加的附带的复杂性。

不变性

需要再次重申,Ramda 函数不会修改输入数据。这是函数式编程的核心原则,也是 Ramda 工作的核心。虽然这些函数可能会改变内部局部变量,但 Ramda 不会改变传递给它的任何数据。

这并不意味着你使用的所有东西都会被复制。Ramda 重用了它所能用到的。因此,在像 assocassocPath 这样的函数,返回具有特定更新属性的对象的克隆中,原始数据的所有非原生(non-primitive)属性在新对象中将以引用的方式使用。如果你想要一个对象的完全解耦的副本,Ramda 提供了 cloneDeep(译者注:现在 Ramda 只提供 clone 用作深拷贝) 函数。

这种不变性对 Ramda 来说是硬性规定。任何牵扯到变更用户数据的 pull request 都会被拒绝。我们认为这是 Ramda 的主要特征之一。

实用性

最后,Ramda 的目标是成为一个实用的库。这更难表述,因为实用性就像 “美丽” 一样:总是在旁观者眼中才能反映出来。永远都会有对不符合 Ramda 哲学的功能的要求,在那些提议者心目中,这些功能都是非常实用的。通常这些函数(功能)本身是有用的,但是由于不符合 Ramda 的哲学而被拒绝。

对于 Ramda 而言,实用性意味着一些具体的事情。

命令式实现

首先,Ramda 的实现并未遵循 LISP、ML 或者 Haskell 库中的优雅的编码技术。我们使用丑陋的命令式的循环,而不是优雅的递归代码块。一些 Ramda 的作者曾经在一个叫 Eweda 的早起的库中走过这条路,代码非常漂亮,但是在解决实际问题上它却失败了。许多列表函数只能处理一千个左右的条目,而且性能也很糟糕。 JavaScript 的设计没有很好的处理递归,大多数当前的引擎不执行任何尾部调用优化。

而 Ramda 的源代码却使用了乱七八糟的丑陋的 while 循环。

这意味着 Ramda 的实现不能作为如何编写功能良好的 JavaScript 的模型(模板)。这太糟糕了。但它是目前的 JavaScript 引擎最实用的一种选择(方案)。

合理的 API

Ramda 还试图就 API 中应该包含什么做出实用的选择。我们并没有试图移植 Clojure、Haskell 或任何其他函数式语言中的任何特定的函数子集,也没有试图模仿更成熟的 JavaScript 库或规范的 API。我们采纳函数的标准是,它们表现出合理的效用。当然,它们也必须与我们的函数式范式相契合才会被考虑,但这还不够;我们必须确信它们将会被用到,并且它们提供了通过当前函数不容易实现的价值。

后者是比较棘手的。有一个平衡的方案,以确定什么情况下语法糖是可以接受的。在之前,我们讨论了 compose 有一个执行顺序相反孪生同胞 pipe。有一种观点认为这是一种浪费,我们不应该把 API 因为这些多余的函数而搞乱。毕竟,

1
R.pipe(fn1, fn2, ..., fnN)

可以重写为如下形式:

1
R.apply(R.compose, R.reverse([fn1, fn2, ..., fnN]));

但是,我们确实选择将 pipe 以及其他一些看似多余的函数包含到其中,当它们符合下面的条件时:

  • 很有可能会被用到
  • 能更好的表达开发人员的意图
  • 足够简单的实现

整洁且一致的 API

对于整体一致 API 的追求,听起来不像是一个现实的考虑,更像是一个纯粹主义者的目标。但事实上,提供简单而一致的 API 使得 Ramda 更易于使用。例如,一旦你习惯了 Ramda 对参数顺序的设定,你将很少需要查阅文档以确定如何构建你的调用。

另外,Ramda 坚决反对可选参数。这个决定有助于形成非常整洁的 API。一个函数应该做什么以及如何调用,通常是非常直观的。

并没有 “什么会帮助我” 的建议

最后,向某个人解释这个问题通常是最困难的,那就是一个用户对什么才是实用的概念与整个库的实用性实际上可能只有一点点关系。即使提出的函数有助于解决某个难题,如果问题太过狭隘,或者解决方案偏离了我们的基础哲学,那么它也不会被纳入到 Ramda 中。虽然实用性是在旁观者眼中反映出来的,但那些能够纵观整个库的旁观者会有一个宏观的不同的视野,只有那些能够在整体上提升 Ramda 的改变才会被采纳。

结论:生而不同

Ramda 的诞生是因为,没有任何其他的库能以我们想要的方式工作。我们想要将可以作用于不可变数据的小型可组合函数,组合成简洁的函数式的 pipeline (管道)。当 Ramda 与类似的库相比较时,这涉及到一些似乎颇具争议的决定。我们并不担心这一点。Ramda 为我们工作的很好,似乎也满足了社区的需求

我们不再孤单。自从我们开始以来,FKit 也萌发了相似的想法。这是一个不太成熟的库,它的工作方式和 Eweda 一样,试图在 API 及其实现上同时保持真正的优雅。在我看来,他们很可能会遇到性能瓶颈。但是,我们无能为力,只能祝福他们。

Ramda 正在努力坚持它作为 “JavaScript 开发人员的实用的函数式库” 的座右铭。我们认为我们正在管理和维护 Ramda。但我们也很乐意倾听 您的想法。

函数式编程中的函数有三种不同的解读方式,分别为纯函数、高阶函数和一等函数。本文分别对这三者的概念、应用和联系进行详解。

纯函数

定义:

  1. 相同的输入必定产生相同的输出;
  2. 在计算的过程中,不会产生副作用。

满足上述两个条件,我们就说该函数是纯函数。

纯函数也即数学意义上的函数,表达的是数据之间的转换(映射)关系,而非计算步骤的详述。数学函数的定义:

函数通常由定义域 X 、值域 Y 以及定义域到值域的映射 ff: X -> Y)组成。

function

纯函数让我们对写出的函数具有完全的控制能力。纯函数的结果 必须 只依赖于输入参数,不受外部环境的影响;同时纯函数在计算结果的过程中,也不会影响(污染)外部环境,即不会产生副作用。

函数组合

纯函数定义中的两个条件保证了它(的计算过程)与外界是完全隔离,这也是函数组合的基础。

只有函数组合中的所有函数都是纯函数,我们组合起来的新函数才会是纯函数。我们可以对使用纯函数组合出来的新函数从数学上证明(推导)其正确性,而无需借助大量的单元测试。

只要在函数组合时引入一个非纯函数,整个组合出来的函数将沦为非纯函数。如果将函数组合比作管道的拼接,只要组成管道的任何一小节有泄露或者外部注入,我们便失去了对整条管道的完全控制。

pipeline-leaking

要想实现函数组合,还需要满足连续性,描述如下:

因为纯函数可以看作定义域到值域映射,待组合的函数中的上一个函数的值域须等于下一个函数的定义域,也即上一个函数的输出(类型)等于下一个的输入(类型)。

假设有两个函数:f: X -> Yg: Y -> Z,只有 codomain(f) = domain(g) 时,fg 才可以组合。

function_composition

引用透明及缓存

在不改变整个程序行为的情况下,如果能将其中的一段代码替换为其执行的结果,我们就说这段代码是引用透明的。

因此,执行一段引用透明的代码(函数),对于相同的参数,总是给出相同的结果。我们也称这样的函数(代码)为纯函数。

引用透明的一个典型应用即函数缓存。我们可以将已经执行过的函数输入值缓存起来,下次调用时,若输入值相同,直接跳过计算过程,用缓存结果代替计算结果返回即可。

函数缓存的实现依赖于闭包,而闭包的实现又依赖于高阶函数,高阶函数的实现又依赖于一等函数。我们按照这条依赖链,从里往外依次对它们进行讲解。

一等函数(First Class Functions)

程序语言会对基本元素的使用方式进行限制,带有最少限制的元素被称为一等公民,其拥有的 “权利” 如下:

  • 可以使用变量命名;
  • 可以提供给函数作为参数;
  • 可以由函数作为结果返回;
  • 可以包含在数据结构中;

乍一看,我们应该首先会想到程序中的基本数据结构(如 number、array、object 等)是一等公民。如果函数也被视为一等公民,我们便可以像使用普通数据一样对其使用变量命名,作为参数或返回值使用,或者将其包含在数据结构中。在这里函数和数据的边界开始变得不再那么分明了。函数被视为一等公民后,其能力和适用范围被大大扩展了。

下面使用 JavaScript 对上面第一条和第四条 “权利” 进行讲解。第二、三条与高阶函数密切相关,将放到下一节的高阶函数中讲解。

使用变量命名

1
const square = x => x * x

上面代码定义了一个求平方值的函数,并将其赋给了 square 变量。

可以包含在数据结构中

Ramda 中有一个API:evolve,其接受的首个参数便是一个属性值为函数的对象。evolve 函数会递归地对 “待处理对象” 的属性进行变换,变换的方式由 transformation 内置函数属性值的对象定义。示例如下(示例中的 R.xxx 都是 Ramda 中的API,相关API的功能可以参考Ramda文档):

1
2
3
4
5
6
7
8
var tomato  = {name: 'Tomato', data: {elapsed: 100, remaining: 1400}, id:123};
var transformations = {
name: R.toUpper,
data: {elapsed: R.add(1), remaining: R.add(-1)}
};

R.evolve(transformations)(tomato);
//=> {name: 'TOMATO', data: {elapsed: 101, remaining: 1399}, id:123}

高阶函数

定义:

使用函数作为输入参数,或者返回结果为函数的函数,被称为高阶函数。

作为参数或返回值的函数,是一等函数的应用之一。高阶函数以一等函数作为基础,只有支持一等函数的语言才能进行高阶函数编程。

以熟悉的 filter 函数为例,我们可以用 filter 对列表中的元素进行过滤,筛选出符合条件的元素。filter 的类型签名和示例代码如下:

1
filter :: (a → Boolean) → [a] → [a]
1
2
3
4
5
const isEven = n => n % 2 === 0;

const filterEven = R.filter(isEven);

filterEven([1, 2, 3, 4]); //=> [2, 4]

filter 接受一个判断函数(判断输入值是否为偶数)isEven,返回一个过滤出偶数的函数 filterEven

闭包

定义:

闭包是由函数及该函数捕获的其上下文中的自由变量组成的记录

举例讲:

1
2
3
4
5
6
7
8
9
10
11
function add(x) {
const xIn = x;
return function addInner(y) {
return xIn + y;
}
}
const inc = add(1);
inc(8); //=> 9;

const plus2 = add(2);
plus2(8); //=> 10;

上述代码中返回的函数 addInner 及由其捕获的在其上下文中定义的自由变量 xIn,便组成了一个闭包。

closure

上述代码中最外层的 add 函数是一个高阶函数,其返回值为一等函数 addInner

其实 add 函数的参数 x 也是 addInner 上下文的一部分,所以 ‘xIn’ 也就没有存在的必要了,add 代码优化如下:

1
2
3
4
5
function add(x) {
return function addInner(y) {
return x + y;
}
}

借助于箭头函数,我们可以进一步优化 add 的实现:

1
const add = x => y => x + y

是不是非常简洁?由此我们可以一窥函数式编程强大的表达能力。

闭包主要用来做数据缓存,而数据缓存应用非常广泛:包括函数工厂模式、模拟拥有私有变量的对象、函数缓存、还有大名鼎鼎的柯里化。

其实上述代码中 add 函数便是柯里化形式的函数。

上述代码中的 const inc = add(1);const plus2 = add(2); 是一种函数工厂模式,通过向 add 函数传入不同的参数,便会产生功能不同的函数。函数工厂可以提高函数的抽象和复用能力。

例如我们有一个如下形式的 Ajax 请求函数:

1
2
3
4
5
6
7
const ajax = method => type => query => { ... };

const get = ajax('GET');
const post = ajax('POST');

const getJson = get('json');
const getHtml = ajax('GET')('text/html') = get('text/html');

我们抽象出了最一般的 ajax 请求函数,在具体应用时,我们用能通过函数工厂生产出作用不同的函数。

通过上面几个小节,我们讲解了纯函数(数学意义上的函数)、一等函数、高阶函数,还有闭包。

下面通过一个集上述所有概念于一身的 函数缓存 ,来结束函数式编程中的 “函数们” 的论述。

函数缓存 memoize

函数实现:

1
2
3
4
5
6
7
8
const memoize = pureFunc => {
const cache = {};
return function() {
const argStr = JSON.stringify(arguments);
cache[argStr] = cache[argStr] || pureFunc.apply(pureFunc, arguments);
return cache[argStr];
};
};

memoize 的功能是对传入函数 pureFunc 进行缓存,返回缓存版本的 pureFunc。当我们使用参数调用缓存的函数时,缓存的函数会到 cache 中查找该参数是否被缓存过,如果有缓存,则不需要再次计算,直接返回已缓存值,否则对本次输入的参数进行计算,缓存计算的结果以备后用,然后将结果返回。

memoize 只有对纯函数的缓存才有意义。因为纯函数是引用透明的,其输出只依赖于输入,并且计算过程不会影响外部环境。

举一个极端的例子,假如我们有一个随机数字生成函数 random(), 如果对其进行了缓存:

1
const memoizedRandom = memoize(random);

memoizedRandom 除了第一次生成一个随机值外,随后的调用都返回第一次缓存的值,这样就失去了 random 的意义。再假如,我们对终端字符输入函数 getchar() 进行了缓存,每次调用都会是第一次获取的字母。

memoize 内部实现了一个闭包的创建。返回的缓存函数和自由变量 cache 共同构成了一个闭包。自由变量 cached 用于对已经计算过的数据(参数)的缓存。而闭包本身是由高阶函数和一等函数实现的。

functions-in-memoize

总结

本文对函数式编程中的 “函数们” 做了详细解释:纯函数、一等函数、高阶函数,并展示了它们的应用。其中纯函数是函数组合的基础;一等函数是高阶函数的实现基础,一等函数和高阶函数又是闭包的实现基础。

最后通过函数缓存函数 memoize 将纯函数、一等函数、高阶函数和闭包联系了起来,用函数式编程中的 “函数们” (函数式三镖客)的一次 “联合行动” 结束本文。

参考文档

What is a Function?.

Functional Programming.

Referential Transparency.