第 11 章:再次变换,自然而然
我们将要在日常代码的实用场景下讨论自然变换(natural transformations)。它们恰好是范畴论(category theory)的一大支柱,在运用数学来推理和重构代码时绝对不可或缺。因此,我认为有责任告知各位,由于本人认知所限,你们即将见证的无疑是一种令人扼腕的不公。那么,我们开始吧。
诅咒这嵌套
我想谈谈嵌套(nesting)的问题。不是准父母们感受到的那种整理和重新布置、带有强迫症倾向的筑巢本能冲动,而是……嗯,好吧,仔细想想,这和我们将在接下来几章看到的也相差不远……无论如何,我所说的嵌套是指将两个或多个不同的类型层层包裹在一个值周围,仿佛呵护新生儿一般。
Right(Maybe('b'));
IO(Task(IO(1000)));
[Identity('bee thousand')];
到目前为止,我们通过精心设计的例子成功避开了这种常见场景,但在实践中,随着编码的进行,类型往往会像驱魔仪式中的耳机线一样缠作一团。如果我们不随时细致地整理类型,我们的代码读起来会比猫咖啡馆里的垮掉派诗人还要毛茸茸。
一出情景喜剧
// getValue :: Selector -> Task Error (Maybe String)
// postComment :: String -> Task Error Comment
// validate :: String -> Either ValidationError String
// saveComment :: () -> Task Error (Maybe (Either ValidationError (Task Error Comment)))
const saveComment = compose(
map(map(map(postComment))),
map(map(validate)),
getValue('#comment'),
);
所有角色都到齐了,这让我们的类型签名(type signature)大为懊恼。请允许我简要解释一下这段代码。我们首先通过 getValue('#comment') 获取用户输入,这是一个检索元素文本的操作。现在,它在查找元素时可能会出错,或者值字符串可能不存在,所以它返回 Task Error (Maybe String)。之后,我们必须同时对 Task 和 Maybe 进行 map 操作,将文本传递给 validate,而 validate 反过来会给我们一个 Either ValidationError 或我们的 String。然后是一连串的 map 操作,将我们当前 Task Error (Maybe (Either ValidationError String)) 中的 String 发送给 postComment,后者返回我们最终的 Task。
真是一团糟。这是抽象类型的大杂烩、业余类型表现主义、多态(polymorphic)的波洛克、整体的蒙德里安。对于这个常见问题有很多解决方案。我们可以将类型组合(compose)成一个庞大的容器、排序并 join(合并)其中几个、将它们同质化(homogenize)、解构它们等等。在本章中,我们将专注于通过自然变换来同质化它们。
全都自然
自然变换是“函子(functors)之间的态射(morphism)”,也就是说,是作用于容器本身的函数。从类型上看,它是一个函数 (Functor f, Functor g) => f a -> g a。它的特别之处在于,我们无论如何都不能窥视函子内部的内容。可以把它想象成高度机密信息的交换——双方都不知道盖有“绝密”印章的马尼拉信封里装的是什么。这是一种结构性操作。一种函子式的服装更换。形式上,自然变换是任何满足以下条件的函数:

