Helper Functions¶
Here is a collection of helper functions used across the examples. You can always download and read the full source code from Gitlab.
log¶
import { jest } from '@jest/globals'
import { log } from './log';
//
// Beside spying, we also `mockImplementation()` to prevent the logging
// to actually happen and pollute our unit testing output.
//
describe('when not args are provided', () => {
beforeEach(() => {
jest.spyOn(global.console, 'log').mockImplementation();
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should only log the divider with the empty line', () => {
log();
const calls = console.log.mock.calls;
expect(calls[0][0]).toEqual('-'.repeat(32));
expect(calls[1][0]).toBe(undefined);
jest.restoreAllMocks();
});
it('should log devidier, params, and empty line', () => {
log('hello', 'world', 2 + 3);
const calls = console.log.mock.calls;
expect(calls[0][0]).toEqual('-'.repeat(32));
expect(calls[1][0]).toEqual('hello');
expect(calls[2][0]).toEqual('world');
expect(calls[3][0]).toEqual(5);
expect(calls[4][0]).toEqual(undefined);
});
});
/**
* Log the arguments to the console.
*
* I mostly use this when I am playing with some idea and I run the npm
* scripts (see package.json) like this:
*
* $ npm run file vidN/foo.js
*
* or
*
* $ npm run watch vidN/foo.js
*
* Especially with watch, I then have delimiting “----”-like line to
* help visualize where each log starts.
*
* @param {...any} args Zero or more values to log.
* @return {Array<args>}.
*
* @example
* log('hey');
* // → --------------------------------
* // → hey
*
* @example
* log('h4ck3r, [1, 2]);
* // → --------------------------------
* // → h4ck3r
* // → [ 1, 2 ]
*/
function log(...args) {
console.log('-'.repeat(32));
args.forEach(function logArg(arg) {
console.log(arg);
});
// Print an empty line.
console.log();
return args;
}
export { log };
id (identity)¶
import { id } from './id';
describe('returns its input', () => {
[
[1, '1'],
[null, 'null'],
[undefined, 'undefined'],
['', "'1'"],
[{ foo: 'bar' }, "{ foo: 'bar' }"],
[[10, 20], '[10, 20]'],
].forEach(([value, description]) => {
it(`should output its input ${description}`, () => {
expect(id(value)).toEqual(value);
});
});
});
/**
* The identity function.
*
* Simply returns the input untouched.
*
* @param {any} x
* @return {any} x
*
* @example
* id(1);
* // → 1
*
* id({ jedi: 'Yoda' });
* // → { jedi: 'Yoda' }
*/
function id(x) {
return x;
}
export { id }
isNil¶
import { isNil } from './isNil';
describe('isNil()', () => {
//
// We say “empty array/object” with a loose interpretation of “empty",
// because we know they inherit some methods and properties. We mean
// “empty” in the sense that we didn't explicitly add values to them
// ourselves.
//
[
[undefined, 'undefined', true],
[null, 'null', true],
['', 'empty string', false],
[0, '0 (zero)', false],
[[], '[] (empty array)', false],
[{}, '{} (empty object)', false],
[NaN, 'NaN', false],
].forEach(function checkIsNil([input, description, expected]) {
it(`should return ${expected} for ${description}`, () => {
expect(isNil(input)).toEqual(expected);
});
});
});
/**
* Checks whether `value` is `undefined` or `null`.
*
* Returns `true` if, and only if, `value` is `undefined` or `null`;
* return `false` for any other value, including `false` empty string or
* array, etc.
*
* @sig * -> Boolean
*
* @param {any} value
* @return {boolean}
*
* @example
* isNil(undefined);
* // → true
*
* isNil(null);
* // → true
*
* isNil(false);
* // → false
*
* isNil(0);
* // → false
*
* isNil('');
* // → false
*
* isNil(NaN);
* // → false
*
* isNil([]);
* // → false
*/
function isNil(value) {
return value === undefined || value === null;
}
export { isNil }
fromNullable¶
import { fromNullable } from './fromNullable';
import {
Left,
Right,
} from './Either';
//
// Not sure about the best way to assert that we got a Left or Right,
// but it seems the toString approach is good enough for our purposes.
//
[
[undefined, 'undefined'],
[null, 'null'],
].forEach(([input, description]) => {
describe(`when input is ${description}`, () => {
it('should return a Left', () => {
expect(
String(fromNullable(input))
).toEqual(String(Left(input)));
});
});
});
//
// We say “empty array/object” with a loose interpretation of “empty",
// because we know they inherit some methods and properties. We mean
// “empty” in the sense that we didn't explicitly add values to them
// ourselves.
//
[
['', 'empty string'],
[0, '0 (zero)'],
[[], '[] (empty array)'],
[{}, '{} (empty object)'],
[NaN, 'NaN'],
].forEach(([input, description]) => {
describe(`when input is ${description}`, () => {
it('should return a Right', () => {
expect(
String(fromNullable(input))
).toEqual(String(Right(input)));
});
});
});
import { isNil } from './isNil.js';
import { Left, Right } from './Either.js';
/**
* Wraps the value into an `Either` type.
*
* @param {any} value
* @return {Either}
*/
function fromNullable(value) {
return isNil(value) ? Left(value) : Right(value);
}
export { fromNullable }
Either (Left, Right)¶
Either Unit Tests¶
import { Right, Left } from './Either';
describe('Left()', () => {
describe('Left().toString()', () => {
[
[1, 'Left(1)'],
['FP', 'Left(FP)'],
].forEach(([input, output]) => {
it(`should toString ${input}`, () => {
expect(String(Left(input))).toEqual(output);
});
});
});
describe('Left().chain()', () => {
[
['id', 'Tomb Raider I 1996', v => v, 'Tomb Raider I 1996'],
['toLower', 'JEDI', s => s.toLowerCase(), 'JEDI'],
['sub1', 0, i => i - 1, 0],
].forEach(([name, input, fn, output]) => {
it(`should NOT apply ${name}`, () => {
expect(
String(Left(input).chain(fn))
).toEqual(String(Left(output)));
});
});
});
describe('Left().map()', () => {
[
['id', 'Tomb Raider I 1996', v => v, 'Tomb Raider I 1996'],
['toLower', 'JEDI', s => s.toLowerCase(), 'JEDI'],
['sub1', 0, i => i - 1, 0],
].forEach(([name, input, fn, output]) => {
it(`should NOT apply ${name}`, () => {
expect(
String(Left(input).map(fn))
).toEqual(String(Left(output)));
});
});
});
describe('Left().fold()', () => {
it('should return the unprocessed value', () => {
expect(
Left('jedi')
.fold(
_ => 'Error: no processing performed',
str => str.toUpperCase()
)
).toEqual('Error: no processing performed');
});
});
describe('Left().map().fold()', () => {
it('should return the unprocessed value', () => {
expect(
Left('jedi')
.map(str => str.toUpperCase())
.map(str => str.split(''))
.map(arr => arr.join('-'))
.fold(
_ => 'Error: no processing performed',
str => str.toUpperCase()
)
).toEqual('Error: no processing performed');
});
});
});
describe('Right()', () => {
describe('Right().toString()', () => {
[
[1, 'Right(1)'],
['FP', 'Right(FP)'],
].forEach(([input, output]) => {
it(`should toString ${input}`, () => {
expect(String(Right(input))).toEqual(output);
});
});
});
describe('Right().chain()', () => {
[
['id', 'Tomb Raider I 1996', v => v, 'Tomb Raider I 1996'],
['toLower', 'JEDI', s => s.toLowerCase(), 'jedi'],
['sub1', 0, i => i - 1, 0 - 1],
].forEach(([name, input, fn, output]) => {
it(`should apply ${name} correctly`, () => {
expect(
Right(input).chain(fn)
).toEqual(output);
});
});
});
describe('Right().map()', () => {
[
['id', 'Tomb Raider I 1996', v => v, 'Tomb Raider I 1996'],
['toLower', 'JEDI', s => s.toLowerCase(), 'jedi'],
['sub1', 0, i => i - 1, 0 - 1],
].forEach(([name, input, fn, output]) => {
it(`should apply ${name} correctly`, () => {
expect(
String(Right(input).map(fn))
).toEqual(String(Right(output)));
expect(
Right(input).map(fn).toString()
).toEqual(Right(output).toString());
});
});
});
describe('Right().fold()', () => {
[
['id', 1, v => v, 1],
['add1', 0, v => v + 1, 0 + 1],
['split and join', 'xyz', v => v.split('').join('-'), 'x-y-z'],
['Number', '3.14', Number, 3.14],
].forEach(([name, input, fn, output]) => {
it(`should apply ${name} to ${input} and produce ${output}`, () => {
expect(Right(input).fold(_ => 'Error', fn)).toEqual(output);
});
});
});
describe('Right().map().fold()', () => {
it('should return the processed value', () => {
expect(
Right('jedi')
.map(str => str.toUpperCase())
.map(str => str.split(''))
.fold(_ => 'Error', arr => arr.join('-'))
).toEqual('J-E-D-I');
});
});
describe('Right().map().fold()', () => {
it('should return the processed value', () => {
expect(
Right('jedi')
.map(str => str.toUpperCase())
.map(str => str.split(''))
.map(arr => arr.join('-'))
.fold(_ => 'Error', v => v)
).toEqual('J-E-D-I');
});
it('should toLower, split, join and then unbox', () => {
const toLower = s => s.toLowerCase();
const split = (sep, val) => val.split(sep);
const join = (sep, val) => val.join(sep);
const strToArr = split.bind(null, '');
const joinWithUnderscore = join.bind(null, '_');
const f = () => undefined;
expect(
Right('JEDI').map(toLower).map(strToArr).fold(f, joinWithUnderscore)
).toEqual('j_e_d_i');
expect(
Right('YODA')
.map(s => s.toLowerCase())
.map(s => s.split(''))
.fold(_ => undefined, s => s.join('-'))
).toEqual('y-o-d-a');
});
});
});
Either Implementation¶
/// <reference path="./typedefs.js" />
/**
* Creates a chainable, `LeftContainer` container.
*
* This function allows the chaining `map()` invocations in a composable
* way, and, if desired, unbox the value using `fold()`.
*
* However, `Left` is supposed to handle failures, and therefore,
* `Left().map()` DOES NOT actually apply the function to the value.
* It simply returns another `LeftContainer` without modifying the
* value.
*
* @sig Value -> LeftContainer
*
* @param {Value} value
* @return {LeftContainer}
*
* @example
* Left('YODA')
* .map(s => s.toLowerCase())
* .map(s => s.split(''))
* .fold(_ => 'error', s => s.join('-'))
* // → 'error'
*/
function Left(value) {
return {
chain: _ => Left(value),
map: _ => Left(value),
fold: (leftFn, _) => leftFn(value),
toString: () => `Left(${value})`,
};
}
/**
* Creates a chainable `RightContainer` container.
*
* This function allows the chaining of `map()` invocations in a composable
* way, and, if desired, unbox the value using `fold()`.
*
* @sig Value -> RightContainer
*
* @param {Value} value
* @return {RightContainer}
*
* @example
* RightContainer('YODA')
* .map(s => s.toLowerCase())
* .map(s => s.split(''))
* .fold(_ => 'error', s => s.join('-'))
* // → 'y-o-d-a'
*/
function Right(value) {
return {
chain: f => f(value),
map: f => Right(f(value)),
fold: (_, rightFn) => rightFn(value),
toString: () => `Right(${value})`,
};
}
export {
Right,
Left,
}
tryCatch¶
import { Left, Right } from './Either.js';
import { tryCatch } from './tryCatch.js';
describe('when we get an exception', () => {
it('should return a Left container', () => {
expect(
String(tryCatch(() => undefined.split('-')))
).toEqual(
// This hard-coded TypeError string bothers me. What other
// approach could be used?
String(
Left(
"TypeError: Cannot read property 'split' of undefined"
)
)
);
});
});
describe('when we get a successfull result', () => {
it('should return a Right container', () => {
expect(
String(tryCatch(() => 'hello-world'.split('-')))
).toEqual(
String(Right('hello,world'))
);
});
});
import { Left, Right } from './Either.js';
/**
* Turns a try/catch into an `Either`, composable container.
*
* @param {Function} f
* @return {Either}
*/
const tryCatch = f => {
try {
return Right(f());
} catch (err) {
return Left(err);
}
};
export { tryCatch }