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 踩坑指南

安装主题

我选用 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.

本文首先介绍了递归的定义、实质、满足条件等,然后利用 Ramda API 和 Spread & Rest 操作符对递归进行实例讲解。

递归的理论基础

递归的定义:一种直接或者间接调用自身的过程。

递归的实质:将待求解问题分解成规模缩小的同类子问题,然后递归调用方法来表示问题的解。是一个不断将问题拆分然后组合的过程。

递归的过程:“能进则进,不进则退”。

递归问题需满足的条件:

  1. 一个(或多个)基本情景 – 一个不使用递归而产生结果的终止情景;
  2. 一组可以将所有其他情景归约至基本情景的规则。

递归的条件归结为:一个终止条件和一组归约的规则。

递归能让我们写出非常简洁、直观的代码,但简洁并不等于简单(容易)。递归是一种从总体到局部的思维过程,与传统的命令式的思维方式差异较大。命令式思维要求显示地提供详细的求解步骤,而递归(或者函数式)要求用概括性的语言对问题进行描述,问题的描述本身就是编程的整个过程,这要求我们具有很强的抽象思维和逻辑推理能力。

递归的编程模式:

  1. 定义终止条件;
  2. 编写递归的归约规则。

但如果语言中实现了惰性求值,也可以不定义终止条件,比如递归生成无限长的序列。

以上便是递归的理论解释,下面我们通过一些实例对递归进行实际演示。

递归的实践

递归能够很好地处理列表和树形数据结构的很多问题。很多时候,我们解决问题的模式就是对不断缩小的列表或树反复做同一件事情。实际上,列表和树本身的构造也可以看做递归的过程。

列表可以看作有列表首元素(头部)和其余元素(尾部)的组合,如下所示:

1
2
var list = R.prepend(1, R.prepend(2, R.prepend(3, []))); // => [1, 2, 3]
var list = [1, ...[2, ...[3, ...[]]]];

本文中我们使用 ramda 和 expect npm库进行演示:

1
2
const R = require('ramda');
const expect = require('expect');

快速排序

下面以经典的快速排序为例开始递归算法的演示。快排的定义为:所有小于头部的元素(它们也需要排序)在先,大于头部的元素(它们也需要排序)在后,终止条件为空数组。

首先我们用模式匹配 R.cond 的方式来进行递归排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var quickSort = R.cond(
[R.isEmpty, R.always([])],
[
R.T,
xs => {
const head = R.head(xs);
const tail = R.tail(xs);
return R.compose(quickSort, R.filter(R.lte(R.__, head)))(tail)
.concat(head)
.concat(R.compose(quickSort, R.filter(R.gte(R.__, head)))(tail));
}]
);

expect(quickSort([5, 1, 9, 4, 6, 7, 3])).toEqual([1, 3, 4, 5, 6, 7, 9]);

快排的过程如下所示(借用 Learn You a Haskell for Great Good! 的图):

quicksort

如果只有一个 if-else 对,我们也可以使用 R.ifElse 来代替 R.cond。另外,我们还可以用 R.whenR.unless 来进一步简化 R.ifElse

使用 Ramda API 的递归规则还是有些冗长,我们可以使用 ES6 中的 Spread & Rest 操作符和 R.unless 对列表的头部元素和其他尾部元素的提取和组合进行简化,如下所示:

1
2
3
4
5
6
7
8
9
10
var quickSort = R.unless(
R.isEmpty,
([head, ...tail]) => [
...R.compose(quickSort, R.filter(R.lte(R.__, head)))(tail),
head,
...R.compose(quickSort, R.filter(R.gte(R.__, head)))(tail),
]
);

expect(quickSort([5, 1, 9, 4, 6, 7, 3])).toEqual([1, 3, 4, 5, 6, 7, 9]);

斐波那契序列

下面展示的是斐波那契序列, 斐波那契序列的数学表达式如下所示:

  • f(0) = 0
  • f(1) = 1
  • f(n) = f(n-1) + f(n-2)