或者用代码表示:
// nt :: (Functor f, Functor g) => f a -> g a
// nt 是一个自然变换函数
compose(map(f), nt) === compose(nt, map(f));
图示和代码都表达了相同的意思:我们可以先运行自然变换然后 map,或者先 map 然后运行自然变换,得到的结果是相同的。顺便一提,这可以从一个自由定理(free theorem)推导出来,尽管自然变换(和函子)并不局限于作用于类型的函数。
有原则的类型转换
作为程序员,我们熟悉类型转换。我们将 Strings 转换为 Booleans,将 Integers 转换为 Floats(尽管 JavaScript 只有 Numbers)。这里的区别仅仅在于我们处理的是代数容器,并且我们有一些理论可供利用。
让我们看一些例子:
// idToMaybe :: Identity a -> Maybe a
const idToMaybe = x => Maybe.of(x.$value);
// idToIO :: Identity a -> IO a
const idToIO = x => IO.of(x.$value);
// eitherToTask :: Either a b -> Task a b
const eitherToTask = either(Task.rejected, Task.of);
// ioToTask :: IO a -> Task () a
const ioToTask = x => new Task((reject, resolve) => resolve(x.unsafePerform()));
// maybeToTask :: Maybe a -> Task () a
const maybeToTask = x => (x.isNothing ? Task.rejected() : Task.of(x.$value));
// arrayToMaybe :: [a] -> Maybe a
const arrayToMaybe = x => Maybe.of(x[0]);
明白了吗?我们只是将一个函子变成另一个。在这个过程中允许丢失信息,只要我们将来要 map 的那个值不会在形态变换的混乱中丢失就行。这正是关键所在:根据我们的定义,即使在转换之后,map 也必须能够继续进行。
一种看待它的方式是,我们正在转换我们的效果(effects)。从这个角度看,我们可以将 ioToTask 视为将同步转换为异步,或者将 arrayToMaybe 从非确定性转换为可能的失败。请注意,在 JavaScript 中我们无法将异步转换为同步,所以我们不能编写 taskToIO ——那将是一种超自然(supernatural)转换。
特性嫉妒
假设我们想使用另一个类型(比如 List)的某些特性,例如 sortBy。自然变换提供了一种很好的方式来转换到目标类型,同时确保我们的 map 操作仍然可靠。
// arrayToList :: [a] -> List a
const arrayToList = List.of;
const doListyThings = compose(sortBy(h), filter(g), arrayToList, map(f));
const doListyThings_ = compose(sortBy(h), filter(g), map(f), arrayToList); // 应用定律
鼻子一动,魔杖轻点三下,放入 arrayToList,瞧!我们的 [a] 就变成了一个 List a,如果愿意,我们就可以 sortBy 了。
此外,通过将 map(f) 移动到自然变换的左侧(如 doListyThings_ 所示),可以更容易地优化/融合操作。
同构的 JavaScript
当我们可以在两种类型之间来回转换而完全不丢失任何信息时,这被认为是同构(isomorphism)。这只是“持有相同数据”的一个花哨说法。如果我们可以提供“到”(to)和“从”(from)的自然变换作为证明,我们就说这两种类型是同构的:
// promiseToTask :: Promise a b -> Task a b
const promiseToTask = x => new Task((reject, resolve) => x.then(resolve).catch(reject));
// taskToPromise :: Task a b -> Promise a b
const taskToPromise = x => new Promise((resolve, reject) => x.fork(reject, resolve));
const x = Promise.resolve('ring');
taskToPromise(promiseToTask(x)) === x; // true
const y = Task.of('rabbit');
promiseToTask(taskToPromise(y)) === y; // true
Q.E.D. Promise 和 Task 是同构的。我们也可以编写一个 listToArray 来补充我们的 arrayToList,并证明它们也是同构的。作为一个反例,arrayToMaybe 不是一个同构,因为它会丢失信息:
// maybeToArray :: Maybe a -> [a]
const maybeToArray = x => (x.isNothing ? [] : [x.$value]);
// arrayToMaybe :: [a] -> Maybe a
const arrayToMaybe = x => Maybe.of(x[0]);
const x = ['elvis costello', 'the attractions'];
// 非同构
maybeToArray(arrayToMaybe(x)); // ['elvis costello']
// 但是一个自然变换
compose(arrayToMaybe, map(replace('elvis', 'lou')))(x); // Just('lou costello')
// ==
compose(map(replace('elvis', 'lou')), arrayToMaybe)(x); // Just('lou costello')
然而,它们确实是自然变换,因为在任意一边进行 map 操作都会得到相同的结果。我在本章讲到这里时顺便提到了同构,但别被这迷惑了,它们是一个极其强大且普遍的概念。不管怎样,我们继续。
更广泛的定义
这些结构性函数绝不局限于类型转换。
这里有几个不同的例子:
reverse :: [a] -> [a]
join :: (Monad m) => m (m a) -> m a
head :: [a] -> a
of :: a -> f a
自然变换定律对这些函数同样适用。可能让你困惑的一点是 head :: [a] -> a 可以被视为 head :: [a] -> Identity a。在证明定律时,我们可以随心所欲地插入 Identity,因为我们反过来可以证明 a 与 Identity a 是同构的(看吧,我告诉过你同构无处不在)。
一种嵌套解决方案
回到我们那喜剧般的类型签名。我们可以在调用代码中散布一些自然变换来强制转换每个不同的类型,使它们统一,从而可以 join。
// getValue :: Selector -> Task Error (Maybe String)
// postComment :: String -> Task Error Comment
// validate :: String -> Either ValidationError String
// saveComment :: () -> Task Error Comment
const saveComment = compose(
chain(postComment),
chain(eitherToTask), // Either -> Task 然后 join
map(validate),
chain(maybeToTask), // Maybe -> Task 然后 join
getValue('#comment'),
);
那么这里我们做了什么?我们仅仅添加了 chain(maybeToTask) 和 chain(eitherToTask)。两者效果相同;它们都自然地将我们 Task 持有的函子转换为另一个 Task,然后 join 这两个 Task。就像窗台上的防鸟刺一样,我们从源头上避免了嵌套。正如光明之城的人们所说,“Mieux vaut prévenir que guérir”(预防胜于治疗)——一分预防胜过十分治疗。
总结
自然变换是作用于我们函子本身的函数。它们是范畴论中极其重要的概念,并且随着更多抽象概念的引入将开始无处不在,但目前,我们已将它们的应用范围限定在几个具体的场景中。正如我们所见,我们可以通过转换类型来达到不同的效果,并保证我们的组合(composition)是可靠的。它们也可以帮助我们处理嵌套类型,尽管它们的普遍效果是将我们的函子同质化到最低的共同标准,这在实践中通常是具有最易变效果的函子(在大多数情况下是 Task)。
这种持续而乏味的类型整理是我们为具象化这些类型——从以太中召唤它们——所付出的代价。当然,隐式的副作用要阴险得多,所以我们在这里进行着正义的斗争。在能够驾驭更大型的类型融合之前,我们还需要工具箱里有更多的工具。接下来,我们将学习使用 Traversable 来重新排序我们的类型。
练习
const eitherToMaybe = either(always(nothing), Maybe.of);
/* globals eitherToMaybe */
const just = eitherToMaybe(Either.of('one eyed willy'));
const noth = eitherToMaybe(left('some error'));
assert(
just instanceof Maybe && just.isJust && just.$value === 'one eyed willy',
'The function maps the `Right()` side incorrectly; hint: `Right(14)` should be mapped to `Just(14)`',
);
assert(
noth instanceof Maybe && noth.isNothing,
'The function maps the `Left()` side incorrectly; hint: `Left(\'error\')` should be mapped to `Nothing`',
);
// 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,
};
}
// eitherToTask :: Either a b -> Task a b
const eitherToTask = either(Task.rejected, Task.of);
const findNameById = compose(map(prop('name')), chain(eitherToTask), findUserById);
/* globals findNameById */
const throwUnexpected = () => {
throw new Error('The function gives incorrect results; a Task has resolved unexpectedly!');
};
const res = findNameById(1);
assert(
res instanceof Task,
'The function has an incorrect type; hint: `findNameById` must return a `Task String User`!',
);
res.fork(throwUnexpected, (val) => {
assert(
!(val instanceof Task),
'The function has an incorrect type; hint: `findNameById` must return a `Task String User`, make sure to flatten any nested Functor!',
);
assert(
!(val instanceof Either),
'The function has an incorrect type; hint: did you use `eitherToTask` ?',
);
assert(
val === 'Albert',
'The function gives incorrect results for the `Right` side of `Either`.',
);
});
const rej = findNameById(999);
assert(
rej instanceof Task,
'The function has an incorrect type; hint: `findNameById` must return a `Task String User`!',
);
rej.fork((val) => {
assert(
!(val instanceof Task),
'The function has an incorrect type; hint: `findNameById` must return a `Task String User`, make sure to flatten any nested Functor!',
);
assert(
val === 'not found',
'The function gives incorrect results for the `Left` side of `Either`.',
);
}, throwUnexpected);
// 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,
};
}
提醒一下,以下函数在练习的上下文中可用:
split :: String -> String -> [String]
intercalate :: String -> [String] -> String
const strToList = split('');
const listToStr = intercalate('');
/* globals strToList, listToStr */
const sortLetters = compose(listToStr, sortBy(identity), strToList);
assert(
sortLetters('sortme') === 'emorst',
'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,
};
}