Practice I. TypeScript functional programming utilities

2021-04-02

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.

Implementations

For some tests I use this helper.

const str = <T>(x: T): string => JSON.stringify(x);

add

test('add', () => expect(add(1)(1)).toEqual(2));

const add = (a: number) => (b: number): number => a + b;

append

test('append', () => expect(str(append([1, 2], 3))).toBe(str([1, 2, 3])));

const append = <T>(list: T[], newElement: T): T[] => [
  ...list,
  newElement
];

apply


test('apply', () => expect(apply(Math.max, [1, 2, 3])).toBe(3));

 const apply = <T>(fn: (...xs: T[]) => T, xs: T[]): T => fn(...xs);

assoc

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 });

assocPath

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);
};

concat

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];

dec

test('dec', () => expect(dec(3)).toEqual(2));

const dec = (x: number): number => x - 1;

dissoc

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;
};

dissocPath

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);
};

div

test('div', () => expect(div(2)(1)).toEqual(2));

const div = (a: number) => (b: number): number => a / b;

drop

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);

dropLast

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);

dropWhile

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]);

endsWith

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);

every

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
  );
}

flatten

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), []);

filter

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);
}

find

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
  );
}

findIndex

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
  );
}

flatten

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), []);

foldl

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;
};

id

test('id', () => expect(id(3)).toEqual(3));

const id = <T>(x: T): T => x;

inc

test('inc', () => expect(inc(3)).toEqual(4));

const inc = (x: number): number => x + 1;

includes

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;
}

isEmpty

test('isEmpty', () => {
  expect(isEmpty([1, 2])).toBeFalsy();
  expect(isEmpty([])).toBeTruthy();
});

const isEmpty = <T>(list: T[]): boolean => !list.length;

join

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 : ''}`,
    ''
  );
};

len

test('len', () => expect(len([1, 2, 3])).toEqual(3));

const len = <T>(list: T[]): number =>
  list.reduce((listLength) => listLength + 1, 0);

map

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);
};

mult

test('mult', () => expect(mult(1)(2)).toEqual(2));

const mult = (a: number) => (b: number): number => a * b;

not

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;

once

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;
  };
};

pluck

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];

range

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);

repeat

test('repeat', () => expect(
    str(repeat('*', 3))
  ).toEqual(str(['*', '*', '*']))
);

const repeat = <T>(el: T, n: number): T[] =>
  Array.from({ length: n }, () => el);

reverse

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], []);

some

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
  );
}

startsWith

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);

subtr

test('subtr', () => expect(subtr(1)(1)).toEqual(0));

const subtr = (a: number) => (b: number): number => a - b;

sum

test('sum', () => expect(sum(1, 1, 1, 1, 1)).toEqual(5));

const sum = (...ns: number[]): number =>
  ns.reduce((acc, v) => acc + v, 0);

tail

test('tail', () => expect(tail(['a', 'b', 'c'])).toEqual(['b', 'c']));

const tail = <T>(li: T[]): T[] | undefined =>
  li.length > 0 ? li.slice(1) : [];

take

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);

tee

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;
};

trim

test('trim', () => expect(trim('  hello, world!  ')).toEqual('hello, world!'));

const trim = (str: string): string => str.trim();

uniq

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);

zip

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]]],
    []
  );

About | Archive