以序列元素值为半径的斐波那契螺旋线如下所示:

fibonacci

代码实现如下:

1
2
3
4
5
6
var fib = n => R.unless(
R.contains(R.__, [0, 1]),
() => fib(n - 1) + fib(n - 2)
)(n);

expect(fib(6)).toEqual(8);

map、filter、reduce

mapfilterreduce 作为函数式编程中处理列表的三个基本函数,在底层实现或者演示时,一般会采用命令式的 for 循环迭代来实现,既然列表本身可以看作递归结构,我们就用递归来尝试实现函数式编程中处理列表的 “三镖客”。

首先以 map 为例,从命令式 for 循环实现开始,然后是递归实现,并在对递归实现的优化中展示函数式编程(或者说 Ramda 库)灵活、多变而又优雅的编程方式,体会什么才是真正的编程之美。

map 的 Hindley-Milner 类型签名如下所示:

1
map :: Functor f => (a → b) → f a → f b

其实 mapFunctor 的一个函数,Functor 是具体范畴之间的映射(态射),关于范畴、Functor、态射等函数式的概念不在本次的讨论范围之内,感兴趣的同学可以自己查看相关资料。在这里,可以将 Functor 看做是列表(列表是 Functor 的一个实例),map 看作不同类型列表之间的映射,将源列表中的元素进行转换,生成一个包含映射后元素的新列表。

map 列表形式的类型签名如下:

1
map :: (a → b) → [a] → [b]

map 命令式实现:

1
2
3
4
5
6
7
8
9
var map = (fn, list) => {
var newList = [];
for (let i = 0; i < list.length; i++) {
newList[i] = fn(list[i]);
}
return newList;
};

expect(map(R.multiply(2), R.range(1, 5))).toEqual([2, 4, 6, 8]);

map 递归实现1,三目运算符版:

1
2
var mapR = (fn, list) =>
(R.isEmpty(list) ? list : R.prepend(fn(R.head(list)), mapR(fn, R.tail(list))));

map 递归实现2,模式匹配(R.cond)版:

1
2
3
4
var mapR2 = (fn, list) => R.cond([
[R.isEmpty, R.identity],
[R.T, R.converge(R.prepend, [R.compose(fn, R.head), xs => mapR2(fn, R.tail(xs))])],
])(list);

map 递归实现3,unless(R.unless)版:

1
2
3
4
var mapR3 = (fn, list) => R.unless(
R.isEmpty,
R.converge(R.prepend, [R.compose(fn, R.head), xs => mapR3(fn, R.tail(xs))])
)(list);

map 递归实现4,Spread Operator + unless 版:

1
2
3
4
var mapR5 = (fn, xs) => R.unless(
R.isEmpty,
([head, ...tail]) => [fn(head), ...mapR5(fn, tail)]
)(xs);

总结一下上述命令式、递归实现以及递归的优化实现的特点。

命令式实现需要显式写出内部的每步实现步骤,包括如何获取、变换和组合元素等;还使用了 mutable 的数据,包括全局的结果列表 newList 和 迭代索引 i。命令式的好处是符合正常的思维方式,但缺点是引入了较多冗余的 “噪声”,比如 for 循环、全局变量 newListi 都是辅助项,这些辅助项稍微增多,代码复杂度会显著上升,真正有用的信息便会淹没在了这些 “噪声” 里面。

反观递归式(函数式)的实现(如 mapSpead Operator + unless 版实现),我们只需要通过对问题的描述,便得到功能的实现。而描述的过程看似简单,实则内含了对问题本质的抽象和逻辑分析,对人们的思维能力要求更高。

再讲一下 Spead Operator 语法糖。很多人不屑于用语法糖,态度往往是:这不就是 xxx 的语法糖吗?没什么大不了的啊。但语法上的便利其实也是很重要的一方面,因为我们想要表达的思想是要由语法来承载的。

这里引用《函数式编程思维》中的一段话:

