Currently, I am brushing up on some TypeScript, and I’ve implemented a set of functions used in functional programming for practice. They are perhaps of interest to someone else, someone learning about functional programming. Each definition includes a test case written with Jest demonstrating the feature. For all definitions I’ve aimed for clarity, not performance.
For some tests I use this helper.
const str = <T>(x: T): string => JSON.stringify(x);
test('add', () => expect(add(1)(1)).toEqual(2));
const add = (a: number) => (b: number): number => a + b;
test('append', () => expect(str(append([1, 2], 3))).toBe(str([1, 2, 3])));
const append = <T>(list: T[], newElement: T): T[] => [
...list,
newElement
];
test('apply', () => expect(apply(Math.max, [1, 2, 3])).toBe(3));
const apply = <T>(fn: (...xs: T[]) => T, xs: T[]): T => fn(...xs);
test('assoc', () =>
expect(str(assoc('b', 2, { a: 1 }))).toEqual(str({ a: 1, b: 2 })));
const assoc = <T>(
k: string,
v: T,
obj: Record<string, unknown>
): Record<string, unknown> => ({ ...obj, [k]: v });
test('assocPath', () => {
const someObject = {
a: {
b: {
c: 3
},
d: 2
},
e: 1
};
const modifyPropC = assocPath(['a', 'b', 'c']);
expect(str(modifyPropC(10, someObject))).toEqual(
str({
a: {
b: {
c: 10
},
d: 2
},
e: 1
})
);
});
const assocPath = <T>(path: string[]) => (
v: T,
obj: Record<string, unknown>
): Record<string, unknown> => {
const setValue = (
[head, ...tail]: string[],
o: Record<string, unknown>
): Record<string, unknown> =>
tail.length
? {
...o,
[head]: setValue(tail, o[head] as Record<string, unknown>)
}
: {
...o,
[head]: v
};
return setValue(path, obj);
};
test('concat', () => {
expect(str(concat([1, 2], [3, 4]))).toEqual(str([1, 2, 3, 4]));
expect(str(concat([1, 2], [3, 4]))).toEqual(str([1, 2, 3, 4]));
});
const concat = <T>(list1: T[], list2: T[]): T[] => [...list1, ...list2];
test('dec', () => expect(dec(3)).toEqual(2));
const dec = (x: number): number => x - 1;
test('dissoc', () =>
expect(str(dissoc('c', { a: 1, b: 2, c: 3 }))).toEqual(str({ a: 1, b: 2 })));
const dissoc = <T extends Record<string, unknown>>(
k: keyof T,
obj: T
): T => {
delete obj[k];
return obj;
};
test('dissocPath', () => {
expect(
str(
dissocPath(
['a', 'b', 'c'],
{ a: { b: { c: 2, d: 2 }, e: 2 } }
)
)
).toEqual(str({ a: { b: { d: 2 }, e: 2 } }));
expect(
str(
dissocPath(
['a', 'b'],
{ a: { b: { c: 2, d: 2 }, e: 2 } }
)
)
).toEqual(str({ a: { e: 2 } }));
});
const dissocPath = (
path: string[],
obj: Record<string, unknown>
): Record<string, unknown> => {
const setValue = (
[head, ...tail]: string[],
o: Record<string, unknown>
): Record<string, unknown> => {
if (!tail.length) {
if (o && head in o) {
delete o[head];
}
return o;
}
return setValue(tail, {
...o,
[head]: setValue(tail, o[head] as Record<string, unknown>)
});
};
return setValue(path, obj);
};
test('div', () => expect(div(2)(1)).toEqual(2));
const div = (a: number) => (b: number): number => a / b;
test('drop', () =>
expect(str(drop(2, [1, 2, 3, 4, 5]))).toEqual(str([3, 4, 5])));
const drop = <T>(n: number, list: T[]): T[] => list.slice(n);
test('dropLast', () =>
expect(str(dropLast(2, [1, 2, 3, 4, 5]))).toEqual(str([1, 2, 3])));
const dropLast = <T>(n: number, list: T[]): T[] =>
list.slice(0, list.length - n);
const lesserThan3 = (x: number): boolean => x < 3;
test('dropWhile', () =>
expect(
str(dropWhile(lesserThan3, [1, 2, 3, 4, 5]))
).toEqual(str([3, 4, 5])));
const dropWhile = <T>(
predicate: (x: T) => boolean,
[head, ...tail]: T[]
): T[] => (predicate(head) ? dropWhile(predicate, tail) : [head, ...tail]);
test('endsWith', () => {
expect(endsWith('world', 'hello, world')).toBeTruthy();
expect(endsWith('welt', 'hello, world')).toBeFalsy();
});
const endsWith = (str1: string, str2: string): boolean =>
str1 === str2.slice(str2.length - str1.length);
test('every', () => {
expect(every(isEven, [2, 2, 2])).toBe(true);
expect(every(isEven, [1, 2, 3])).toBe(false)
});
function every<T>(
fn: (x: T, i: number, arr: T[]) => boolean,
list: T[]
): boolean {
return list.reduce(
(isSomeElementTrue: boolean, element: T, i: number, arr: T[]): boolean =>
!isSomeElementTrue ? false : fn(element, i, arr),
true
);
}
test('flatten', () => {
expect(
str(
flatten([
[1, 2],
[3, 4]
])
)
).toEqual(str([1, 2, 3, 4]));
expect(str(flatten([[[1, 2]], [[3, 4]]]))).toEqual(
str([
[1, 2],
[3, 4]
])
);
});
const flatten = <T>(li: T[][]): T[] =>
li.reduce((acc, v) => acc.concat(v), []);
test('filter', () => expect(filter(isEven, [1, 2, 3, 4, 5])).toEqual([2, 4]));
function filter<T>(
fn: (x: T, i: number, arr: T[]) => boolean,
list: T[]
): T[] | ((a: T[]) => T[]) {
const _filter = (li: T[]) =>
li.reduce(
(acc: T[], x: T, i: number, arr: T[]) =>
fn(x, i, arr) ? [...acc, x] : acc,
[]
);
return list === undefined ? (l: T[]): T[] => _filter(l) : _filter(list);
}
test('find', () => expect(find(isEven, [1, 2, 3, 4])).toEqual(2));
type NoContent = undefined;
function find<T>(
fn: (x: T, i: number, arr: T[]) => boolean,
list: T[]
): T | NoContent {
return list.reduce(
(firstFound: T | undefined, element: T, i: number, arr: T[]) =>
!firstFound && fn(element, i, arr) ? element : firstFound,
undefined
);
}
test('findIndex', () => expect(findIndex(isEven, [1, 2, 3, 4])).toEqual(1));
type NotFound = -1;
function findIndex<T>(
fn: (x: T, i: number, arr: T[]) => boolean,
list: T[]
): number | NotFound {
return list.reduce(
(firstFound: number, element: T, i: number, arr: T[]) =>
firstFound === -1 && fn(element, i, arr) ? i : firstFound,
-1
);
}
test('flatten', () => {
expect(
str(
flatten([
[1, 2],
[3, 4]
])
)
).toEqual(str([1, 2, 3, 4]));
expect(str(flatten([[[1, 2]], [[3, 4]]]))).toEqual(
str([
[1, 2],
[3, 4]
])
);
});
const flatten = <T>(li: T[][]): T[] =>
li.reduce((acc, v) => acc.concat(v), []);
test('foldl', () => {
expect(
foldl((a: number, b: number): number => a + b, 0, [1, 2, 3, 4, 5])
).toEqual(15);
expect(
foldl(
(acc: number[], v: number): number[] => [...acc, inc(v)],
[],
[1, 2, 3, 4, 5]
)
).toEqual([2, 3, 4, 5, 6]);
expect(
foldl(
(acc: { answer: number }, v: number): { answer: number } => ({
answer: acc.answer + v
}),
{ answer: 0 },
[1, 2, 3]
)
).toEqual({ answer: 6 });
});
type Reducer<U, T> = (acc: U, next: T) => U;
const foldl = <T, U>(
reducer: Reducer<U, T>,
result: U,
list: T[]
): U => {
while (list.length) {
result = reducer(result, head(list));
list.shift();
}
return result;
};
test('head', () => expect(head(['a', 'b'])).toEqual('a'));
const head = <T>(li: T[]): T | undefined =>
li.length > 0 ? li[0] : undefined;
test('id', () => expect(id(3)).toEqual(3));
const id = <T>(x: T): T => x;
test('inc', () => expect(inc(3)).toEqual(4));
const inc = (x: number): number => x + 1;
test('includes', () => {
expect(includes(3, [1, 2, 3, 4])).toBeTruthy();
expect(includes(5, [1, 2, 3, 4])).toBeFalsy();
});
function includes<T>(el: T, list: T[]): boolean {
return list.indexOf(el) >= 0 ? true : false;
}
test('isEmpty', () => {
expect(isEmpty([1, 2])).toBeFalsy();
expect(isEmpty([])).toBeTruthy();
});
const isEmpty = <T>(list: T[]): boolean => !list.length;
test('join', () =>
expect(join(['hello,', 'world'], ' ')).toEqual('hello, world'));
const join = <T>(list: T[], delimiter = ''): string => {
return list.reduce(
(acc, v, i, arr) => `${acc}${v}${i !== arr.length - 1 ? delimiter : ''}`,
''
);
};
test('len', () => expect(len([1, 2, 3])).toEqual(3));
const len = <T>(list: T[]): number =>
list.reduce((listLength) => listLength + 1, 0);
test('map', () => {
expect(str(map(add(10))([1, 2, 3]))).toEqual(str([11, 12, 13]));
expect(str(map(add1)([1, 2, 3]))).toEqual(str([2, 3, 4]));
});
const map = <T, U>(fn: (x: T, i?: number, arr?: T[]) => U) => (
list: T[]
): U[] => {
const _map = <T, U>(fn: (x: T, i?: number, arr?: T[]) => U, _l: T[]): U[] =>
_l.reduce(
(acc: U[], x: T, i: number, arr: T[]): U[] => [...acc, fn(x, i, arr)],
[]
);
return _map(fn, list);
};
test('mult', () => expect(mult(1)(2)).toEqual(2));
const mult = (a: number) => (b: number): number => a * b;
test('not', () => {
expect(not(0)).toBeTruthy();
expect(not(1)).toBeFalsy();
expect(not(false)).toBeTruthy();
expect(not(true)).toBeFalsy();
});
const not = (el: boolean | 0 | 1): boolean => !el;
test('once', () => {
const add1ToValue = once(add1);
let newValue;
newValue = add1ToValue(0);
newValue = add1ToValue(newValue);
newValue = add1ToValue(newValue);
expect(newValue).toEqual(1);
const add1ToList = (l: number[]) => l.map(add1);
const add1ToListOnce = once(add1ToList);
let newArray;
newArray = add1ToListOnce([1, 2, 3]);
newArray = add1ToListOnce(newArray);
newArray = add1ToListOnce(newArray);
expect(str(newArray)).toEqual(str([2, 3, 4]));
});
const once = <T, U>(fn: (...as: T[]) => U): ((...vs: T[]) => U) => {
let _v: U;
return (...args: T[]) => {
_v ||= fn(...args);
return _v;
};
};
test('pluck', () => expect(pluck({ a: 1, b: 2 }, 'a')).toBe(1));
const pluck = <T, U extends keyof T>(obj: T, k: U): T[U] => obj[k];
test('range', () => expect(str(range(2, 8))).toEqual(str([2, 3, 4, 5, 6, 7])));
const range = (a: number, b: number): number[] =>
Array.from({ length: b - a }, (_, i) => i + a);
test('repeat', () => expect(
str(repeat('*', 3))
).toEqual(str(['*', '*', '*']))
);
const repeat = <T>(el: T, n: number): T[] =>
Array.from({ length: n }, () => el);
test('reverse', () => expect(str(reverse([1, 2, 3]))).toEqual(str([3, 2, 1])));
const reverse = <T>(list: T[]): T[] =>
list.reduce((acc: T[], v: T) => [v, ...acc], []);
test('some', () => {
expect(some(isEven, [1, 1, 1])).toBe(false);
expect(some(isEven, [1, 2, 3])).toBe(true);
});
function some<T>(
fn: (x: T, i: number, arr: T[]) => boolean,
list: T[]
): boolean {
return list.reduce(
(isSomeElementTrue: boolean, element: T, i: number, arr: T[]): boolean =>
!isSomeElementTrue ? fn(element, i, arr) : true,
false
);
}
test('startsWith', () => {
expect(startsWith('hello', 'hello, world')).toBeTruthy();
expect(startsWith('bonjour', 'hello, world')).toBeFalsy();
});
const startsWith = (str1: string, str2: string): boolean =>
str1 === str2.slice(0, str1.length);
test('subtr', () => expect(subtr(1)(1)).toEqual(0));
const subtr = (a: number) => (b: number): number => a - b;
test('sum', () => expect(sum(1, 1, 1, 1, 1)).toEqual(5));
const sum = (...ns: number[]): number =>
ns.reduce((acc, v) => acc + v, 0);
test('tail', () => expect(tail(['a', 'b', 'c'])).toEqual(['b', 'c']));
const tail = <T>(li: T[]): T[] | undefined =>
li.length > 0 ? li.slice(1) : [];
test('take', () => expect(str(take(2, [1, 2, 3, 4, 5]))).toEqual(str([1, 2])));
const take = <T>(n: number, list: T[]): T[] => list.slice(0, n);
test('tee', () => {
let isChanged1 = false;
const mockFn = <T>(_: T) => (isChanged1 = true);
expect(tee(mockFn)(2)).toEqual(2);
expect(isChanged1).toBeTruthy();
let isChanged2 = false;
const mockFn2 = <T>(_: T) => (isChanged2 = true);
expect(tee(mockFn2)([1, 2, 3])).toEqual([1, 2, 3]);
expect(isChanged2).toBeTruthy();
});
const tee = <T>(fn: (x: T) => void) => (v: T): T => {
fn(v);
return v;
};
test('trim', () => expect(trim(' hello, world! ')).toEqual('hello, world!'));
const trim = (str: string): string => str.trim();
test('uniq', () =>
expect(str(uniq([1, 1, 1, 1, 2, 3, 1, 1]))).toEqual(str([1, 2, 3])));
const uniq = <T>(list: T[]): T[] =>
list.filter((v, i, a) => a.indexOf(v) === i);
test('zip', () =>
expect(str(zip(['a', 'b'], [1, 2]))).toEqual(
str([
['a', 1],
['b', 2]
])
));
const zip = <T, U>(list1: T[], list2: U[]): [T, U][] =>
list1.reduce(
(acc: [T, U][], v: T, i: number): [T, U][] => [...acc, [v, list2[i]]],
[]
);