Chapter 12: Traversing the Stone
So far, in our cirque du conteneur, you've seen us tame the ferocious functor, bending it to our will to perform any operation that strikes our fancy. You've been dazzled by the juggling of many dangerous effects at once using function application to collect the results. Sat there in amazement as containers vanished in thin air by joining them together. At the side effect sideshow, we've seen them composed into one. And most recently, we've ventured beyond what's natural and transformed one type into another before your very eyes.
And now for our next trick, we'll look at traversals. We'll watch types soar over one another as if they were trapeze artists holding our value intact. We'll reorder effects like the trolleys in a tilt-a-whirl. When our containers get intertwined like the limbs of a contortionist, we can use this interface to straighten things out. We'll witness different effects with different orderings. Fetch me my pantaloons and slide whistle, let's get started.
Types n' Types
Let's get weird:
// 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('hail the monarchy'), Task('smash the patriarchy')]
Here we read a bunch of files and end up with a useless array of tasks. How might we fork each one of these? It would be most agreeable if we could switch the types around to have Task Error [String]
instead of [Task Error String]
. That way, we'd have one future value holding all the results, which is much more amenable to our async needs than several future values arriving at their leisure.
Here's one last example of a sticky situation:
// getAttribute :: String -> Node -> Maybe String
// $ :: Selector -> IO Node
// getControlNode :: Selector -> IO (Maybe (IO Node))
const getControlNode = compose(map(map($)), map(getAttribute('aria-controls')), $);
Look at those IO
s longing to be together. It'd be just lovely to join
them, let them dance cheek to cheek, but alas a Maybe
stands between them like a chaperone at prom. Our best move here would be to shift their positions next to one another, that way each type can be together at last and our signature can be simplified to IO (Maybe Node)
.
Type Feng Shui
The Traversable interface consists of two glorious functions: sequence
and traverse
.
Let's rearrange our types using 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'))
See what has happened here? Our nested type gets turned inside out like a pair of leather trousers on a humid summer night. The inner functor is shifted to the outside and vice versa. It should be known that sequence
is bit particular about its arguments. It looks like this:
// sequence :: (Traversable t, Applicative f) => (a -> f a) -> t (f a) -> f (t a)
const sequence = curry((of, x) => x.sequence(of));
Let's start with the second argument. It must be a Traversable holding an Applicative, which sounds quite restrictive, but just so happens to be the case more often than not. It is the t (f a)
which gets turned into a f (t a)
. Isn't that expressive? It's clear as day the two types do-si-do around each other. That first argument there is merely a crutch and only necessary in an untyped language. It is a type constructor (our of) provided so that we can invert map-reluctant types like Left
- more on that in a minute.
Using sequence
, we can shift types around with the precision of a sidewalk thimblerigger. But how does it work? Let's look at how a type, say Either
, would implement it:
class Right extends Either {
// ...
sequence(of) {
return this.$value.map(Either.of);
}
}
Ah yes, if our $value
is a functor (it must be an applicative, in fact), we can simply map
our constructor to leap frog the type.
You may have noticed that we've ignored the of
entirely. It is passed in for the occasion where mapping is futile, as is the case with Left
:
class Left extends Either {
// ...
sequence(of) {
return of(this);
}
}
We'd like the types to always end up in the same arrangement, therefore it is necessary for types like Left
who don't actually hold our inner applicative to get a little help in doing so. The Applicative interface requires that we first have a Pointed Functor so we'll always have a of
to pass in. In a language with a type system, the outer type can be inferred from the signature and does not need to be explicitly given.
Effect Assortment
Different orders have different outcomes where our containers are concerned. If I have [Maybe a]
, that's a collection of possible values whereas if I have a Maybe [a]
, that's a possible collection of values. The former indicates we'll be forgiving and keep "the good ones", while the latter means it's an "all or nothing" type of situation. Likewise, Either Error (Task Error a)
could represent a client side validation and Task Error (Either Error a)
could be a server side one. Types can be swapped to give us different effects.
// 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));
Here we have two different functions based on if we map
or traverse
. The first, partition
will give us an array of Left
s and Right
s according to the predicate function. This is useful to keep precious data around for future use rather than filtering it out with the bathwater. validate
instead will give us the first item that fails the predicate in Left
, or all the items in Right
if everything is hunky dory. By choosing a different type order, we get different behavior.
Let's look at the traverse
function of List
, to see how the validate
method is made.
traverse(of, fn) {
return this.$value.reduce(
(f, a) => fn(a).map(b => bs => bs.concat(b)).ap(f),
of(new List([])),
);
}
This just runs a reduce
on the list. The reduce function is (f, a) => fn(a).map(b => bs => bs.concat(b)).ap(f)
, which looks a bit scary, so let's step through it.
reduce(..., ...)
Remember the signature of
reduce :: [a] -> (f -> a -> f) -> f -> f
. The first argument is actually provided by the dot-notation on$value
, so it's a list of things. Then we need a function from af
(the accumulator) and aa
(the iteree) to return us a new accumulator.of(new List([]))
The seed value is
of(new List([]))
, which in our case isRight([]) :: Either e [a]
. Notice thatEither e [a]
will also be our final resulting type!fn :: Applicative f => a -> f a
If we apply it to our example above,
fn
is actuallyfromPredicate(f) :: a -> Either e a
.fn(a) :: Either e a
.map(b => bs => bs.concat(b))
When
Right
,Either.map
passes the right value to the function and returns a newRight
with the result. In this case the function has one parameter (b
), and returns another function (bs => bs.concat(b)
, whereb
is in scope due to the closure). WhenLeft
, the left value is returned.fn(a).map(b => bs => bs.concat(b)) :: Either e ([a] -> [a])
.
ap(f)
Remember that
f
is an Applicative here, so we can apply the functionbs => bs.concat(b)
to whatever valuebs :: [a]
is inf
. Fortunately for us,f
comes from our initial seed and has the following type:f :: Either e [a]
which is by the way, preserved when we applybs => bs.concat(b)
. Whenf
isRight
, this callsbs => bs.concat(b)
, which returns aRight
with the item added to the list. WhenLeft
, the left value (from the previous step or previous iteration respectively) is returned.fn(a).map(b => bs => bs.concat(b)).ap(f) :: Either e [a]
This apparently miraculous transformation is achieved with just 6 measly lines of code in List.traverse
, and is accomplished with of
, map
and ap
, so will work for any Applicative Functor. This is a great example
of how those abstraction can help to write highly generic code with only a few assumptions (that can, incidentally, be declared and checked at the type level!).
Waltz of the Types
Time to revisit and clean our initial examples.
// 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(['hail the monarchy', 'smash the patriarchy']);
Using traverse
instead of map
, we've successfully herded those unruly Task
s into a nice coordinated array of results. This is like Promise.all()
, if you're familiar, except it isn't just a one-off, custom function, no, this works for any traversable type. These mathematical apis tend to capture most things we'd like to do in an interoperable, reusable way, rather than each library reinventing these functions for a single type.
Let's clean up the last example for closure (no, not that kind):
// getAttribute :: String -> Node -> Maybe String
// $ :: Selector -> IO Node
// getControlNode :: Selector -> IO (Maybe Node)
const getControlNode = compose(chain(traverse(IO.of, $)), map(getAttribute('aria-controls')), $);
Instead of map(map($))
we have chain(traverse(IO.of, $))
which inverts our types as it maps then flattens the two IO
s via chain
.
No Law and Order
Well now, before you get all judgemental and bang the backspace button like a gavel to retreat from the chapter, take a moment to recognize that these laws are useful code guarantees. 'Tis my conjecture that the goal of most program architecture is an attempt to place useful restrictions on our code to narrow the possibilities, to guide us into the answers as designers and readers.
An interface without laws is merely indirection. Like any other mathematical structure, we must expose properties for our own sanity. This has a similar effect as encapsulation since it protects the data, enabling us to swap out the interface with another law abiding citizen.
Come along now, we've got some laws to suss out.
Identity
const identity1 = compose(sequence(Identity.of), map(Identity.of));
const identity2 = Identity.of;
// test it out with Right
identity1(Either.of('stuff'));
// Identity(Right('stuff'))
identity2(Either.of('stuff'));
// Identity(Right('stuff'))
This should be straightforward. If we place an Identity
in our functor, then turn it inside out with sequence
that's the same as just placing it on the outside to begin with. We chose Right
as our guinea pig as it is easy to try the law and inspect. An arbitrary functor there is normal, however, the use of a concrete functor here, namely Identity
in the law itself might raise some eyebrows. Remember a category is defined by morphisms between its objects that have associative composition and identity. When dealing with the category of functors, natural transformations are the morphisms and Identity
is, well identity. The Identity
functor is as fundamental in demonstrating laws as our compose
function. In fact, we should give up the ghost and follow suit with our Compose type:
Composition
const comp1 = compose(sequence(Compose.of), map(Compose.of));
const comp2 = (Fof, Gof) => compose(Compose.of, map(sequence(Gof)), sequence(Fof));
// Test it out with some types we have lying around
comp1(Identity(Right([true])));
// Compose(Right([Identity(true)]))
comp2(Either.of, Array)(Identity(Right([true])));
// Compose(Right([Identity(true)]))
This law preserves composition as one would expect: if we swap compositions of functors, we shouldn't see any surprises since the composition is a functor itself. We arbitrarily chose true
, Right
, Identity
, and Array
to test it out. Libraries like quickcheck or jsverify can help us test the law by fuzz testing the inputs.
As a natural consequence of the above law, we get the ability to fuse traversals, which is nice from a performance standpoint.
Naturality
const natLaw1 = (of, nt) => compose(nt, sequence(of));
const natLaw2 = (of, nt) => compose(sequence(of), map(nt));
// test with a random natural transformation and our friendly Identity/Right functors.
// 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'))
This is similar to our identity law. If we first swing the types around then run a natural transformation on the outside, that should equal mapping a natural transformation, then flipping the types.
A natural consequence of this law is:
traverse(A.of, A.of) === A.of;
Which, again, is nice from a performance standpoint.
In Summary
Traversable is a powerful interface that gives us the ability to rearrange our types with the ease of a telekinetic interior decorator. We can achieve different effects with different orders as well as iron out those nasty type wrinkles that keep us from join
ing them down. Next, we'll take a bit of a detour to see one of the most powerful interfaces of functional programming and perhaps even algebra itself: Monoids bring it all together
Exercises
Considering the following elements:
// 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, }; }
We now define the following validation function:
// validate :: Player -> Either String Player
const validate = player => (player.name ? Either.of(player) : left('must have name'));
// 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, }; }
Finally, we consider some file-system helpers:
// 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, }; }