我跟 Martin Fowler 在巴塞罗那的一辆出租车上有过一次记忆深刻的讨论,我们聊的是 Smalltalk 的衰落和 Java 的兴盛。Fowler 在这两种语言上都有很深厚的积累,他说,起初 他觉得从 Smalltalk 到 Java 的变化只是一些语法上的不便,结果却发现被阻碍的还有原先 语言所承载的思维方式。在语法处处掣肘下塑造出来的抽象,很难配合我们的思维过程而 不产生无谓的摩擦。

下面对 filterreduce 的递归实现做一个展示。

filter 的递归实现如下:

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
var filterR = (pred, list) =>
R.isEmpty(list) ? list : pred(R.head(list)) ? R.prepend(R.head(list), filterR(pred, R.tail(list))) : filterR(pred, R.tail(list));

var filterR2 = (pred, list) => R.cond([
[R.isEmpty, R.identity],
[R.compose(pred, R.head), R.converge(R.prepend, [R.head, xs => filterR2(pred, R.tail(xs))])],
[R.T, xs => filterR2(pred, R.tail(xs))],
])(list);

var filterR3 = (pred, list) => R.unless(
R.isEmpty,
R.converge(R.concat, [R.compose(R.ifElse(pred, R.of, R.always([])), R.head), xs => filterR3(pred, R.tail(xs))])
)(list);

var filterR3 = (pred, list) => R.unless(
R.isEmpty,
R.converge(R.concat, [R.compose(R.ifElse(pred, R.of, R.always([])), R.head), xs => filterR3(pred, R.tail(xs))])
)(list);

var filterR4 = (pred, list) => R.unless(
R.isEmpty,
([head, ...tail]) => [...(pred(head) ? [head] : []), ...filterR4(pred, tail)]
)(list);

expect(filterR(a => a % 2, R.range(1, 10))).toEqual([1, 3, 5, 7, 9]);

reduce 的递归实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var reduceR = (fn, acc, list) =>
(R.isEmpty(list) ? acc : reduceR(fn, fn(acc, R.head(list)), R.tail(list)));

var reduceR2 = (fn, acc, list) => R.cond([
[R.isEmpty, () => acc],
[R.T, xs => reduceR2(fn, fn(acc, R.head(xs)), R.tail(xs))],
])(list);

var reduceR3 = (fn, acc, list) => R.ifElse(
R.isEmpty,
() => acc,
xs => reduceR3(fn, fn(acc, R.head(xs)), R.tail(xs))
)(list);

expect(reduceR(R.add, 0, R.range(1, 5))).toEqual(10);

总结

本文从递归的定义、实质等基本理论开始,然后是递归的一些实例,其中大量运用了 ramda 函数式编程库中的 API,既是对递归的演示,也是对 ramda API 的实践展示。展示了递归和函数式在编程时的强悍的表达能力和对极致简约的追求。

Ramda API 已经快到 300 个了,对每个 API 按类型分别进行简介,当作 Ramda 的一份 CheatSheet 。

列表

