Chapter 10: Applicative Functors
Applying Applicatives
The name applicative functor is pleasantly descriptive given its functional origins. Functional programmers are notorious for coming up with names like mappend
or liftA4
, which seem perfectly natural when viewed in the math lab, but hold the clarity of an indecisive Darth Vader at the drive thru in any other context.
Anyhow, the name should spill the beans on what this interface gives us: the ability to apply functors to each other.
Now, why would a normal, rational person such as yourself want such a thing? What does it even mean to apply one functor to another?
To answer these questions, we'll start with a situation you may have already encountered in your functional travels. Let's say, hypothetically, that we have two functors (of the same type) and we'd like to call a function with both of their values as arguments. Something simple like adding the values of two Container
s.
// We can't do this because the numbers are bottled up.
add(Container.of(2), Container.of(3));
// NaN
// Let's use our trusty map
const containerOfAdd2 = map(add, Container.of(2));
// Container(add(2))
We have ourselves a Container
with a partially applied function inside. More specifically, we have a Container(add(2))
and we'd like to apply its add(2)
to the 3
in Container(3)
to complete the call. In other words, we'd like to apply one functor to another.
Now, it just so happens that we already have the tools to accomplish this task. We can chain
and then map
the partially applied add(2)
like so:
Container.of(2).chain(two => Container.of(3).map(add(two)));
The issue here is that we are stuck in the sequential world of monads wherein nothing may be evaluated until the previous monad has finished its business. We have ourselves two strong, independent values and I should think it unnecessary to delay the creation of Container(3)
merely to satisfy the monad's sequential demands.
In fact, it would be lovely if we could succinctly apply one functor's contents to another's value without these needless functions and variables should we find ourselves in this pickle jar.
Ships in Bottles
ap
is a function that can apply the function contents of one functor to the value contents of another. Say that five times fast.
Container.of(add(2)).ap(Container.of(3));
// Container(5)
// all together now
Container.of(2).map(add).ap(Container.of(3));
// Container(5)
There we are, nice and neat. Good news for Container(3)
as it's been set free from the jail of the nested monadic function. It's worth mentioning again that add
, in this case, gets partially applied during the first map
so this only works when add
is curried.
We can define ap
like so:
Container.prototype.ap = function (otherContainer) {
return otherContainer.map(this.$value);
};
Remember, this.$value
will be a function and we'll be accepting another functor so we need only map
it. And with that we have our interface definition:
An applicative functor is a pointed functor with an
ap
method
Note the dependence on pointed. The pointed interface is crucial here as we'll see throughout the following examples.
Now, I sense your skepticism (or perhaps confusion and horror), but keep an open mind; this ap
character will prove useful. Before we get into it, let's explore a nice property.
F.of(x).map(f) === F.of(f).ap(F.of(x));
In proper English, mapping f
is equivalent to ap
ing a functor of f
. Or in properer English, we can place x
into our container and map(f)
OR we can lift both f
and x
into our container and ap
them. This allows us to write in a left-to-right fashion:
Maybe.of(add).ap(Maybe.of(2)).ap(Maybe.of(3));
// Maybe(5)
Task.of(add).ap(Task.of(2)).ap(Task.of(3));
// Task(5)
One might even recognise the vague shape of a normal function call if viewed mid squint. We'll look at the pointfree version later in the chapter, but for now, this is the preferred way to write such code. Using of
, each value gets transported to the magical land of containers, this parallel universe where each application can be async or null or what have you and ap
will apply functions within this fantastical place. It's like building a ship in a bottle.
Did you see there? We used Task
in our example. This is a prime situation where applicative functors pull their weight. Let's look at a more in-depth example.
Coordination Motivation
Say we're building a travel site and we'd like to retrieve both a list of tourist destinations and local events. Each of these are separate, stand-alone api calls.
// Http.get :: String -> Task Error HTML
const renderPage = curry((destinations, events) => { /* render page */ });
Task.of(renderPage).ap(Http.get('/destinations')).ap(Http.get('/events'));
// Task("<div>some page with dest and events</div>")
Both Http
calls will happen instantly and renderPage
will be called when both are resolved. Contrast this with the monadic version where one Task
must finish before the next fires off. Since we don't need the destinations to retrieve events, we are free from sequential evaluation.
Again, because we're using partial application to achieve this result, we must ensure renderPage
is curried or it will not wait for both Tasks
to finish. Incidentally, if you've ever had to do such a thing manually, you'll appreciate the astonishing simplicity of this interface. This is the kind of beautiful code that takes us one step closer to the singularity.
Let's look at another example.
// $ :: String -> IO DOM
const $ = selector => new IO(() => document.querySelector(selector));
// getVal :: String -> IO String
const getVal = compose(map(prop('value')), $);
// signIn :: String -> String -> Bool -> User
const signIn = curry((username, password, rememberMe) => { /* signing in */ });
IO.of(signIn).ap(getVal('#email')).ap(getVal('#password')).ap(IO.of(false));
// IO({ id: 3, email: 'gg@allin.com' })
signIn
is a curried function of 3 arguments so we have to ap
accordingly. With each ap
, signIn
receives one more argument until it is complete and runs. We can continue this pattern with as many arguments as necessary. Another thing to note is that two arguments end up naturally in IO
whereas the last one needs a little help from of
to lift it into IO
since ap
expects the function and all its arguments to be in the same type.
Bro, Do You Even Lift?
Let's examine a pointfree way to write these applicative calls. Since we know map
is equal to of/ap
, we can write generic functions that will ap
as many times as we specify:
const liftA2 = curry((g, f1, f2) => f1.map(g).ap(f2));
const liftA3 = curry((g, f1, f2, f3) => f1.map(g).ap(f2).ap(f3));
// liftA4, etc
liftA2
is a strange name. It sounds like one of the finicky freight elevators in a rundown factory or a vanity plate for a cheap limo company. Once enlightened, however, it's self explanatory: lift these pieces into the applicative functor world.
When I first saw this 2-3-4 nonsense it struck me as ugly and unnecessary. After all, we can check the arity of functions in JavaScript and build this up dynamically. However, it is often useful to partially apply liftA(N)
itself, so it cannot vary in argument length.
Let's see this in use:
// checkEmail :: User -> Either String Email
// checkName :: User -> Either String String
const user = {
name: 'John Doe',
email: 'blurp_blurp',
};
// createUser :: Email -> String -> IO User
const createUser = curry((email, name) => { /* creating... */ });
Either.of(createUser).ap(checkEmail(user)).ap(checkName(user));
// Left('invalid email')
liftA2(createUser, checkEmail(user), checkName(user));
// Left('invalid email')
Since createUser
takes two arguments, we use the corresponding liftA2
. The two statements are equivalent, but the liftA2
version has no mention of Either
. This makes it more generic and flexible since we are no longer married to a specific type.
Let's see the previous examples written this way:
liftA2(add, Maybe.of(2), Maybe.of(3));
// Maybe(5)
liftA2(renderPage, Http.get('/destinations'), Http.get('/events'));
// Task('<div>some page with dest and events</div>')
liftA3(signIn, getVal('#email'), getVal('#password'), IO.of(false));
// IO({ id: 3, email: 'gg@allin.com' })
Operators
In languages like Haskell, Scala, PureScript, and Swift, where it is possible to create your own infix operators you may see syntax like this:
-- Haskell / PureScript
add <$> Right 2 <*> Right 3
// JavaScript
map(add, Right(2)).ap(Right(3));
It's helpful to know that <$>
is map
(aka fmap
) and <*>
is just ap
. This allows for a more natural function application style and can help remove some parenthesis.
Free Can Openers
We haven't spoken much about derived functions. Seeing as all of these interfaces are built off of each other and obey a set of laws, we can define some weaker interfaces in terms of the stronger ones.
For instance, we know that an applicative is first a functor, so if we have an applicative instance, surely we can define a functor for our type.
This kind of perfect computational harmony is possible because we're working within a mathematical framework. Mozart couldn't have done better even if he had torrented Ableton as a child.
I mentioned earlier that of/ap
is equivalent to map
. We can use this knowledge to define map
for free:
// map derived from of/ap
X.prototype.map = function map(f) {
return this.constructor.of(f).ap(this);
};
Monads are at the top of the food chain, so to speak, so if we have chain
, we get functor and applicative for free:
// map derived from chain
X.prototype.map = function map(f) {
return this.chain(a => this.constructor.of(f(a)));
};
// ap derived from chain/map
X.prototype.ap = function ap(other) {
return this.chain(f => other.map(f));
};
If we can define a monad, we can define both the applicative and functor interfaces. This is quite remarkable as we get all of these can openers for free. We can even examine a type and automate this process.
It should be pointed out that part of ap
's appeal is the ability to run things concurrently so defining it via chain
is missing out on that optimization. Despite that, it's good to have an immediate working interface while one works out the best possible implementation.
Why not just use monads and be done with it, you ask? It's good practice to work with the level of power you need, no more, no less. This keeps cognitive load to a minimum by ruling out possible functionality. For this reason, it's good to favor applicatives over monads.
Monads have the unique ability to sequence computation, assign variables, and halt further execution all thanks to the downward nesting structure. When one sees applicatives in use, they needn't concern themselves with any of that business.
Now, on to the legalities ...
Laws
Like the other mathematical constructs we've explored, applicative functors hold some useful properties for us to rely on in our daily code. First off, you should know that applicatives are "closed under composition", meaning ap
will never change container types on us (yet another reason to favor over monads). That's not to say we cannot have multiple different effects - we can stack our types knowing that they will remain the same during the entirety of our application.
To demonstrate:
const tOfM = compose(Task.of, Maybe.of);
liftA2(liftA2(concat), tOfM('Rainy Days and Mondays'), tOfM(' always get me down'));
// Task(Maybe(Rainy Days and Mondays always get me down))
See, no need to worry about different types getting in the mix.
Time to look at our favorite categorical law: identity:
Identity
// identity
A.of(id).ap(v) === v;
Right, so applying id
all from within a functor shouldn't alter the value in v
. For example:
const v = Identity.of('Pillow Pets');
Identity.of(id).ap(v) === v;
Identity.of(id)
makes me chuckle at its futility. Anyway, what's interesting is that, as we've already established, of/ap
is the same as map
so this law follows directly from functor identity: map(id) == id
.
The beauty in using these laws is that, like a militant kindergarten gym coach, they force all of our interfaces to play well together.
Homomorphism
// homomorphism
A.of(f).ap(A.of(x)) === A.of(f(x));
A homomorphism is just a structure preserving map. In fact, a functor is just a homomorphism between categories as it preserves the original category's structure under the mapping.
We're really just stuffing our normal functions and values into a container and running the computation in there so it should come as no surprise that we will end up with the same result if we apply the whole thing inside the container (left side of the equation) or apply it outside, then place it in there (right side).
A quick example:
Either.of(toUpperCase).ap(Either.of('oreos')) === Either.of(toUpperCase('oreos'));
Interchange
The interchange law states that it doesn't matter if we choose to lift our function into the left or right side of ap
.
// interchange
v.ap(A.of(x)) === A.of(f => f(x)).ap(v);
Here is an example:
const v = Task.of(reverse);
const x = 'Sparklehorse';
v.ap(Task.of(x)) === Task.of(f => f(x)).ap(v);
Composition
And finally composition which is just a way to check that our standard function composition holds when applying inside of containers.
// composition
A.of(compose).ap(u).ap(v).ap(w) === u.ap(v.ap(w));
const u = IO.of(toUpperCase);
const v = IO.of(concat('& beyond'));
const w = IO.of('blood bath ');
IO.of(compose).ap(u).ap(v).ap(w) === u.ap(v.ap(w));
In Summary
A good use case for applicatives is when one has multiple functor arguments. They give us the ability to apply functions to arguments all within the functor world. Though we could already do so with monads, we should prefer applicative functors when we aren't in need of monadic specific functionality.
We're almost finished with container apis. We've learned how to map
, chain
, and now ap
functions. In the next chapter, we'll learn how to work better with multiple functors and disassemble them in a principled way.
Chapter 11: Transformation Again, Naturally
Exercises
const safeAdd = curry((a, b) => Maybe.of(add).ap(a).ap(b));
/* globals safeAdd */ const res = safeAdd(Maybe.of(2), Maybe.of(3)); assert( res instanceof Maybe && typeof res.$value === 'number', 'The function has a wrong type; make sure to wrap your numbers inside `Maybe.of`', ); assert( safeAdd(Maybe.of(2), Maybe.of(3)).$value === 5, 'The function gives incorrect results; did you use `add` ?', ); assert( safeAdd(Maybe.of(null), Maybe.of(3)).isNothing, 'The function gives incorrect results; `Nothing` should be returned when at least one value is `null`', ); assert( safeAdd(Maybe.of(2), Maybe.of(null)).isNothing, 'The function gives incorrect results; `Nothing` should be returned when at least one value is `null`', ); assert( withSpyOn('ap', Maybe.prototype, () => safeAdd(Maybe.of(2), Maybe.of(3))), 'The function seems incorrect; did you use `ap` ?', );
// 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, }; }
const safeAdd = liftA2(add);
/* globals safeAdd */ const res = safeAdd(Maybe.of(2), Maybe.of(3)); assert( res instanceof Maybe && typeof res.$value === 'number', 'The function has a wrong type; make sure to wrap your numbers inside `Maybe.of`', ); assert( safeAdd(Maybe.of(2), Maybe.of(3)).$value === 5, 'The function gives incorrect results; did you use `add` ?', ); assert( safeAdd(Maybe.of(null), Maybe.of(3)).isNothing, 'The function gives incorrect results; `Nothing` should be returned when at least one value is `null`', ); assert( safeAdd(Maybe.of(2), Maybe.of(null)).isNothing, 'The function gives incorrect results; `Nothing` should be returned when at least one value is `null`', ); assert( liftA2.partially && safeAdd.name === 'liftA2', 'The function seems incorrect; did you use `liftA2`? Remember that `liftA2` is currified!', );
// 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, }; }
For the next exercise, we consider the following helpers:
const localStorage = {
player1: { id:1, name: 'Albert' },
player2: { id:2, name: 'Theresa' },
};
// getFromCache :: String -> IO User
const getFromCache = x => new IO(() => localStorage[x]);
// game :: User -> User -> String
const game = curry((p1, p2) => `${p1.name} vs ${p2.name}`);
const startGame = IO.of(game) .ap(getFromCache('player1')) .ap(getFromCache('player2'));
/* globals startGame */ assert( startGame instanceof IO && typeof startGame.unsafePerformIO() === 'string', 'The answer has a wrong type; `startGame` should be an `IO String`', ); assert( startGame.unsafePerformIO() === `${albert.name} vs ${theresa.name}`, 'The answer 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, }; }