第十二章:穿越顽石
到目前为止,在我们的“容器马戏团”(cirque du conteneur)里,你已经看到我们驯服了凶猛的函子(functor),让它屈服于我们的意志,执行任何我们心血来潮的操作。你曾对使用函数应用(application)同时处理(杂耍般地)多种危险效果(effects)并收集结果的表演眼花缭乱。曾惊讶于容器通过压平(joining)在一起而凭空消失。在副作用(side effect)的余兴表演中,我们看到它们被组合(composed)成一个。而最近,我们更是超越了自然,在你眼前将一种类型转换(transformed)成了另一种。
现在,我们的下一个戏法,将聚焦于遍历(traversals)。我们将看到类型(type)像空中飞人一样互相飞跃,同时保持我们的值(value)完好无损。我们将像旋转飞车里的吊舱一样重排效果。当我们的容器(container)像柔术演员的四肢一样缠绕在一起时,我们可以使用这个接口(interface)来理顺它们。我们将见证不同顺序带来的不同效果。拿上我的灯笼裤和滑哨,我们开始吧。
类型套类型
让我们来点不一样的:
// readFile :: FileName -> Task Error String
// firstWords :: String -> String
const firstWords = compose(intercalate(' '), take(3), split(' '));
// tldr :: FileName -> Task Error String
const tldr = compose(map(firstWords), readFile);
map(tldr, ['file1', 'file2']);
// [Task('拥护君主制'), Task('粉碎父权制')]
这里我们读取了一堆文件,最终得到了一个没什么用的任务(Task)数组。我们该如何 fork 其中的每一个呢?如果我们能把类型交换一下,得到 Task Error [String] 而不是 [Task Error String],那就太好了。这样,我们就能得到一个持有所有结果的未来值(future value),这比让几个未来值各自悠闲地到达,要更适合我们的异步需求。
这是最后一个棘手情况的例子:
// getAttribute :: String -> Node -> Maybe String
// $ :: Selector -> IO Node
// getControlNode :: Selector -> IO (Maybe (IO Node))
const getControlNode = compose(map(map($)), map(getAttribute('aria-controls')), $);
看看那些渴望聚合在一起的 IO。要是能 join(压平)它们,让它们紧密贴合,那该多好,可惜啊,一个 Maybe 像舞会上的监护人一样挡在它们中间。这里最好的做法是把它们的位置挪到一起,这样每种类型最终都能聚合在一起,我们的签名就可以简化为 IO (Maybe Node)。
类型风水
Traversable(可遍历)接口包含两个强大的函数:sequence 和 traverse。
让我们用 sequence 来重新排列类型:
sequence(List.of, Maybe.of(['the facts'])); // [Just('the facts')]
sequence(Task.of, new Map({ a: Task.of(1), b: Task.of(2) })); // Task(Map({ a: 1, b: 2 }))
sequence(IO.of, Either.of(IO.of('buckle my shoe'))); // IO(Right('buckle my shoe'))
sequence(Either.of, [Either.of('wing')]); // Right(['wing'])
sequence(Task.of, left('wing')); // Task(Left('wing'))
看到这里发生了什么吗?我们嵌套的类型就像在潮湿夏夜里的一条皮裤被翻了个底朝天。内层的函子被移到了外层,反之亦然。需要知道的是,sequence 对其参数有点挑剔。它看起来像这样:
// sequence :: (Traversable t, Applicative f) => (a -> f a) -> t (f a) -> f (t a)
const sequence = curry((of, x) => x.sequence(of));
让我们从第二个参数开始。它必须是一个持有应用函子(Applicative)的可遍历(Traversable)类型,这听起来限制性很强,但实际上这种情况经常出现。正是 t (f a) 被转换成了 f (t a)。这难道不形象吗?两种类型就像跳方块舞(do-si-do)一样互相交换位置,一目了然。那里的第一个参数仅仅是一个辅助手段,只在无类型语言中才需要。它是一个类型构造器(type constructor)(即我们的 of),提供它的目的是为了让我们能够反转像 Left 这样不情愿被 map 的类型——稍后会详细介绍。
使用 sequence,我们可以像街头玩猜球戏法的人那样精确地移动类型。但它是如何工作的呢?让我们看看像 Either 这样的类型是如何实现它的:
class Right extends Either {
// ...
sequence(of) {
// 如果 this.$value 是一个应用函子,我们可以 map Either.of 到它上面
return this.$value.map(Either.of);
}
}
啊哈,如果我们的 $value 是一个函子(实际上,它必须是一个应用函子),我们可以简单地 map 我们的构造函数来“跳房子”般地越过这个类型。
你可能已经注意到我们完全忽略了 of。它是在映射无效的情况下传入的,就像 Left 的情况一样:
class Left extends Either {
// ...
sequence(of) {
// Left 不持有应用函子,所以我们直接使用提供的 of 函数
return of(this);
}
}
我们希望类型最终总能处于相同的排列方式,因此,像 Left 这样实际上并不持有我们内部应用函子的类型,就需要一点帮助来做到这一点。Applicative 接口要求我们首先有一个 Pointed 函子(Pointed Functor),所以我们总会有一个 of 可以传入。在有类型系统的语言中,外部类型可以从签名中推断出来,不需要显式给出。
效果分类
对于我们的容器而言,不同的顺序会产生不同的结果。如果我有一个 [Maybe a],那它是一组可能存在的值;而如果我有一个 Maybe [a],那它是一个可能存在的集合值。前者表示我们会比较宽容,保留“好的那些”,而后者则意味着这是一个“要么全有,要么全无”的情况。同样地,Either Error (Task Error a) 可以代表客户端验证,而 Task Error (Either Error a) 可以代表服务器端验证。类型可以互换以产生不同的效果。
// fromPredicate :: (a -> Bool) -> a -> Either e a
// partition :: (a -> Bool) -> [a] -> [Either e a]
const partition = f => map(fromPredicate(f));
// validate :: (a -> Bool) -> [a] -> Either e [a]
const validate = f => traverse(Either.of, fromPredicate(f));
这里我们基于使用 map 还是 traverse 得到了两个不同的函数。第一个函数 partition 会根据谓词(predicate)函数给我们一个由 Left 和 Right 组成的数组。这对于保留有价值的数据以备将来使用很有用,而不是把它和洗澡水一起倒掉。而 validate 则会在第一个不满足谓词的项出现时给我们一个 Left,或者在所有项都符合要求(hunky dory)时给我们一个包含所有项的 Right。通过选择不同的类型顺序,我们得到了不同的行为。
让我们看看 List 的 traverse 函数,了解 validate 方法是如何实现的。
traverse(of, fn) {
return this.$value.reduce(
(f, a) => fn(a).map(b => bs => bs.concat(b)).ap(f),
of(new List([])),
);
}
这只是在列表上运行了一个 reduce。这个 reduce 函数是 (f, a) => fn(a).map(b => bs => bs.concat(b)).ap(f),看起来有点吓人,让我们逐步分析一下。
reduce(..., ...)记住
reduce的签名:reduce :: [a] -> (f -> a -> f) -> f -> f。第一个参数实际上是由$value上的点表示法提供的,所以它是一个列表。 然后我们需要一个函数,它接受一个f(累加器 (accumulator))和一个a(被迭代项 (iteree)),并返回一个新的累加器。of(new List([]))种子值(seed value)是
of(new List([])),在我们的例子中是Right([]) :: Either e [a]。注意Either e [a]也将是我们的最终结果类型!fn :: Applicative f => a -> f a如果将其应用于我们上面的例子,
fn实际上是fromPredicate(f) :: a -> Either e a。fn(a) :: Either e a
.map(b => bs => bs.concat(b))当值为
Right时,Either.map将 right 值传递给函数,并返回一个包含结果的新Right。在这种情况下,该函数有一个参数(b),并返回另一个函数(bs => bs.concat(b),其中b由于闭包(closure)而在作用域内)。当值为Left时,返回 left 值。fn(a).map(b => bs => bs.concat(b)) :: Either e ([a] -> [a])
.
ap(f)记住这里的
f是一个应用函子,所以我们可以将函数bs => bs.concat(b)应用于f中的任何值bs :: [a]。幸运的是,f来自我们的初始种子值,并且具有以下类型:f :: Either e [a],顺便说一句,当我们应用bs => bs.concat(b)时,这个类型得以保留。 当f是Right时,它会调用bs => bs.concat(b),返回一个将项添加到列表后的Right。当值为Left时,返回 left 值(分别来自上一步或上一次迭代)。fn(a).map(b => bs => bs.concat(b)).ap(f) :: Either e [a]
这个看似神奇的转换仅通过 List.traverse 中区区 6 行代码就实现了,并且是利用 of、map 和 ap 完成的,因此它适用于任何应用函子(Applicative Functor)。这是一个很好的例子,说明了这些抽象如何帮助我们编写高度通用的代码,而只需做出很少的假设(顺便说一句,这些假设可以在类型级别声明和检查!)。
类型的华尔兹
是时候回顾并清理我们最初的例子了。
// readFile :: FileName -> Task Error String
// firstWords :: String -> String
const firstWords = compose(intercalate(' '), take(3), split(' '));
// tldr :: FileName -> Task Error String
const tldr = compose(map(firstWords), readFile);
traverse(Task.of, tldr, ['file1', 'file2']);
// Task(['拥护君主制', '粉碎父权制']);
使用 traverse 而不是 map,我们成功地将那些不守规矩的 Task 归集成一个协调一致的结果数组。如果你熟悉的话,这就像 Promise.all(),但它不仅仅是一个一次性的自定义函数,不,它适用于任何可遍历(traversable)类型。这些数学化的 API 倾向于以可互操作、可重用的方式捕获我们想做的大多数事情,而不是让每个库都为单一类型重新发明这些函数。
让我们清理最后一个例子来收尾(不,不是闭包那种收尾):
// getAttribute :: String -> Node -> Maybe String
// $ :: Selector -> IO Node
// getControlNode :: Selector -> IO (Maybe Node)
const getControlNode = compose(chain(traverse(IO.of, $)), map(getAttribute('aria-controls')), $);
我们用 chain(traverse(IO.of, $)) 替代了 map(map($)),它在映射时反转了我们的类型,然后通过 chain 压平了两个 IO。
没有法律与秩序
好了,在你变得吹毛求疵、像敲法槌一样猛敲退格键想要退出本章之前,花点时间认识到这些定律(laws)是很有用的代码保证。我的猜想是,大多数程序架构的目标都是试图对我们的代码施加有用的限制,以缩小可能性,从而在设计者和阅读者层面引导我们找到答案。
没有定律的接口仅仅是一层间接调用。像任何其他数学结构一样,为了我们自己(心智健全)着想,我们必须公开其属性。这具有与封装(encapsulation)类似的效果,因为它保护了数据,使我们能够将接口替换为另一个遵纪守法(law abiding)的实现。
跟上,我们有一些定律需要弄清楚。
恒等定律 (Identity)
const identity1 = compose(sequence(Identity.of), map(Identity.of));
const identity2 = Identity.of;
// 用 Right 来测试一下
identity1(Either.of('stuff'));
// Identity(Right('stuff'))
identity2(Either.of('stuff'));
// Identity(Right('stuff'))
这应该很直观。如果我们将一个 Identity 放入我们的函子中,然后用 sequence 将其内外翻转,这与一开始就将其放在外部是相同的。我们选择 Right 作为试验品,因为它易于尝试和检验该定律。在那里使用任意函子是正常的,然而,在此处定律本身中使用具体的函子,即 Identity,可能会让人感到意外。回想一下,范畴(category)是由其对象(objects)之间的态射(morphisms)定义的,这些态射具有可结合的组合(composition)和恒等(identity)特性。当处理函子范畴时,自然变换(natural transformations)是态射,而 Identity 就是那个恒等。Identity 函子在演示定律方面与我们的 compose 函数一样基础。事实上,我们应该放弃挣扎,对我们的 Compose 类型也遵循同样的模式:
组合定律 (Composition)
const comp1 = compose(sequence(Compose.of), map(Compose.of));
const comp2 = (Fof, Gof) => compose(Compose.of, map(sequence(Gof)), sequence(Fof));
// 用我们手头现有的一些类型来测试一下
comp1(Identity(Right([true])));
// Compose(Right([Identity(true)]))
comp2(Either.of, Array)(Identity(Right([true])));
// Compose(Right([Identity(true)]))
正如所期望的,这条定律保持了组合性:如果我们交换函子的组合,我们不应该看到任何意外,因为组合本身也是一个函子。我们随意选择了 true、Right、Identity 和 Array 来进行测试。像 quickcheck 或 jsverify 这样的库可以通过对输入进行模糊测试(fuzz testing)来帮助我们测试定律。
作为上述定律的一个自然结果,我们获得了融合遍历(fuse traversals)的能力,从性能角度来看这很好。
自然性定律 (Naturality)
const natLaw1 = (of, nt) => compose(nt, sequence(of));
const natLaw2 = (of, nt) => compose(sequence(of), map(nt));
// 用一个随机的自然变换和我们友好的 Identity/Right 函子来测试。
// maybeToEither :: Maybe a -> Either () a
const maybeToEither = x => (x.$value ? new Right(x.$value) : new Left());
natLaw1(Maybe.of, maybeToEither)(Identity.of(Maybe.of('barlow one')));
// Right(Identity('barlow one'))
natLaw2(Either.of, maybeToEither)(Identity.of(Maybe.of('barlow one')));
// Right(Identity('barlow one'))
这与我们的恒等定律类似。如果我们先交换类型,然后在外部运行一个自然变换,这应该等同于先映射一个自然变换,然后再翻转类型。
这个定律的一个自然推论是:
traverse(A.of, A.of) === A.of;
同样,从性能的角度来看,这很好。
总结
Traversable 是一个强大的接口,它让我们能够像用意念操控的室内设计师一样轻松地重新排列类型。我们可以通过不同的顺序实现不同的效果,并抚平那些阻碍我们 join(压平)它们的讨厌的类型皱褶。接下来,我们将稍微绕道,去看看函数式编程乃至整个代数中最强大的接口之一:幺半群将一切聚合
练习
考虑以下元素:
// httpGet :: Route -> Task Error JSON
// routes :: Map Route Route
const routes = new Map({ '/': '/', '/about': '/about' });
// getJsons :: Map Route Route -> Task Error (Map Route JSON) const getJsons = traverse(Task.of, httpGet);
/* globals getJsons */
const throwUnexpected = () => {
throw new Error('The function gives incorrect results; a Task has resolved unexpectedly!');
};
const res = getJsons(routes);
assert(
res instanceof Task,
'The function has an invalid type; hint: `getJsons` must return a `Task`!',
);
res.fork(throwUnexpected, ($res) => {
assert(
$res.$value['/'] === 'json for /' && $res.$value['/about'] === 'json for /about',
'The function gives incorrect results; hint: did you correctly map `httpGet` over the Map\'s values?',
);
const callees = getJsons.callees; // eslint-disable-line prefer-destructuring
if (callees && callees[0] === 'map' && callees[1] === 'sequence') {
throw new Error('The function could be written in a simpler form; hint: compose(sequence(of), map(fn)) === traverse(of, fn)');
}
});
// NOTE We keep named function here to leverage this in the `compose` function,
// and later on in the validations scripts.
/* eslint-disable prefer-arrow-callback */
/* ---------- Internals ---------- */
function namedAs(value, fn) {
Object.defineProperty(fn, 'name', { value });
return fn;
}
// NOTE This file is loaded by gitbook's exercises plugin. When it does, there's an
// `assert` function available in the global scope.
/* eslint-disable no-undef, global-require */
if (typeof assert !== 'function' && typeof require === 'function') {
global.assert = require('assert');
}
assert.arrayEqual = function assertArrayEqual(actual, expected, message = 'arrayEqual') {
if (actual.length !== expected.length) {
throw new Error(message);
}
for (let i = 0; i < expected.length; i += 1) {
if (expected[i] !== actual[i]) {
throw new Error(message);
}
}
};
/* eslint-enable no-undef, global-require */
function inspect(x) {
if (x && typeof x.inspect === 'function') {
return x.inspect();
}
function inspectFn(f) {
return f.name ? f.name : f.toString();
}
function inspectTerm(t) {
switch (typeof t) {
case 'string':
return `'${t}'`;
case 'object': {
const ts = Object.keys(t).map(k => [k, inspect(t[k])]);
return `{${ts.map(kv => kv.join(': ')).join(', ')}}`;
}
default:
return String(t);
}
}
function inspectArgs(args) {
return Array.isArray(args) ? `[${args.map(inspect).join(', ')}]` : inspectTerm(args);
}
return (typeof x === 'function') ? inspectFn(x) : inspectArgs(x);
}
/* eslint-disable no-param-reassign */
function withSpyOn(prop, obj, fn) {
const orig = obj[prop];
let called = false;
obj[prop] = function spy(...args) {
called = true;
return orig.call(this, ...args);
};
fn();
obj[prop] = orig;
return called;
}
/* eslint-enable no-param-reassign */
const typeMismatch = (src, got, fn) => `Type Mismatch in function '${fn}'
${fn} :: ${got}
instead of
${fn} :: ${src}`;
const capitalize = s => `${s[0].toUpperCase()}${s.substring(1)}`;
const ordinal = (i) => {
switch (i) {
case 1:
return '1st';
case 2:
return '2nd';
case 3:
return '3rd';
default:
return `${i}th`; // NOTE won't get any much bigger ...
}
};
const getType = (x) => {
if (x === null) {
return 'Null';
}
if (typeof x === 'undefined') {
return '()';
}
if (Array.isArray(x)) {
return `[${x[0] ? getType(x[0]) : '?'}]`;
}
if (typeof x.getType === 'function') {
return x.getType();
}
if (x.constructor && x.constructor.name) {
return x.constructor.name;
}
return capitalize(typeof x);
};
/* ---------- Essential FP Functions ---------- */
// NOTE A slightly pumped up version of `curry` which also keeps track of
// whether a function was called partially or with all its arguments at once.
// This is useful to provide insights during validation of exercises.
function curry(fn) {
assert(
typeof fn === 'function',
typeMismatch('function -> ?', [getType(fn), '?'].join(' -> '), 'curry'),
);
const arity = fn.length;
return namedAs(fn.name, function $curry(...args) {
$curry.partially = this && this.partially;
if (args.length < arity) {
return namedAs(fn.name, $curry.bind({ partially: true }, ...args));
}
return fn.call(this || { partially: false }, ...args);
});
}
// NOTE A slightly pumped up version of `compose` which also keeps track of the chain
// of callees. In the end, a function created with `compose` holds a `callees` variable
// with the list of all the callees' names.
// This is useful to provide insights during validation of exercises
function compose(...fns) {
const n = fns.length;
return function $compose(...args) {
$compose.callees = [];
let $args = args;
for (let i = n - 1; i >= 0; i -= 1) {
const fn = fns[i];
assert(
typeof fn === 'function',
`Invalid Composition: ${ordinal(n - i)} element in a composition isn't a function`,
);
$compose.callees.push(fn.name);
$args = [fn.call(null, ...$args)];
}
return $args[0];
};
}
/* ---------- Algebraic Data Structures ---------- */
class Either {
static of(x) {
return new Right(x); // eslint-disable-line no-use-before-define
}
constructor(x) {
this.$value = x;
}
}
class Left extends Either {
get isLeft() { // eslint-disable-line class-methods-use-this
return true;
}
get isRight() { // eslint-disable-line class-methods-use-this
return false;
}
ap() {
return this;
}
chain() {
return this;
}
inspect() {
return `Left(${inspect(this.$value)})`;
}
getType() {
return `(Either ${getType(this.$value)} ?)`;
}
join() {
return this;
}
map() {
return this;
}
sequence(of) {
return of(this);
}
traverse(of, fn) {
return of(this);
}
}
class Right extends Either {
get isLeft() { // eslint-disable-line class-methods-use-this
return false;
}
get isRight() { // eslint-disable-line class-methods-use-this
return true;
}
ap(f) {
return f.map(this.$value);
}
chain(fn) {
return fn(this.$value);
}
inspect() {
return `Right(${inspect(this.$value)})`;
}
getType() {
return `(Either ? ${getType(this.$value)})`;
}
join() {
return this.$value;
}
map(fn) {
return Either.of(fn(this.$value));
}
sequence(of) {
return this.traverse(of, x => x);
}
traverse(of, fn) {
fn(this.$value).map(Either.of);
}
}
class Identity {
static of(x) {
return new Identity(x);
}
constructor(x) {
this.$value = x;
}
ap(f) {
return f.map(this.$value);
}
chain(fn) {
return this.map(fn).join();
}
inspect() {
return `Identity(${inspect(this.$value)})`;
}
getType() {
return `(Identity ${getType(this.$value)})`;
}
join() {
return this.$value;
}
map(fn) {
return Identity.of(fn(this.$value));
}
sequence(of) {
return this.traverse(of, x => x);
}
traverse(of, fn) {
return fn(this.$value).map(Identity.of);
}
}
class IO {
static of(x) {
return new IO(() => x);
}
constructor(io) {
assert(
typeof io === 'function',
'invalid `io` operation given to IO constructor. Use `IO.of` if you want to lift a value in a default minimal IO context.',
);
this.unsafePerformIO = io;
}
ap(f) {
return this.chain(fn => f.map(fn));
}
chain(fn) {
return this.map(fn).join();
}
inspect() {
return `IO(${inspect(this.unsafePerformIO())})`;
}
getType() {
return `(IO ${getType(this.unsafePerformIO())})`;
}
join() {
return this.unsafePerformIO();
}
map(fn) {
return new IO(compose(fn, this.unsafePerformIO));
}
}
class Map {
constructor(x) {
assert(
typeof x === 'object' && x !== null,
'tried to create `Map` with non object-like',
);
this.$value = x;
}
inspect() {
return `Map(${inspect(this.$value)})`;
}
getType() {
const sample = this.$value[Object.keys(this.$value)[0]];
return `(Map String ${sample ? getType(sample) : '?'})`;
}
insert(k, v) {
const singleton = {};
singleton[k] = v;
return new Map(Object.assign({}, this.$value, singleton));
}
reduce(fn, zero) {
return this.reduceWithKeys((acc, _, k) => fn(acc, k), zero);
}
reduceWithKeys(fn, zero) {
return Object.keys(this.$value)
.reduce((acc, k) => fn(acc, this.$value[k], k), zero);
}
map(fn) {
return new Map(this.reduceWithKeys((obj, v, k) => {
obj[k] = fn(v); // eslint-disable-line no-param-reassign
return obj;
}, {}));
}
sequence(of) {
return this.traverse(of, x => x);
}
traverse(of, fn) {
return this.reduceWithKeys(
(f, a, k) => fn(a).map(b => m => m.insert(k, b)).ap(f),
of(new Map({})),
);
}
}
class List {
static of(x) {
return new List([x]);
}
constructor(xs) {
assert(
Array.isArray(xs),
'tried to create `List` from non-array',
);
this.$value = xs;
}
concat(x) {
return new List(this.$value.concat(x));
}
inspect() {
return `List(${inspect(this.$value)})`;
}
getType() {
const sample = this.$value[0];
return `(List ${sample ? getType(sample) : '?'})`;
}
map(fn) {
return new List(this.$value.map(fn));
}
sequence(of) {
return this.traverse(of, x => x);
}
traverse(of, fn) {
return this.$value.reduce(
(f, a) => fn(a).map(b => bs => bs.concat(b)).ap(f),
of(new List([])),
);
}
}
class Maybe {
static of(x) {
return new Maybe(x);
}
get isNothing() {
return this.$value === null || this.$value === undefined;
}
get isJust() {
return !this.isNothing;
}
constructor(x) {
this.$value = x;
}
ap(f) {
return this.isNothing ? this : f.map(this.$value);
}
chain(fn) {
return this.map(fn).join();
}
inspect() {
return this.isNothing ? 'Nothing' : `Just(${inspect(this.$value)})`;
}
getType() {
return `(Maybe ${this.isJust ? getType(this.$value) : '?'})`;
}
join() {
return this.isNothing ? this : this.$value;
}
map(fn) {
return this.isNothing ? this : Maybe.of(fn(this.$value));
}
sequence(of) {
return this.traverse(of, x => x);
}
traverse(of, fn) {
return this.isNothing ? of(this) : fn(this.$value).map(Maybe.of);
}
}
class Task {
constructor(fork) {
assert(
typeof fork === 'function',
'invalid `fork` operation given to Task constructor. Use `Task.of` if you want to lift a value in a default minimal Task context.',
);
this.fork = fork;
}
static of(x) {
return new Task((_, resolve) => resolve(x));
}
static rejected(x) {
return new Task((reject, _) => reject(x));
}
ap(f) {
return this.chain(fn => f.map(fn));
}
chain(fn) {
return new Task((reject, resolve) => this.fork(reject, x => fn(x).fork(reject, resolve)));
}
inspect() { // eslint-disable-line class-methods-use-this
return 'Task(?)';
}
getType() { // eslint-disable-line class-methods-use-this
return '(Task ? ?)';
}
join() {
return this.chain(x => x);
}
map(fn) {
return new Task((reject, resolve) => this.fork(reject, compose(resolve, fn)));
}
}
// In nodejs the existance of a class method named `inspect` will trigger a deprecation warning
// when passing an instance to `console.log`:
// `(node:3845) [DEP0079] DeprecationWarning: Custom inspection function on Objects via .inspect() is deprecated`
// The solution is to alias the existing inspect method with the special inspect symbol exported by node
if (typeof module !== 'undefined' && typeof this !== 'undefined' && this.module !== module) {
const customInspect = require('util').inspect.custom;
const assignCustomInspect = it => it.prototype[customInspect] = it.prototype.inspect;
[Left, Right, Identity, IO, Map, List, Maybe, Task].forEach(assignCustomInspect);
}
const identity = function identity(x) { return x; };
const either = curry(function either(f, g, e) {
if (e.isLeft) {
return f(e.$value);
}
return g(e.$value);
});
const left = function left(x) { return new Left(x); };
const maybe = curry(function maybe(v, f, m) {
if (m.isNothing) {
return v;
}
return f(m.$value);
});
const nothing = Maybe.of(null);
const reject = function reject(x) { return Task.rejected(x); };
const chain = curry(function chain(fn, m) {
assert(
typeof fn === 'function' && typeof m.chain === 'function',
typeMismatch('Monad m => (a -> m b) -> m a -> m a', [getType(fn), getType(m), 'm a'].join(' -> '), 'chain'),
);
return m.chain(fn);
});
const join = function join(m) {
assert(
typeof m.chain === 'function',
typeMismatch('Monad m => m (m a) -> m a', [getType(m), 'm a'].join(' -> '), 'join'),
);
return m.join();
};
const map = curry(function map(fn, f) {
assert(
typeof fn === 'function' && typeof f.map === 'function',
typeMismatch('Functor f => (a -> b) -> f a -> f b', [getType(fn), getType(f), 'f b'].join(' -> '), 'map'),
);
return f.map(fn);
});
const sequence = curry(function sequence(of, x) {
assert(
typeof of === 'function' && typeof x.sequence === 'function',
typeMismatch('(Applicative f, Traversable t) => (a -> f a) -> t (f a) -> f (t a)', [getType(of), getType(x), 'f (t a)'].join(' -> '), 'sequence'),
);
return x.sequence(of);
});
const traverse = curry(function traverse(of, fn, x) {
assert(
typeof of === 'function' && typeof fn === 'function' && typeof x.traverse === 'function',
typeMismatch(
'(Applicative f, Traversable t) => (a -> f a) -> (a -> f b) -> t a -> f (t b)',
[getType(of), getType(fn), getType(x), 'f (t b)'].join(' -> '),
'traverse',
),
);
return x.traverse(of, fn);
});
const unsafePerformIO = function unsafePerformIO(io) {
assert(
io instanceof IO,
typeMismatch('IO a', getType(io), 'unsafePerformIO'),
);
return io.unsafePerformIO();
};
const liftA2 = curry(function liftA2(fn, a1, a2) {
assert(
typeof fn === 'function'
&& typeof a1.map === 'function'
&& typeof a2.ap === 'function',
typeMismatch('Applicative f => (a -> b -> c) -> f a -> f b -> f c', [getType(fn), getType(a1), getType(a2)].join(' -> '), 'liftA2'),
);
return a1.map(fn).ap(a2);
});
const liftA3 = curry(function liftA3(fn, a1, a2, a3) {
assert(
typeof fn === 'function'
&& typeof a1.map === 'function'
&& typeof a2.ap === 'function'
&& typeof a3.ap === 'function',
typeMismatch('Applicative f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d', [getType(fn), getType(a1), getType(a2)].join(' -> '), 'liftA2'),
);
return a1.map(fn).ap(a2).ap(a3);
});
const always = curry(function always(a, b) { return a; });
/* ---------- Pointfree Classic Utilities ---------- */
const append = curry(function append(a, b) {
assert(
typeof a === 'string' && typeof b === 'string',
typeMismatch('String -> String -> String', [getType(a), getType(b), 'String'].join(' -> '), 'concat'),
);
return b.concat(a);
});
const add = curry(function add(a, b) {
assert(
typeof a === 'number' && typeof b === 'number',
typeMismatch('Number -> Number -> Number', [getType(a), getType(b), 'Number'].join(' -> '), 'add'),
);
return a + b;
});
const concat = curry(function concat(a, b) {
assert(
typeof a === 'string' && typeof b === 'string',
typeMismatch('String -> String -> String', [getType(a), getType(b), 'String'].join(' -> '), 'concat'),
);
return a.concat(b);
});
const eq = curry(function eq(a, b) {
assert(
getType(a) === getType(b),
typeMismatch('a -> a -> Boolean', [getType(a), getType(b), 'Boolean'].join(' -> '), eq),
);
return a === b;
});
const filter = curry(function filter(fn, xs) {
assert(
typeof fn === 'function' && Array.isArray(xs),
typeMismatch('(a -> Boolean) -> [a] -> [a]', [getType(fn), getType(xs), getType(xs)].join(' -> '), 'filter'),
);
return xs.filter(fn);
});
const flip = curry(function flip(fn, a, b) {
assert(
typeof fn === 'function',
typeMismatch('(a -> b) -> (b -> a)', [getType(fn), '(b -> a)'].join(' -> '), 'flip'),
);
return fn(b, a);
});
const forEach = curry(function forEach(fn, xs) {
assert(
typeof fn === 'function' && Array.isArray(xs),
typeMismatch('(a -> ()) -> [a] -> ()', [getType(fn), getType(xs), '()'].join(' -> '), 'forEach'),
);
xs.forEach(fn);
});
const intercalate = curry(function intercalate(str, xs) {
assert(
typeof str === 'string' && Array.isArray(xs) && (xs.length === 0 || typeof xs[0] === 'string'),
typeMismatch('String -> [String] -> String', [getType(str), getType(xs), 'String'].join(' -> '), 'intercalate'),
);
return xs.join(str);
});
const head = function head(xs) {
assert(
Array.isArray(xs) || typeof xs === 'string',
typeMismatch('[a] -> a', [getType(xs), 'a'].join(' -> '), 'head'),
);
return xs[0];
};
const last = function last(xs) {
assert(
Array.isArray(xs) || typeof xs === 'string',
typeMismatch('[a] -> a', [getType(xs), 'a'].join(' -> '), 'last'),
);
return xs[xs.length - 1];
};
const match = curry(function match(re, str) {
assert(
re instanceof RegExp && typeof str === 'string',
typeMismatch('RegExp -> String -> Boolean', [getType(re), getType(str), 'Boolean'].join(' -> '), 'match'),
);
return re.test(str);
});
const prop = curry(function prop(p, obj) {
assert(
typeof p === 'string' && typeof obj === 'object' && obj !== null,
typeMismatch('String -> Object -> a', [getType(p), getType(obj), 'a'].join(' -> '), 'prop'),
);
return obj[p];
});
const reduce = curry(function reduce(fn, zero, xs) {
assert(
typeof fn === 'function' && Array.isArray(xs),
typeMismatch('(b -> a -> b) -> b -> [a] -> b', [getType(fn), getType(zero), getType(xs), 'b'].join(' -> '), 'reduce'),
);
return xs.reduce(
function $reduceIterator($acc, $x) { return fn($acc, $x); },
zero,
);
});
const safeHead = namedAs('safeHead', compose(Maybe.of, head));
const safeProp = curry(function safeProp(p, obj) { return Maybe.of(prop(p, obj)); });
const sortBy = curry(function sortBy(fn, xs) {
assert(
typeof fn === 'function' && Array.isArray(xs),
typeMismatch('Ord b => (a -> b) -> [a] -> [a]', [getType(fn), getType(xs), '[a]'].join(' -> '), 'sortBy'),
);
return xs.sort((a, b) => {
if (fn(a) === fn(b)) {
return 0;
}
return fn(a) > fn(b) ? 1 : -1;
});
});
const split = curry(function split(s, str) {
assert(
typeof s === 'string' && typeof str === 'string',
typeMismatch('String -> String -> [String]', [getType(s), getType(str), '[String]'].join(' -> '), 'split'),
);
return str.split(s);
});
const take = curry(function take(n, xs) {
assert(
typeof n === 'number' && (Array.isArray(xs) || typeof xs === 'string'),
typeMismatch('Number -> [a] -> [a]', [getType(n), getType(xs), getType(xs)].join(' -> '), 'take'),
);
return xs.slice(0, n);
});
const toLowerCase = function toLowerCase(s) {
assert(
typeof s === 'string',
typeMismatch('String -> String', [getType(s), '?'].join(' -> '), 'toLowerCase'),
);
return s.toLowerCase();
};
const toUpperCase = function toUpperCase(s) {
assert(
typeof s === 'string',
typeMismatch('String -> String', [getType(s), '?'].join(' -> '), 'toLowerCase'),
);
return s.toUpperCase();
};
/* ---------- Chapter 4 ---------- */
const keepHighest = function keepHighest(x, y) {
try {
keepHighest.calledBy = keepHighest.caller;
} catch (err) {
// NOTE node.js runs in strict mode and prohibit the usage of '.caller'
// There's a ugly hack to retrieve the caller from stack trace.
const [, caller] = /at (\S+)/.exec(err.stack.split('\n')[2]);
keepHighest.calledBy = namedAs(caller, () => {});
}
return x >= y ? x : y;
};
/* ---------- Chapter 5 ---------- */
const cars = [{
name: 'Ferrari FF',
horsepower: 660,
dollar_value: 700000,
in_stock: true,
}, {
name: 'Spyker C12 Zagato',
horsepower: 650,
dollar_value: 648000,
in_stock: false,
}, {
name: 'Jaguar XKR-S',
horsepower: 550,
dollar_value: 132000,
in_stock: true,
}, {
name: 'Audi R8',
horsepower: 525,
dollar_value: 114200,
in_stock: false,
}, {
name: 'Aston Martin One-77',
horsepower: 750,
dollar_value: 1850000,
in_stock: true,
}, {
name: 'Pagani Huayra',
horsepower: 700,
dollar_value: 1300000,
in_stock: false,
}];
const average = function average(xs) {
return xs.reduce(add, 0) / xs.length;
};
/* ---------- Chapter 8 ---------- */
const albert = {
id: 1,
active: true,
name: 'Albert',
address: {
street: {
number: 22,
name: 'Walnut St',
},
},
};
const gary = {
id: 2,
active: false,
name: 'Gary',
address: {
street: {
number: 14,
},
},
};
const theresa = {
id: 3,
active: true,
name: 'Theresa',
};
const yi = { id: 4, name: 'Yi', active: true };
const showWelcome = namedAs('showWelcome', compose(concat('Welcome '), prop('name')));
const checkActive = function checkActive(user) {
return user.active
? Either.of(user)
: left('Your account is not active');
};
const save = function save(user) {
return new IO(() => Object.assign({}, user, { saved: true }));
};
const validateUser = curry(function validateUser(validate, user) {
return validate(user).map(_ => user); // eslint-disable-line no-unused-vars
});
/* ---------- Chapter 9 ---------- */
const getFile = IO.of('/home/mostly-adequate/ch09.md');
const pureLog = function pureLog(str) { return new IO(() => { console.log(str); return str; }); };
const addToMailingList = function addToMailingList(email) { return IO.of([email]); };
const emailBlast = function emailBlast(list) { return IO.of(list.join(',')); };
const validateEmail = function validateEmail(x) {
return /\S+@\S+\.\S+/.test(x)
? Either.of(x)
: left('invalid email');
};
/* ---------- Chapter 10 ---------- */
const localStorage = { player1: albert, player2: theresa };
const game = curry(function game(p1, p2) { return `${p1.name} vs ${p2.name}`; });
const getFromCache = function getFromCache(x) { return new IO(() => localStorage[x]); };
/* ---------- Chapter 11 ---------- */
const findUserById = function findUserById(id) {
switch (id) {
case 1:
return Task.of(Either.of(albert));
case 2:
return Task.of(Either.of(gary));
case 3:
return Task.of(Either.of(theresa));
default:
return Task.of(left('not found'));
}
};
const eitherToTask = namedAs('eitherToTask', either(Task.rejected, Task.of));
/* ---------- Chapter 12 ---------- */
const httpGet = function httpGet(route) { return Task.of(`json for ${route}`); };
const routes = new Map({
'/': '/',
'/about': '/about',
});
const validate = function validate(player) {
return player.name
? Either.of(player)
: left('must have name');
};
const readdir = function readdir(dir) {
return Task.of(['file1', 'file2', 'file3']);
};
const readfile = curry(function readfile(encoding, file) {
return Task.of(`content of ${file} (${encoding})`);
});
/* ---------- Exports ---------- */
if (typeof module === 'object') {
module.exports = {
// Utils
withSpyOn,
// Essential FP helpers
always,
compose,
curry,
either,
identity,
inspect,
left,
liftA2,
liftA3,
maybe,
nothing,
reject,
// Algebraic Data Structures
Either,
IO,
Identity,
Left,
List,
Map,
Maybe,
Right,
Task,
// Currified version of 'standard' functions
append,
add,
chain,
concat,
eq,
filter,
flip,
forEach,
head,
intercalate,
join,
last,
map,
match,
prop,
reduce,
safeHead,
safeProp,
sequence,
sortBy,
split,
take,
toLowerCase,
toUpperCase,
traverse,
unsafePerformIO,
// Chapter 04
keepHighest,
// Chapter 05
cars,
average,
// Chapter 08
albert,
gary,
theresa,
yi,
showWelcome,
checkActive,
save,
validateUser,
// Chapter 09
getFile,
pureLog,
addToMailingList,
emailBlast,
validateEmail,
// Chapter 10
localStorage,
getFromCache,
game,
// Chapter 11
findUserById,
eitherToTask,
// Chapter 12
httpGet,
routes,
validate,
readdir,
readfile,
};
}
我们现在定义以下验证函数:
// validate :: Player -> Either String Player
const validate = player => (player.name ? Either.of(player) : left('必须有名字'));
// startGame :: [Player] -> Either Error String
const startGame = compose(map(always('game started!')), traverse(Either.of, validate));
/* globals startGame */
const res = startGame(new List([albert, theresa]));
assert(
res instanceof Either,
'The function has an invalid type; hint: `startGame` must return a `Either`!',
);
assert(
res.isRight && res.$value === 'game started!',
'The function gives incorrect results; a game should have started for a list of valid players!',
);
const rej = startGame(new List([gary, { what: 14 }]));
assert(
rej.isLeft && rej.$value === 'must have name',
'The function gives incorrect results; a game shouldn\'t be started if the list contains invalid players!',
);
const callees = startGame.callees; // eslint-disable-line prefer-destructuring
if (callees && callees[0] === 'map' && callees[1] === 'sequence') {
throw new Error('The function could be written in a simpler form; hint: compose(sequence(of), map(fn)) === traverse(of, fn)');
}
// NOTE We keep named function here to leverage this in the `compose` function,
// and later on in the validations scripts.
/* eslint-disable prefer-arrow-callback */
/* ---------- Internals ---------- */
function namedAs(value, fn) {
Object.defineProperty(fn, 'name', { value });
return fn;
}
// NOTE This file is loaded by gitbook's exercises plugin. When it does, there's an
// `assert` function available in the global scope.
/* eslint-disable no-undef, global-require */
if (typeof assert !== 'function' && typeof require === 'function') {
global.assert = require('assert');
}
assert.arrayEqual = function assertArrayEqual(actual, expected, message = 'arrayEqual') {
if (actual.length !== expected.length) {
throw new Error(message);
}
for (let i = 0; i < expected.length; i += 1) {
if (expected[i] !== actual[i]) {
throw new Error(message);
}
}
};
/* eslint-enable no-undef, global-require */
function inspect(x) {
if (x && typeof x.inspect === 'function') {
return x.inspect();
}
function inspectFn(f) {
return f.name ? f.name : f.toString();
}
function inspectTerm(t) {
switch (typeof t) {
case 'string':
return `'${t}'`;
case 'object': {
const ts = Object.keys(t).map(k => [k, inspect(t[k])]);
return `{${ts.map(kv => kv.join(': ')).join(', ')}}`;
}
default:
return String(t);
}
}
function inspectArgs(args) {
return Array.isArray(args) ? `[${args.map(inspect).join(', ')}]` : inspectTerm(args);
}
return (typeof x === 'function') ? inspectFn(x) : inspectArgs(x);
}
/* eslint-disable no-param-reassign */
function withSpyOn(prop, obj, fn) {
const orig = obj[prop];
let called = false;
obj[prop] = function spy(...args) {
called = true;
return orig.call(this, ...args);
};
fn();
obj[prop] = orig;
return called;
}
/* eslint-enable no-param-reassign */
const typeMismatch = (src, got, fn) => `Type Mismatch in function '${fn}'
${fn} :: ${got}
instead of
${fn} :: ${src}`;
const capitalize = s => `${s[0].toUpperCase()}${s.substring(1)}`;
const ordinal = (i) => {
switch (i) {
case 1:
return '1st';
case 2:
return '2nd';
case 3:
return '3rd';
default:
return `${i}th`; // NOTE won't get any much bigger ...
}
};
const getType = (x) => {
if (x === null) {
return 'Null';
}
if (typeof x === 'undefined') {
return '()';
}
if (Array.isArray(x)) {
return `[${x[0] ? getType(x[0]) : '?'}]`;
}
if (typeof x.getType === 'function') {
return x.getType();
}
if (x.constructor && x.constructor.name) {
return x.constructor.name;
}
return capitalize(typeof x);
};
/* ---------- Essential FP Functions ---------- */
// NOTE A slightly pumped up version of `curry` which also keeps track of
// whether a function was called partially or with all its arguments at once.
// This is useful to provide insights during validation of exercises.
function curry(fn) {
assert(
typeof fn === 'function',
typeMismatch('function -> ?', [getType(fn), '?'].join(' -> '), 'curry'),
);
const arity = fn.length;
return namedAs(fn.name, function $curry(...args) {
$curry.partially = this && this.partially;
if (args.length < arity) {
return namedAs(fn.name, $curry.bind({ partially: true }, ...args));
}
return fn.call(this || { partially: false }, ...args);
});
}
// NOTE A slightly pumped up version of `compose` which also keeps track of the chain
// of callees. In the end, a function created with `compose` holds a `callees` variable
// with the list of all the callees' names.
// This is useful to provide insights during validation of exercises
function compose(...fns) {
const n = fns.length;
return function $compose(...args) {
$compose.callees = [];
let $args = args;
for (let i = n - 1; i >= 0; i -= 1) {
const fn = fns[i];
assert(
typeof fn === 'function',
`Invalid Composition: ${ordinal(n - i)} element in a composition isn't a function`,
);
$compose.callees.push(fn.name);
$args = [fn.call(null, ...$args)];
}
return $args[0];
};
}
/* ---------- Algebraic Data Structures ---------- */
class Either {
static of(x) {
return new Right(x); // eslint-disable-line no-use-before-define
}
constructor(x) {
this.$value = x;
}
}
class Left extends Either {
get isLeft() { // eslint-disable-line class-methods-use-this
return true;
}
get isRight() { // eslint-disable-line class-methods-use-this
return false;
}
ap() {
return this;
}
chain() {
return this;
}
inspect() {
return `Left(${inspect(this.$value)})`;
}
getType() {
return `(Either ${getType(this.$value)} ?)`;
}
join() {
return this;
}
map() {
return this;
}
sequence(of) {
return of(this);
}
traverse(of, fn) {
return of(this);
}
}
class Right extends Either {
get isLeft() { // eslint-disable-line class-methods-use-this
return false;
}
get isRight() { // eslint-disable-line class-methods-use-this
return true;
}
ap(f) {
return f.map(this.$value);
}
chain(fn) {
return fn(this.$value);
}
inspect() {
return `Right(${inspect(this.$value)})`;
}
getType() {
return `(Either ? ${getType(this.$value)})`;
}
join() {
return this.$value;
}
map(fn) {
return Either.of(fn(this.$value));
}
sequence(of) {
return this.traverse(of, x => x);
}
traverse(of, fn) {
fn(this.$value).map(Either.of);
}
}
class Identity {
static of(x) {
return new Identity(x);
}
constructor(x) {
this.$value = x;
}
ap(f) {
return f.map(this.$value);
}
chain(fn) {
return this.map(fn).join();
}
inspect() {
return `Identity(${inspect(this.$value)})`;
}
getType() {
return `(Identity ${getType(this.$value)})`;
}
join() {
return this.$value;
}
map(fn) {
return Identity.of(fn(this.$value));
}
sequence(of) {
return this.traverse(of, x => x);
}
traverse(of, fn) {
return fn(this.$value).map(Identity.of);
}
}
class IO {
static of(x) {
return new IO(() => x);
}
constructor(io) {
assert(
typeof io === 'function',
'invalid `io` operation given to IO constructor. Use `IO.of` if you want to lift a value in a default minimal IO context.',
);
this.unsafePerformIO = io;
}
ap(f) {
return this.chain(fn => f.map(fn));
}
chain(fn) {
return this.map(fn).join();
}
inspect() {
return `IO(${inspect(this.unsafePerformIO())})`;
}
getType() {
return `(IO ${getType(this.unsafePerformIO())})`;
}
join() {
return this.unsafePerformIO();
}
map(fn) {
return new IO(compose(fn, this.unsafePerformIO));
}
}
class Map {
constructor(x) {
assert(
typeof x === 'object' && x !== null,
'tried to create `Map` with non object-like',
);
this.$value = x;
}
inspect() {
return `Map(${inspect(this.$value)})`;
}
getType() {
const sample = this.$value[Object.keys(this.$value)[0]];
return `(Map String ${sample ? getType(sample) : '?'})`;
}
insert(k, v) {
const singleton = {};
singleton[k] = v;
return new Map(Object.assign({}, this.$value, singleton));
}
reduce(fn, zero) {
return this.reduceWithKeys((acc, _, k) => fn(acc, k), zero);
}
reduceWithKeys(fn, zero) {
return Object.keys(this.$value)
.reduce((acc, k) => fn(acc, this.$value[k], k), zero);
}
map(fn) {
return new Map(this.reduceWithKeys((obj, v, k) => {
obj[k] = fn(v); // eslint-disable-line no-param-reassign
return obj;
}, {}));
}
sequence(of) {
return this.traverse(of, x => x);
}
traverse(of, fn) {
return this.reduceWithKeys(
(f, a, k) => fn(a).map(b => m => m.insert(k, b)).ap(f),
of(new Map({})),
);
}
}
class List {
static of(x) {
return new List([x]);
}
constructor(xs) {
assert(
Array.isArray(xs),
'tried to create `List` from non-array',
);
this.$value = xs;
}
concat(x) {
return new List(this.$value.concat(x));
}
inspect() {
return `List(${inspect(this.$value)})`;
}
getType() {
const sample = this.$value[0];
return `(List ${sample ? getType(sample) : '?'})`;
}
map(fn) {
return new List(this.$value.map(fn));
}
sequence(of) {
return this.traverse(of, x => x);
}
traverse(of, fn) {
return this.$value.reduce(
(f, a) => fn(a).map(b => bs => bs.concat(b)).ap(f),
of(new List([])),
);
}
}
class Maybe {
static of(x) {
return new Maybe(x);
}
get isNothing() {
return this.$value === null || this.$value === undefined;
}
get isJust() {
return !this.isNothing;
}
constructor(x) {
this.$value = x;
}
ap(f) {
return this.isNothing ? this : f.map(this.$value);
}
chain(fn) {
return this.map(fn).join();
}
inspect() {
return this.isNothing ? 'Nothing' : `Just(${inspect(this.$value)})`;
}
getType() {
return `(Maybe ${this.isJust ? getType(this.$value) : '?'})`;
}
join() {
return this.isNothing ? this : this.$value;
}
map(fn) {
return this.isNothing ? this : Maybe.of(fn(this.$value));
}
sequence(of) {
return this.traverse(of, x => x);
}
traverse(of, fn) {
return this.isNothing ? of(this) : fn(this.$value).map(Maybe.of);
}
}
class Task {
constructor(fork) {
assert(
typeof fork === 'function',
'invalid `fork` operation given to Task constructor. Use `Task.of` if you want to lift a value in a default minimal Task context.',
);
this.fork = fork;
}
static of(x) {
return new Task((_, resolve) => resolve(x));
}
static rejected(x) {
return new Task((reject, _) => reject(x));
}
ap(f) {
return this.chain(fn => f.map(fn));
}
chain(fn) {
return new Task((reject, resolve) => this.fork(reject, x => fn(x).fork(reject, resolve)));
}
inspect() { // eslint-disable-line class-methods-use-this
return 'Task(?)';
}
getType() { // eslint-disable-line class-methods-use-this
return '(Task ? ?)';
}
join() {
return this.chain(x => x);
}
map(fn) {
return new Task((reject, resolve) => this.fork(reject, compose(resolve, fn)));
}
}
// In nodejs the existance of a class method named `inspect` will trigger a deprecation warning
// when passing an instance to `console.log`:
// `(node:3845) [DEP0079] DeprecationWarning: Custom inspection function on Objects via .inspect() is deprecated`
// The solution is to alias the existing inspect method with the special inspect symbol exported by node
if (typeof module !== 'undefined' && typeof this !== 'undefined' && this.module !== module) {
const customInspect = require('util').inspect.custom;
const assignCustomInspect = it => it.prototype[customInspect] = it.prototype.inspect;
[Left, Right, Identity, IO, Map, List, Maybe, Task].forEach(assignCustomInspect);
}
const identity = function identity(x) { return x; };
const either = curry(function either(f, g, e) {
if (e.isLeft) {
return f(e.$value);
}
return g(e.$value);
});
const left = function left(x) { return new Left(x); };
const maybe = curry(function maybe(v, f, m) {
if (m.isNothing) {
return v;
}
return f(m.$value);
});
const nothing = Maybe.of(null);
const reject = function reject(x) { return Task.rejected(x); };
const chain = curry(function chain(fn, m) {
assert(
typeof fn === 'function' && typeof m.chain === 'function',
typeMismatch('Monad m => (a -> m b) -> m a -> m a', [getType(fn), getType(m), 'm a'].join(' -> '), 'chain'),
);
return m.chain(fn);
});
const join = function join(m) {
assert(
typeof m.chain === 'function',
typeMismatch('Monad m => m (m a) -> m a', [getType(m), 'm a'].join(' -> '), 'join'),
);
return m.join();
};
const map = curry(function map(fn, f) {
assert(
typeof fn === 'function' && typeof f.map === 'function',
typeMismatch('Functor f => (a -> b) -> f a -> f b', [getType(fn), getType(f), 'f b'].join(' -> '), 'map'),
);
return f.map(fn);
});
const sequence = curry(function sequence(of, x) {
assert(
typeof of === 'function' && typeof x.sequence === 'function',
typeMismatch('(Applicative f, Traversable t) => (a -> f a) -> t (f a) -> f (t a)', [getType(of), getType(x), 'f (t a)'].join(' -> '), 'sequence'),
);
return x.sequence(of);
});
const traverse = curry(function traverse(of, fn, x) {
assert(
typeof of === 'function' && typeof fn === 'function' && typeof x.traverse === 'function',
typeMismatch(
'(Applicative f, Traversable t) => (a -> f a) -> (a -> f b) -> t a -> f (t b)',
[getType(of), getType(fn), getType(x), 'f (t b)'].join(' -> '),
'traverse',
),
);
return x.traverse(of, fn);
});
const unsafePerformIO = function unsafePerformIO(io) {
assert(
io instanceof IO,
typeMismatch('IO a', getType(io), 'unsafePerformIO'),
);
return io.unsafePerformIO();
};
const liftA2 = curry(function liftA2(fn, a1, a2) {
assert(
typeof fn === 'function'
&& typeof a1.map === 'function'
&& typeof a2.ap === 'function',
typeMismatch('Applicative f => (a -> b -> c) -> f a -> f b -> f c', [getType(fn), getType(a1), getType(a2)].join(' -> '), 'liftA2'),
);
return a1.map(fn).ap(a2);
});
const liftA3 = curry(function liftA3(fn, a1, a2, a3) {
assert(
typeof fn === 'function'
&& typeof a1.map === 'function'
&& typeof a2.ap === 'function'
&& typeof a3.ap === 'function',
typeMismatch('Applicative f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d', [getType(fn), getType(a1), getType(a2)].join(' -> '), 'liftA2'),
);
return a1.map(fn).ap(a2).ap(a3);
});
const always = curry(function always(a, b) { return a; });
/* ---------- Pointfree Classic Utilities ---------- */
const append = curry(function append(a, b) {
assert(
typeof a === 'string' && typeof b === 'string',
typeMismatch('String -> String -> String', [getType(a), getType(b), 'String'].join(' -> '), 'concat'),
);
return b.concat(a);
});
const add = curry(function add(a, b) {
assert(
typeof a === 'number' && typeof b === 'number',
typeMismatch('Number -> Number -> Number', [getType(a), getType(b), 'Number'].join(' -> '), 'add'),
);
return a + b;
});
const concat = curry(function concat(a, b) {
assert(
typeof a === 'string' && typeof b === 'string',
typeMismatch('String -> String -> String', [getType(a), getType(b), 'String'].join(' -> '), 'concat'),
);
return a.concat(b);
});
const eq = curry(function eq(a, b) {
assert(
getType(a) === getType(b),
typeMismatch('a -> a -> Boolean', [getType(a), getType(b), 'Boolean'].join(' -> '), eq),
);
return a === b;
});
const filter = curry(function filter(fn, xs) {
assert(
typeof fn === 'function' && Array.isArray(xs),
typeMismatch('(a -> Boolean) -> [a] -> [a]', [getType(fn), getType(xs), getType(xs)].join(' -> '), 'filter'),
);
return xs.filter(fn);
});
const flip = curry(function flip(fn, a, b) {
assert(
typeof fn === 'function',
typeMismatch('(a -> b) -> (b -> a)', [getType(fn), '(b -> a)'].join(' -> '), 'flip'),
);
return fn(b, a);
});
const forEach = curry(function forEach(fn, xs) {
assert(
typeof fn === 'function' && Array.isArray(xs),
typeMismatch('(a -> ()) -> [a] -> ()', [getType(fn), getType(xs), '()'].join(' -> '), 'forEach'),
);
xs.forEach(fn);
});
const intercalate = curry(function intercalate(str, xs) {
assert(
typeof str === 'string' && Array.isArray(xs) && (xs.length === 0 || typeof xs[0] === 'string'),
typeMismatch('String -> [String] -> String', [getType(str), getType(xs), 'String'].join(' -> '), 'intercalate'),
);
return xs.join(str);
});
const head = function head(xs) {
assert(
Array.isArray(xs) || typeof xs === 'string',
typeMismatch('[a] -> a', [getType(xs), 'a'].join(' -> '), 'head'),
);
return xs[0];
};
const last = function last(xs) {
assert(
Array.isArray(xs) || typeof xs === 'string',
typeMismatch('[a] -> a', [getType(xs), 'a'].join(' -> '), 'last'),
);
return xs[xs.length - 1];
};
const match = curry(function match(re, str) {
assert(
re instanceof RegExp && typeof str === 'string',
typeMismatch('RegExp -> String -> Boolean', [getType(re), getType(str), 'Boolean'].join(' -> '), 'match'),
);
return re.test(str);
});
const prop = curry(function prop(p, obj) {
assert(
typeof p === 'string' && typeof obj === 'object' && obj !== null,
typeMismatch('String -> Object -> a', [getType(p), getType(obj), 'a'].join(' -> '), 'prop'),
);
return obj[p];
});
const reduce = curry(function reduce(fn, zero, xs) {
assert(
typeof fn === 'function' && Array.isArray(xs),
typeMismatch('(b -> a -> b) -> b -> [a] -> b', [getType(fn), getType(zero), getType(xs), 'b'].join(' -> '), 'reduce'),
);
return xs.reduce(
function $reduceIterator($acc, $x) { return fn($acc, $x); },
zero,
);
});
const safeHead = namedAs('safeHead', compose(Maybe.of, head));
const safeProp = curry(function safeProp(p, obj) { return Maybe.of(prop(p, obj)); });
const sortBy = curry(function sortBy(fn, xs) {
assert(
typeof fn === 'function' && Array.isArray(xs),
typeMismatch('Ord b => (a -> b) -> [a] -> [a]', [getType(fn), getType(xs), '[a]'].join(' -> '), 'sortBy'),
);
return xs.sort((a, b) => {
if (fn(a) === fn(b)) {
return 0;
}
return fn(a) > fn(b) ? 1 : -1;
});
});
const split = curry(function split(s, str) {
assert(
typeof s === 'string' && typeof str === 'string',
typeMismatch('String -> String -> [String]', [getType(s), getType(str), '[String]'].join(' -> '), 'split'),
);
return str.split(s);
});
const take = curry(function take(n, xs) {
assert(
typeof n === 'number' && (Array.isArray(xs) || typeof xs === 'string'),
typeMismatch('Number -> [a] -> [a]', [getType(n), getType(xs), getType(xs)].join(' -> '), 'take'),
);
return xs.slice(0, n);
});
const toLowerCase = function toLowerCase(s) {
assert(
typeof s === 'string',
typeMismatch('String -> String', [getType(s), '?'].join(' -> '), 'toLowerCase'),
);
return s.toLowerCase();
};
const toUpperCase = function toUpperCase(s) {
assert(
typeof s === 'string',
typeMismatch('String -> String', [getType(s), '?'].join(' -> '), 'toLowerCase'),
);
return s.toUpperCase();
};
/* ---------- Chapter 4 ---------- */
const keepHighest = function keepHighest(x, y) {
try {
keepHighest.calledBy = keepHighest.caller;
} catch (err) {
// NOTE node.js runs in strict mode and prohibit the usage of '.caller'
// There's a ugly hack to retrieve the caller from stack trace.
const [, caller] = /at (\S+)/.exec(err.stack.split('\n')[2]);
keepHighest.calledBy = namedAs(caller, () => {});
}
return x >= y ? x : y;
};
/* ---------- Chapter 5 ---------- */
const cars = [{
name: 'Ferrari FF',
horsepower: 660,
dollar_value: 700000,
in_stock: true,
}, {
name: 'Spyker C12 Zagato',
horsepower: 650,
dollar_value: 648000,
in_stock: false,
}, {
name: 'Jaguar XKR-S',
horsepower: 550,
dollar_value: 132000,
in_stock: true,
}, {
name: 'Audi R8',
horsepower: 525,
dollar_value: 114200,
in_stock: false,
}, {
name: 'Aston Martin One-77',
horsepower: 750,
dollar_value: 1850000,
in_stock: true,
}, {
name: 'Pagani Huayra',
horsepower: 700,
dollar_value: 1300000,
in_stock: false,
}];
const average = function average(xs) {
return xs.reduce(add, 0) / xs.length;
};
/* ---------- Chapter 8 ---------- */
const albert = {
id: 1,
active: true,
name: 'Albert',
address: {
street: {
number: 22,
name: 'Walnut St',
},
},
};
const gary = {
id: 2,
active: false,
name: 'Gary',
address: {
street: {
number: 14,
},
},
};
const theresa = {
id: 3,
active: true,
name: 'Theresa',
};
const yi = { id: 4, name: 'Yi', active: true };
const showWelcome = namedAs('showWelcome', compose(concat('Welcome '), prop('name')));
const checkActive = function checkActive(user) {
return user.active
? Either.of(user)
: left('Your account is not active');
};
const save = function save(user) {
return new IO(() => Object.assign({}, user, { saved: true }));
};
const validateUser = curry(function validateUser(validate, user) {
return validate(user).map(_ => user); // eslint-disable-line no-unused-vars
});
/* ---------- Chapter 9 ---------- */
const getFile = IO.of('/home/mostly-adequate/ch09.md');
const pureLog = function pureLog(str) { return new IO(() => { console.log(str); return str; }); };
const addToMailingList = function addToMailingList(email) { return IO.of([email]); };
const emailBlast = function emailBlast(list) { return IO.of(list.join(',')); };
const validateEmail = function validateEmail(x) {
return /\S+@\S+\.\S+/.test(x)
? Either.of(x)
: left('invalid email');
};
/* ---------- Chapter 10 ---------- */
const localStorage = { player1: albert, player2: theresa };
const game = curry(function game(p1, p2) { return `${p1.name} vs ${p2.name}`; });
const getFromCache = function getFromCache(x) { return new IO(() => localStorage[x]); };
/* ---------- Chapter 11 ---------- */
const findUserById = function findUserById(id) {
switch (id) {
case 1:
return Task.of(Either.of(albert));
case 2:
return Task.of(Either.of(gary));
case 3:
return Task.of(Either.of(theresa));
default:
return Task.of(left('not found'));
}
};
const eitherToTask = namedAs('eitherToTask', either(Task.rejected, Task.of));
/* ---------- Chapter 12 ---------- */
const httpGet = function httpGet(route) { return Task.of(`json for ${route}`); };
const routes = new Map({
'/': '/',
'/about': '/about',
});
const validate = function validate(player) {
return player.name
? Either.of(player)
: left('must have name');
};
const readdir = function readdir(dir) {
return Task.of(['file1', 'file2', 'file3']);
};
const readfile = curry(function readfile(encoding, file) {
return Task.of(`content of ${file} (${encoding})`);
});
/* ---------- Exports ---------- */
if (typeof module === 'object') {
module.exports = {
// Utils
withSpyOn,
// Essential FP helpers
always,
compose,
curry,
either,
identity,
inspect,
left,
liftA2,
liftA3,
maybe,
nothing,
reject,
// Algebraic Data Structures
Either,
IO,
Identity,
Left,
List,
Map,
Maybe,
Right,
Task,
// Currified version of 'standard' functions
append,
add,
chain,
concat,
eq,
filter,
flip,
forEach,
head,
intercalate,
join,
last,
map,
match,
prop,
reduce,
safeHead,
safeProp,
sequence,
sortBy,
split,
take,
toLowerCase,
toUpperCase,
traverse,
unsafePerformIO,
// Chapter 04
keepHighest,
// Chapter 05
cars,
average,
// Chapter 08
albert,
gary,
theresa,
yi,
showWelcome,
checkActive,
save,
validateUser,
// Chapter 09
getFile,
pureLog,
addToMailingList,
emailBlast,
validateEmail,
// Chapter 10
localStorage,
getFromCache,
game,
// Chapter 11
findUserById,
eitherToTask,
// Chapter 12
httpGet,
routes,
validate,
readdir,
readfile,
};
}
最后,我们考虑一些文件系统辅助函数:
// readfile :: String -> String -> Task Error String
// readdir :: String -> Task Error [String]
// readFirst :: String -> Task Error (Maybe String)
const readFirst = compose(
chain(traverse(Task.of, readfile('utf-8'))),
map(safeHead),
readdir,
);
/* globals readFirst */
const res = readFirst('__dirname');
const throwUnexpected = () => {
throw new Error('The function gives incorrect results; a Task has resolved unexpectedly!');
};
assert(
res instanceof Task,
'The function has an invalid type; hint: `readFirst` must return a `Task`!',
);
res.fork(throwUnexpected, ($res) => {
assert(
$res instanceof Maybe,
'The function has an invalid type; hint: `readFirst` must return a `Task Error (Maybe String)`!',
);
assert(
$res.isJust && $res.$value === 'content of file1 (utf-8)',
'The function gives incorrect results.',
);
});
// NOTE We keep named function here to leverage this in the `compose` function,
// and later on in the validations scripts.
/* eslint-disable prefer-arrow-callback */
/* ---------- Internals ---------- */
function namedAs(value, fn) {
Object.defineProperty(fn, 'name', { value });
return fn;
}
// NOTE This file is loaded by gitbook's exercises plugin. When it does, there's an
// `assert` function available in the global scope.
/* eslint-disable no-undef, global-require */
if (typeof assert !== 'function' && typeof require === 'function') {
global.assert = require('assert');
}
assert.arrayEqual = function assertArrayEqual(actual, expected, message = 'arrayEqual') {
if (actual.length !== expected.length) {
throw new Error(message);
}
for (let i = 0; i < expected.length; i += 1) {
if (expected[i] !== actual[i]) {
throw new Error(message);
}
}
};
/* eslint-enable no-undef, global-require */
function inspect(x) {
if (x && typeof x.inspect === 'function') {
return x.inspect();
}
function inspectFn(f) {
return f.name ? f.name : f.toString();
}
function inspectTerm(t) {
switch (typeof t) {
case 'string':
return `'${t}'`;
case 'object': {
const ts = Object.keys(t).map(k => [k, inspect(t[k])]);
return `{${ts.map(kv => kv.join(': ')).join(', ')}}`;
}
default:
return String(t);
}
}
function inspectArgs(args) {
return Array.isArray(args) ? `[${args.map(inspect).join(', ')}]` : inspectTerm(args);
}
return (typeof x === 'function') ? inspectFn(x) : inspectArgs(x);
}
/* eslint-disable no-param-reassign */
function withSpyOn(prop, obj, fn) {
const orig = obj[prop];
let called = false;
obj[prop] = function spy(...args) {
called = true;
return orig.call(this, ...args);
};
fn();
obj[prop] = orig;
return called;
}
/* eslint-enable no-param-reassign */
const typeMismatch = (src, got, fn) => `Type Mismatch in function '${fn}'
${fn} :: ${got}
instead of
${fn} :: ${src}`;
const capitalize = s => `${s[0].toUpperCase()}${s.substring(1)}`;
const ordinal = (i) => {
switch (i) {
case 1:
return '1st';
case 2:
return '2nd';
case 3:
return '3rd';
default:
return `${i}th`; // NOTE won't get any much bigger ...
}
};
const getType = (x) => {
if (x === null) {
return 'Null';
}
if (typeof x === 'undefined') {
return '()';
}
if (Array.isArray(x)) {
return `[${x[0] ? getType(x[0]) : '?'}]`;
}
if (typeof x.getType === 'function') {
return x.getType();
}
if (x.constructor && x.constructor.name) {
return x.constructor.name;
}
return capitalize(typeof x);
};
/* ---------- Essential FP Functions ---------- */
// NOTE A slightly pumped up version of `curry` which also keeps track of
// whether a function was called partially or with all its arguments at once.
// This is useful to provide insights during validation of exercises.
function curry(fn) {
assert(
typeof fn === 'function',
typeMismatch('function -> ?', [getType(fn), '?'].join(' -> '), 'curry'),
);
const arity = fn.length;
return namedAs(fn.name, function $curry(...args) {
$curry.partially = this && this.partially;
if (args.length < arity) {
return namedAs(fn.name, $curry.bind({ partially: true }, ...args));
}
return fn.call(this || { partially: false }, ...args);
});
}
// NOTE A slightly pumped up version of `compose` which also keeps track of the chain
// of callees. In the end, a function created with `compose` holds a `callees` variable
// with the list of all the callees' names.
// This is useful to provide insights during validation of exercises
function compose(...fns) {
const n = fns.length;
return function $compose(...args) {
$compose.callees = [];
let $args = args;
for (let i = n - 1; i >= 0; i -= 1) {
const fn = fns[i];
assert(
typeof fn === 'function',
`Invalid Composition: ${ordinal(n - i)} element in a composition isn't a function`,
);
$compose.callees.push(fn.name);
$args = [fn.call(null, ...$args)];
}
return $args[0];
};
}
/* ---------- Algebraic Data Structures ---------- */
class Either {
static of(x) {
return new Right(x); // eslint-disable-line no-use-before-define
}
constructor(x) {
this.$value = x;
}
}
class Left extends Either {
get isLeft() { // eslint-disable-line class-methods-use-this
return true;
}
get isRight() { // eslint-disable-line class-methods-use-this
return false;
}
ap() {
return this;
}
chain() {
return this;
}
inspect() {
return `Left(${inspect(this.$value)})`;
}
getType() {
return `(Either ${getType(this.$value)} ?)`;
}
join() {
return this;
}
map() {
return this;
}
sequence(of) {
return of(this);
}
traverse(of, fn) {
return of(this);
}
}
class Right extends Either {
get isLeft() { // eslint-disable-line class-methods-use-this
return false;
}
get isRight() { // eslint-disable-line class-methods-use-this
return true;
}
ap(f) {
return f.map(this.$value);
}
chain(fn) {
return fn(this.$value);
}
inspect() {
return `Right(${inspect(this.$value)})`;
}
getType() {
return `(Either ? ${getType(this.$value)})`;
}
join() {
return this.$value;
}
map(fn) {
return Either.of(fn(this.$value));
}
sequence(of) {
return this.traverse(of, x => x);
}
traverse(of, fn) {
fn(this.$value).map(Either.of);
}
}
class Identity {
static of(x) {
return new Identity(x);
}
constructor(x) {
this.$value = x;
}
ap(f) {
return f.map(this.$value);
}
chain(fn) {
return this.map(fn).join();
}
inspect() {
return `Identity(${inspect(this.$value)})`;
}
getType() {
return `(Identity ${getType(this.$value)})`;
}
join() {
return this.$value;
}
map(fn) {
return Identity.of(fn(this.$value));
}
sequence(of) {
return this.traverse(of, x => x);
}
traverse(of, fn) {
return fn(this.$value).map(Identity.of);
}
}
class IO {
static of(x) {
return new IO(() => x);
}
constructor(io) {
assert(
typeof io === 'function',
'invalid `io` operation given to IO constructor. Use `IO.of` if you want to lift a value in a default minimal IO context.',
);
this.unsafePerformIO = io;
}
ap(f) {
return this.chain(fn => f.map(fn));
}
chain(fn) {
return this.map(fn).join();
}
inspect() {
return `IO(${inspect(this.unsafePerformIO())})`;
}
getType() {
return `(IO ${getType(this.unsafePerformIO())})`;
}
join() {
return this.unsafePerformIO();
}
map(fn) {
return new IO(compose(fn, this.unsafePerformIO));
}
}
class Map {
constructor(x) {
assert(
typeof x === 'object' && x !== null,
'tried to create `Map` with non object-like',
);
this.$value = x;
}
inspect() {
return `Map(${inspect(this.$value)})`;
}
getType() {
const sample = this.$value[Object.keys(this.$value)[0]];
return `(Map String ${sample ? getType(sample) : '?'})`;
}
insert(k, v) {
const singleton = {};
singleton[k] = v;
return new Map(Object.assign({}, this.$value, singleton));
}
reduce(fn, zero) {
return this.reduceWithKeys((acc, _, k) => fn(acc, k), zero);
}
reduceWithKeys(fn, zero) {
return Object.keys(this.$value)
.reduce((acc, k) => fn(acc, this.$value[k], k), zero);
}
map(fn) {
return new Map(this.reduceWithKeys((obj, v, k) => {
obj[k] = fn(v); // eslint-disable-line no-param-reassign
return obj;
}, {}));
}
sequence(of) {
return this.traverse(of, x => x);
}
traverse(of, fn) {
return this.reduceWithKeys(
(f, a, k) => fn(a).map(b => m => m.insert(k, b)).ap(f),
of(new Map({})),
);
}
}
class List {
static of(x) {
return new List([x]);
}
constructor(xs) {
assert(
Array.isArray(xs),
'tried to create `List` from non-array',
);
this.$value = xs;
}
concat(x) {
return new List(this.$value.concat(x));
}
inspect() {
return `List(${inspect(this.$value)})`;
}
getType() {
const sample = this.$value[0];
return `(List ${sample ? getType(sample) : '?'})`;
}
map(fn) {
return new List(this.$value.map(fn));
}
sequence(of) {
return this.traverse(of, x => x);
}
traverse(of, fn) {
return this.$value.reduce(
(f, a) => fn(a).map(b => bs => bs.concat(b)).ap(f),
of(new List([])),
);
}
}
class Maybe {
static of(x) {
return new Maybe(x);
}
get isNothing() {
return this.$value === null || this.$value === undefined;
}
get isJust() {
return !this.isNothing;
}
constructor(x) {
this.$value = x;
}
ap(f) {
return this.isNothing ? this : f.map(this.$value);
}
chain(fn) {
return this.map(fn).join();
}
inspect() {
return this.isNothing ? 'Nothing' : `Just(${inspect(this.$value)})`;
}
getType() {
return `(Maybe ${this.isJust ? getType(this.$value) : '?'})`;
}
join() {
return this.isNothing ? this : this.$value;
}
map(fn) {
return this.isNothing ? this : Maybe.of(fn(this.$value));
}
sequence(of) {
return this.traverse(of, x => x);
}
traverse(of, fn) {
return this.isNothing ? of(this) : fn(this.$value).map(Maybe.of);
}
}
class Task {
constructor(fork) {
assert(
typeof fork === 'function',
'invalid `fork` operation given to Task constructor. Use `Task.of` if you want to lift a value in a default minimal Task context.',
);
this.fork = fork;
}
static of(x) {
return new Task((_, resolve) => resolve(x));
}
static rejected(x) {
return new Task((reject, _) => reject(x));
}
ap(f) {
return this.chain(fn => f.map(fn));
}
chain(fn) {
return new Task((reject, resolve) => this.fork(reject, x => fn(x).fork(reject, resolve)));
}
inspect() { // eslint-disable-line class-methods-use-this
return 'Task(?)';
}
getType() { // eslint-disable-line class-methods-use-this
return '(Task ? ?)';
}
join() {
return this.chain(x => x);
}
map(fn) {
return new Task((reject, resolve) => this.fork(reject, compose(resolve, fn)));
}
}
// In nodejs the existance of a class method named `inspect` will trigger a deprecation warning
// when passing an instance to `console.log`:
// `(node:3845) [DEP0079] DeprecationWarning: Custom inspection function on Objects via .inspect() is deprecated`
// The solution is to alias the existing inspect method with the special inspect symbol exported by node
if (typeof module !== 'undefined' && typeof this !== 'undefined' && this.module !== module) {
const customInspect = require('util').inspect.custom;
const assignCustomInspect = it => it.prototype[customInspect] = it.prototype.inspect;
[Left, Right, Identity, IO, Map, List, Maybe, Task].forEach(assignCustomInspect);
}
const identity = function identity(x) { return x; };
const either = curry(function either(f, g, e) {
if (e.isLeft) {
return f(e.$value);
}
return g(e.$value);
});
const left = function left(x) { return new Left(x); };
const maybe = curry(function maybe(v, f, m) {
if (m.isNothing) {
return v;
}
return f(m.$value);
});
const nothing = Maybe.of(null);
const reject = function reject(x) { return Task.rejected(x); };
const chain = curry(function chain(fn, m) {
assert(
typeof fn === 'function' && typeof m.chain === 'function',
typeMismatch('Monad m => (a -> m b) -> m a -> m a', [getType(fn), getType(m), 'm a'].join(' -> '), 'chain'),
);
return m.chain(fn);
});
const join = function join(m) {
assert(
typeof m.chain === 'function',
typeMismatch('Monad m => m (m a) -> m a', [getType(m), 'm a'].join(' -> '), 'join'),
);
return m.join();
};
const map = curry(function map(fn, f) {
assert(
typeof fn === 'function' && typeof f.map === 'function',
typeMismatch('Functor f => (a -> b) -> f a -> f b', [getType(fn), getType(f), 'f b'].join(' -> '), 'map'),
);
return f.map(fn);
});
const sequence = curry(function sequence(of, x) {
assert(
typeof of === 'function' && typeof x.sequence === 'function',
typeMismatch('(Applicative f, Traversable t) => (a -> f a) -> t (f a) -> f (t a)', [getType(of), getType(x), 'f (t a)'].join(' -> '), 'sequence'),
);
return x.sequence(of);
});
const traverse = curry(function traverse(of, fn, x) {
assert(
typeof of === 'function' && typeof fn === 'function' && typeof x.traverse === 'function',
typeMismatch(
'(Applicative f, Traversable t) => (a -> f a) -> (a -> f b) -> t a -> f (t b)',
[getType(of), getType(fn), getType(x), 'f (t b)'].join(' -> '),
'traverse',
),
);
return x.traverse(of, fn);
});
const unsafePerformIO = function unsafePerformIO(io) {
assert(
io instanceof IO,
typeMismatch('IO a', getType(io), 'unsafePerformIO'),
);
return io.unsafePerformIO();
};
const liftA2 = curry(function liftA2(fn, a1, a2) {
assert(
typeof fn === 'function'
&& typeof a1.map === 'function'
&& typeof a2.ap === 'function',
typeMismatch('Applicative f => (a -> b -> c) -> f a -> f b -> f c', [getType(fn), getType(a1), getType(a2)].join(' -> '), 'liftA2'),
);
return a1.map(fn).ap(a2);
});
const liftA3 = curry(function liftA3(fn, a1, a2, a3) {
assert(
typeof fn === 'function'
&& typeof a1.map === 'function'
&& typeof a2.ap === 'function'
&& typeof a3.ap === 'function',
typeMismatch('Applicative f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d', [getType(fn), getType(a1), getType(a2)].join(' -> '), 'liftA2'),
);
return a1.map(fn).ap(a2).ap(a3);
});
const always = curry(function always(a, b) { return a; });
/* ---------- Pointfree Classic Utilities ---------- */
const append = curry(function append(a, b) {
assert(
typeof a === 'string' && typeof b === 'string',
typeMismatch('String -> String -> String', [getType(a), getType(b), 'String'].join(' -> '), 'concat'),
);
return b.concat(a);
});
const add = curry(function add(a, b) {
assert(
typeof a === 'number' && typeof b === 'number',
typeMismatch('Number -> Number -> Number', [getType(a), getType(b), 'Number'].join(' -> '), 'add'),
);
return a + b;
});
const concat = curry(function concat(a, b) {
assert(
typeof a === 'string' && typeof b === 'string',
typeMismatch('String -> String -> String', [getType(a), getType(b), 'String'].join(' -> '), 'concat'),
);
return a.concat(b);
});
const eq = curry(function eq(a, b) {
assert(
getType(a) === getType(b),
typeMismatch('a -> a -> Boolean', [getType(a), getType(b), 'Boolean'].join(' -> '), eq),
);
return a === b;
});
const filter = curry(function filter(fn, xs) {
assert(
typeof fn === 'function' && Array.isArray(xs),
typeMismatch('(a -> Boolean) -> [a] -> [a]', [getType(fn), getType(xs), getType(xs)].join(' -> '), 'filter'),
);
return xs.filter(fn);
});
const flip = curry(function flip(fn, a, b) {
assert(
typeof fn === 'function',
typeMismatch('(a -> b) -> (b -> a)', [getType(fn), '(b -> a)'].join(' -> '), 'flip'),
);
return fn(b, a);
});
const forEach = curry(function forEach(fn, xs) {
assert(
typeof fn === 'function' && Array.isArray(xs),
typeMismatch('(a -> ()) -> [a] -> ()', [getType(fn), getType(xs), '()'].join(' -> '), 'forEach'),
);
xs.forEach(fn);
});
const intercalate = curry(function intercalate(str, xs) {
assert(
typeof str === 'string' && Array.isArray(xs) && (xs.length === 0 || typeof xs[0] === 'string'),
typeMismatch('String -> [String] -> String', [getType(str), getType(xs), 'String'].join(' -> '), 'intercalate'),
);
return xs.join(str);
});
const head = function head(xs) {
assert(
Array.isArray(xs) || typeof xs === 'string',
typeMismatch('[a] -> a', [getType(xs), 'a'].join(' -> '), 'head'),
);
return xs[0];
};
const last = function last(xs) {
assert(
Array.isArray(xs) || typeof xs === 'string',
typeMismatch('[a] -> a', [getType(xs), 'a'].join(' -> '), 'last'),
);
return xs[xs.length - 1];
};
const match = curry(function match(re, str) {
assert(
re instanceof RegExp && typeof str === 'string',
typeMismatch('RegExp -> String -> Boolean', [getType(re), getType(str), 'Boolean'].join(' -> '), 'match'),
);
return re.test(str);
});
const prop = curry(function prop(p, obj) {
assert(
typeof p === 'string' && typeof obj === 'object' && obj !== null,
typeMismatch('String -> Object -> a', [getType(p), getType(obj), 'a'].join(' -> '), 'prop'),
);
return obj[p];
});
const reduce = curry(function reduce(fn, zero, xs) {
assert(
typeof fn === 'function' && Array.isArray(xs),
typeMismatch('(b -> a -> b) -> b -> [a] -> b', [getType(fn), getType(zero), getType(xs), 'b'].join(' -> '), 'reduce'),
);
return xs.reduce(
function $reduceIterator($acc, $x) { return fn($acc, $x); },
zero,
);
});
const safeHead = namedAs('safeHead', compose(Maybe.of, head));
const safeProp = curry(function safeProp(p, obj) { return Maybe.of(prop(p, obj)); });
const sortBy = curry(function sortBy(fn, xs) {
assert(
typeof fn === 'function' && Array.isArray(xs),
typeMismatch('Ord b => (a -> b) -> [a] -> [a]', [getType(fn), getType(xs), '[a]'].join(' -> '), 'sortBy'),
);
return xs.sort((a, b) => {
if (fn(a) === fn(b)) {
return 0;
}
return fn(a) > fn(b) ? 1 : -1;
});
});
const split = curry(function split(s, str) {
assert(
typeof s === 'string' && typeof str === 'string',
typeMismatch('String -> String -> [String]', [getType(s), getType(str), '[String]'].join(' -> '), 'split'),
);
return str.split(s);
});
const take = curry(function take(n, xs) {
assert(
typeof n === 'number' && (Array.isArray(xs) || typeof xs === 'string'),
typeMismatch('Number -> [a] -> [a]', [getType(n), getType(xs), getType(xs)].join(' -> '), 'take'),
);
return xs.slice(0, n);
});
const toLowerCase = function toLowerCase(s) {
assert(
typeof s === 'string',
typeMismatch('String -> String', [getType(s), '?'].join(' -> '), 'toLowerCase'),
);
return s.toLowerCase();
};
const toUpperCase = function toUpperCase(s) {
assert(
typeof s === 'string',
typeMismatch('String -> String', [getType(s), '?'].join(' -> '), 'toLowerCase'),
);
return s.toUpperCase();
};
/* ---------- Chapter 4 ---------- */
const keepHighest = function keepHighest(x, y) {
try {
keepHighest.calledBy = keepHighest.caller;
} catch (err) {
// NOTE node.js runs in strict mode and prohibit the usage of '.caller'
// There's a ugly hack to retrieve the caller from stack trace.
const [, caller] = /at (\S+)/.exec(err.stack.split('\n')[2]);
keepHighest.calledBy = namedAs(caller, () => {});
}
return x >= y ? x : y;
};
/* ---------- Chapter 5 ---------- */
const cars = [{
name: 'Ferrari FF',
horsepower: 660,
dollar_value: 700000,
in_stock: true,
}, {
name: 'Spyker C12 Zagato',
horsepower: 650,
dollar_value: 648000,
in_stock: false,
}, {
name: 'Jaguar XKR-S',
horsepower: 550,
dollar_value: 132000,
in_stock: true,
}, {
name: 'Audi R8',
horsepower: 525,
dollar_value: 114200,
in_stock: false,
}, {
name: 'Aston Martin One-77',
horsepower: 750,
dollar_value: 1850000,
in_stock: true,
}, {
name: 'Pagani Huayra',
horsepower: 700,
dollar_value: 1300000,
in_stock: false,
}];
const average = function average(xs) {
return xs.reduce(add, 0) / xs.length;
};
/* ---------- Chapter 8 ---------- */
const albert = {
id: 1,
active: true,
name: 'Albert',
address: {
street: {
number: 22,
name: 'Walnut St',
},
},
};
const gary = {
id: 2,
active: false,
name: 'Gary',
address: {
street: {
number: 14,
},
},
};
const theresa = {
id: 3,
active: true,
name: 'Theresa',
};
const yi = { id: 4, name: 'Yi', active: true };
const showWelcome = namedAs('showWelcome', compose(concat('Welcome '), prop('name')));
const checkActive = function checkActive(user) {
return user.active
? Either.of(user)
: left('Your account is not active');
};
const save = function save(user) {
return new IO(() => Object.assign({}, user, { saved: true }));
};
const validateUser = curry(function validateUser(validate, user) {
return validate(user).map(_ => user); // eslint-disable-line no-unused-vars
});
/* ---------- Chapter 9 ---------- */
const getFile = IO.of('/home/mostly-adequate/ch09.md');
const pureLog = function pureLog(str) { return new IO(() => { console.log(str); return str; }); };
const addToMailingList = function addToMailingList(email) { return IO.of([email]); };
const emailBlast = function emailBlast(list) { return IO.of(list.join(',')); };
const validateEmail = function validateEmail(x) {
return /\S+@\S+\.\S+/.test(x)
? Either.of(x)
: left('invalid email');
};
/* ---------- Chapter 10 ---------- */
const localStorage = { player1: albert, player2: theresa };
const game = curry(function game(p1, p2) { return `${p1.name} vs ${p2.name}`; });
const getFromCache = function getFromCache(x) { return new IO(() => localStorage[x]); };
/* ---------- Chapter 11 ---------- */
const findUserById = function findUserById(id) {
switch (id) {
case 1:
return Task.of(Either.of(albert));
case 2:
return Task.of(Either.of(gary));
case 3:
return Task.of(Either.of(theresa));
default:
return Task.of(left('not found'));
}
};
const eitherToTask = namedAs('eitherToTask', either(Task.rejected, Task.of));
/* ---------- Chapter 12 ---------- */
const httpGet = function httpGet(route) { return Task.of(`json for ${route}`); };
const routes = new Map({
'/': '/',
'/about': '/about',
});
const validate = function validate(player) {
return player.name
? Either.of(player)
: left('must have name');
};
const readdir = function readdir(dir) {
return Task.of(['file1', 'file2', 'file3']);
};
const readfile = curry(function readfile(encoding, file) {
return Task.of(`content of ${file} (${encoding})`);
});
/* ---------- Exports ---------- */
if (typeof module === 'object') {
module.exports = {
// Utils
withSpyOn,
// Essential FP helpers
always,
compose,
curry,
either,
identity,
inspect,
left,
liftA2,
liftA3,
maybe,
nothing,
reject,
// Algebraic Data Structures
Either,
IO,
Identity,
Left,
List,
Map,
Maybe,
Right,
Task,
// Currified version of 'standard' functions
append,
add,
chain,
concat,
eq,
filter,
flip,
forEach,
head,
intercalate,
join,
last,
map,
match,
prop,
reduce,
safeHead,
safeProp,
sequence,
sortBy,
split,
take,
toLowerCase,
toUpperCase,
traverse,
unsafePerformIO,
// Chapter 04
keepHighest,
// Chapter 05
cars,
average,
// Chapter 08
albert,
gary,
theresa,
yi,
showWelcome,
checkActive,
save,
validateUser,
// Chapter 09
getFile,
pureLog,
addToMailingList,
emailBlast,
validateEmail,
// Chapter 10
localStorage,
getFromCache,
game,
// Chapter 11
findUserById,
eitherToTask,
// Chapter 12
httpGet,
routes,
validate,
readdir,
readfile,
};
}