Action Function
列表转换 map
列表过滤:过滤出符合条件的元素 filter
列表过滤:过滤掉符合条件的元素 reject
列表折叠:从左向右对所有元素依次归约(折叠) reduce
列表折叠:从右向左对所有元素依次归约(折叠) reduceRight
列表折叠(增强版) transduce
列表去重 uniq
列表去重:对处理后的元素做相等性判断 uniqBy
列表去重:通过断言函数(predicate)判断 uniqWith
列表排序 sort
列表翻转 reverse
列表拼接 concat
列表长度 length
列表表头拼接元素 prepend
列表表尾拼接元素 append
更新指定索引处的值 adjust
替换指定索引处的值 update
将列表元素转换为其指定的属性值,等价于 R.map(R.prop) pluck
为列表迭代函数添加两个参数:索引和整个列表 addIndex
取出特定索引范围内的元素 slice
将列表通过分隔符拼接成字符串 join
取出第 N 个元素 nth
取出前 N 个元素 take
取出后 N 个元素 takeLast
从前往后取出满足条件的元素,直至不满足条件的首个元素止 takeWhile
从后向前取出满足条件的元素,直至不满足条件的首个元素止 takeLastWhile
删除前 N 个元素 drop
删除后 N 个元素 dropLast
从前往后删除满足条件的元素,直至不满足条件的首个元素止 dropWhile
从后向前删除满足条件的元素,直至不满足条件的首个元素止 dropLastWhile
取出首个元素 head
取出末尾元素 last
取出前 length - 1 个元素(删除末尾元素) init
取出后 length - 1 个元素(删除首个元素) tail
求差集:{a∣a∈xs ∩ a∉ys} difference
求差集:{a∣a∉xs ∩ a∈ys} without
求差集:根据条件计算第一个列表与第二个列表的差集 differenceWith
求对称差集:{(xs ∪ ys) - (xs ∩ ys)} symmetricDifference
求对称差集:根据条件计算所有不属于两个列表交集的元素 symmetricDifferenceWith
求交集:{xs ∩ ys} intersection
求交集:从 xs 中挑选出在 ys 中符合条件的元素 innerJoin
求并集:{xs ∪ ys} union
求并集:根据条件判断两元素是否重复 unionWith
查找列表中首个满足条件的元素 find
查找列表中首个满足条件的元素的索引 findIndex
查找列表中最后一个满足条件的元素 findLast
查找列表中最后一个满足条件的元素的索引 findLastIndex
查找给定元素在列表中首次出现时的索引 indexOf
查找给定元素在列表中末次出现时的索引 lastIndexOf
列表判断:判断元素是否包含在列表中 contains
列表判断:判断是否列表中所有元素都满足条件 all
列表判断:判断是否列表中所有元素都不满足条件 none
列表判断:判断是否列表中存在满足条件的元素 any
列表判断:判断列表是否以给定的值开头 startsWith
列表判断:判断列表是否以给定的值结尾 endsWith
列表分组:按是否符合条件,将元素分为两组 partition
列表分组:对列表中元素按指定规则分组 groupBy
列表分段:对列表中元素按指定规则分段 groupWith
列表分组:对列表中元素按指定规则分组折叠 reduceBy
列表分割:在指定索引处 splitAt
列表分割:每隔 N 个元素 splitEvery
列表分割:按条件分割 splitWhen
对两个列表相同位置的元素进行组合 zip
对两个列表相同位置的元素进行键值对组合,fromPairs ∘ zip zipObj
对两个列表相同位置的元素按规则进行组合 zipWith
由一系列键值对列表创建对象 fromPairs
列表彻底扁平化 flatten
列表单层扁平化 unnest
先对列表内元素做 Kleisli 映射,再做扁平化,flatMap,>>= chain
函子间的自然变化? sequence
列表插入 insert
将子列表插入列表 insertAll
在列表元素之间插入分割元素 intersperse
列表转换 + 折叠? into
将 reduce 的迭代过程记录下来 mapAccum
将 reduceRight 的迭代过程记录下来 mapAccumRight
合并多个对象 mergeAll
由两个参数组成列表 pair
从 reduce 或 transduce 中提前退出迭代时的值 reduced
可以提前退出的 reduce 迭代 reduceWhile
列表生成:生成左闭右开的升序数字列表 range
列表生成:生成含有 N 个同一元素的列表 repeat
列表生成:函数执行 N 次,生成 N 元列表 times
列表生成:通过迭代函数生成列表 unfold
二维列表行列式转换 transpose
二维列表生成 xprod

函数

Action Function
函数组合:纵向,从右往左 compose
函数组合:纵向,从左往右 pipe
函数组合:纵向 o
函数组合:横向 converge
函数组合:横向 useWith
Kleisili 函数组合 composeK
Kleisili 函数组合 pipeK
Promise 函数组合 composeP
Promise 函数组合 pipeP
单位函数:输出等于输入 identity
函数柯里化 curry
N 元函数柯里化 curryN
将柯里化函数 转为 N 元函数 uncurryN
柯里化函数的参数占位符 __
参数部分调用:从左往右 partial
参数部分调用:从右往左 partialRight
函数缓存 memoize
函数缓存:可以自定义缓存键值 memoizeWith
只执行一次的函数 once
创建返回恒定值的函数 always
恒定返回 true 的函数 T
恒定返回 false 的函数 F
Applicative Functor 的 ap 方法,<*> ap
将函数作用于参数列表 apply
将接受 单列表参数 的函数转为接受 普通参数列表 的函数 unapply
将首个参数(函数)作用于其余参数 call
绑定函数上下文 bind
利用属性值为函数的对象生成同构对象 applySpec
将函数列表作用于参数列表 juxt
将给定值传给给定函数,CPS: flip($) applyTo
比较函数,一般用于排序 comparator
升序比较函数 ascend
降序比较函数 descend
将函数封装为 N 元函数 nArg
将函数封装为一元函数 unary
将函数封装为二元函数 binary
提取第 N 个参数 nthArg
将构造函数封装为普通函数,创建实例时,不需要 new 操作符 construct
将构造函数封装为 N 元普通函数,创建实例时,不需要 new 操作符 constructN
通过函数名调用函数 invoker
创建相应类型的空值 empty
判断是否为空值 isEmpty
交换函数前两个参数的位置 flip
函数提升 lift
N 元函数提升 liftN
生成单元素列表 of
输出等于输入,但产生副作用的函数,一般用于调试 tap
异常捕获 tryCatch

对象

Action Function
属性设置 assoc
属性按路径设置 assocPath
属性删除 dissoc
属性按路径删除 disscoPath
获取属性值 prop
获取属性值,带有默认值 propOr
获取路径上的属性值 path
获取路径上的属性值,带有默认值 pathOr
判断属性是否满足给定的条件 propSatisfies
判断属性是否与给定值相等 propEq
判断两个对象指定的属性值是否相等 eqProps
判断路径上的属性值是否满足给定的条件 pathSatisfies
判断路径上的属性值是否与给定值相等 pathEq
获取属性值组成的列表 props
判断属性是否为给定类型 propIs
判断多个属性是否同时满足给定的条件 where
判断多个属性是否等于给定对应属性值 whereEq
删除多个属性 omit
提取多个属性 pick
提取多个属性 pickAll
对列表中元素提取多个属性,模拟 SQL 的 select project
提取键值满足条件的属性 pickBy
对特定属性进行特定变换 evolve
是否包含指定的键 has
是否包含指定的键:包括原型链上的键 hasIn
键值对换位 invertObj
键值对换位:将值放入数组中 invert
取出所有的键 keys
取出所有的键:包括原型链上的键 keysIn
取出所有的值 values
取出所有的值:包括原型链上的值 valuesIn
透镜:包括属性的 getter 和 setter lens
透镜:指定索引的透镜 lensIndex
透镜:指定路径的透镜 lensPath
透镜:指定属性的透镜 lensProp
透镜:对被 lens 聚焦的属性做变换 over
透镜:对被 lens 聚焦的属性进行设置 set
透镜:读取被 lens 聚焦的属性值 view
Objectmap,转换函数参数:(value, key, obj) mapObjIndexed
对象合并 merge
对象合并:对重复的属性值按给定规则合并 mergeWith
对象合并:对重复的属性值按给定规则合并 mergeWithKey
对象深递归合并:以左侧对象属性为主 mergeDeepLeft
对象深递归合并:以右侧对象属性为主 mergeDeepRight
对象深递归合并:对重复的非对象类型的值按给定规则合并 mergeDeepWith
对象深递归合并:对重复的非对象类型的值按给定规则合并 mergeDeepWithKey
创建包含单个键值对的对象 objOf
将对象键值对转换为元素为键值二元组的列表 toPairs
将对象键值对转换为元素为键值二元组的列表:包括原型链上的键 toPairsIn
将二元组的列表转换为对象 fromPairs

逻辑运算

Action Function
判断是否满足所有条件 allPass
判断是否满足任一条件 anyPass
判断是否同时满足两个条件 both
判断是否满足两个条件中的任意一个 either
逻辑与操作 and
逻辑或操作 or
模式匹配,相当于多个 if/else cond
单个 if/elsecond 的特例 ifElse
满足条件,则执行处理函数,否则原样返回输入值,ifElse 的特例 when
不满足条件时,执行处理函数,否则原样返回输入值,ifElse 的特例 unless
逻辑非操作,参数为布尔值 not
对函数返回值取反 complement
添加默认值 defaultTo
一直计算,直到满足给定条件 until
判断给定值是否为该类型的空值 isEmpty
判断给定值是否为 nullundefined isNil
返回给定值所属类型的空值 empty

关系运算

Action Function
等于 equals
完全相等 identical
通过规则判断是否相等 eqBy
大于 gt
大于等于 gte
小于 lt
小于等于 lte
限定有序数据类型的范围 clamp
求两个数的较大值 max
按规则求两个数的较大值 maxBy
求两个数的较小值 min
按规则求两个数的较小值 minBy
求差集:{a∣a∈xs ∩ a∉ys} difference
求差集:{a∣a∉xs ∩ a∈ys} without
求差集:根据条件计算第一个列表与第二个列表的差集 differenceWith
求对称差集:{(xs ∪ ys) - (xs ∩ ys)} symmetricDifference
求对称差集:根据条件计算所有不属于两个列表交集的元素 symmetricDifferenceWith
求交集:{xs ∩ ys} intersection
求交集:从 xs 中挑选出在 ys 中符合条件的元素 innerJoin
求并集:{xs ∪ ys} union
求并集:根据条件判断两元素是否重复 unionWith

数学运算

Action Function
add
subtract
multiply
divide
加1 inc
减1 dec
取反 negate
列表累加和 sum
列表累乘积 product
列表平均值 mean
列表中位数 median
取模:算术 mathMod
取模:JS modulo

类型操作

Action Function
类型判断 is
类型描述 type
属性类型判断 propIs
判断是否为 nullundefined isNil

参考资料

关于 Applicative Functor,及其操作符 R.ap (haskell 中的 <*>) 和 lift (haskell 中的 <$>) 的论述可参考下列资料:

  1. 《Learn You A Haskell For Great Good》的 Applicative 章节,函数 (->) r 也是 Applicative,需要深入理解。
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
Function :: ((->) r)
Function a = ((->) r) a
= r -> a

;; FunctorApplicativeMonad 的类定义

class Functor f where
fmap :: (a -> b) -> f a -> f b

class (Functor f) => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b

class Monad m where
return :: a -> m a

(>>=) :: m a -> (a -> m b) -> m b

(>>) :: m a -> m b -> m b
x >> y = x >>= \_ -> y

fail :: String -> m a
fail msg = error msg

;; 作为FunctorApplicativeMonad 的实例的 Function 的定义,可以对比上面的类定义查看
instance Functor ((->) r) where
fmap = (.)

fmap :: (a -> b) -> (c -> a) -> (c -> b)

f :: a -> b

g :: c -> a
h :: c -> b

const h = fmap(f, g) = map(f, g) = compose(f, g)

instance Applicative ((->) r) where
pure x = (\_ -> x)
f <*> g = \x -> f x (g x)

f <*> g = (r->(a->b)) -> (r->a) -> (r->b)
= (r->a->b) -> (r->a) -> r -> b

instance Monad ((->) r) where
return x = \_ -> x
h >>= f = \w -> f (h w) w
  1. 《JS 函数式编程指南》

    1. 第 10 章: Applicative Functor
    2. 第 10 章: Applicative Functor lift
  2. stackoverflow: function as Functor/Applicative Functor/Monad:

    1. confused about function as instance of Functor in haskell
    2. functions as applicative functors (Haskell / LYAH)
    3. Function as an Instance of Monad
  3. Functions as Functors

What are typeclasses?

Typeclasses define a set of functions that can have different implementations depending on the type of data they are given.