Skip to content

Instantly share code, notes, and snippets.

@reidev275
Last active April 22, 2024 14:42
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save reidev275/dd8dac76d26ef27907ad5a4c740bc9ad to your computer and use it in GitHub Desktop.
Save reidev275/dd8dac76d26ef27907ad5a4c740bc9ad to your computer and use it in GitHub Desktop.
//
// Prelude
// A zero dependency, one file drop in for faster Typescript development with fewer bugs
// through type safe, functional programming. Comments are inline with links to blog posts motiviating the use.
// alias for a function with airity 1
export type Fn<A, B> = (a: A) => B;
// alias for a function with airity 2
export type Fn2<A, B, C> = (a: A, b: B) => C;
// alias for an Isomorphic function
export type Iso<A> = Fn<A, A>;
// the identity function
export const id = <A>(a: A): A => a;
// general function composition
export const compose =
<A, B, C>(f: Fn<A, B>, g: Fn<B, C>): Fn<A, C> =>
(a) =>
g(f(a));
//
// Semigroups - Common ways to combine values
export type Semigroup<A> = {
append: (x: A, y: A) => A;
};
// Common Semigroups namespaced for discoverability
export namespace Semigroup {
export const sum = {
append: (x: number, y: number) => x + y,
};
export const product = {
append: (x: number, y: number) => x * y,
};
export const all = {
append: (x: boolean, y: boolean) => x && y,
};
export const any = {
append: (x: boolean, y: boolean) => x || y,
};
export const stringConcat = {
append: (x: string, y: string) => `${x}${y}`,
};
export const arrayConcat = {
append: <A>(x: A[], y: A[]) => [...x, ...y],
};
export const max = {
append: (x: number, y: number) => (x < y ? y : x),
};
export const min = {
append: (x: number, y: number) => (y < x ? y : x),
};
export const ordering = {
append: (x: Ordering, y: Ordering) => (x === Ordering.EQ ? y : x),
};
export const first = {
append: <A>(x: A, y: A) => x,
};
export const last = {
append: <A>(x: A, y: A) => y,
};
export const fn = <B>(S: Semigroup<B>) => ({
append:
<A>(f: Fn<A, B>, g: Fn<A, B>) =>
(a: A) =>
S.append(f(a), g(a)),
});
export const iso = {
append:
<A>(a: Iso<A>, b: Iso<A>): Iso<A> =>
(x) =>
b(a(x)),
};
}
//
// Ordering - Ways of compositional array sorting
// https://medium.com/@reidev275/creating-composable-sorting-hierarchies-in-typescript-47ac89441643
export enum Ordering {
GT = 1,
LT = -1,
EQ = 0,
}
export type Ord<A> = {
compare(x: A, y: A): Ordering;
};
export namespace Ord {
export const contramap = <A, B>(mapper: Fn<B, A>, O: Ord<A>): Ord<B> => ({
compare: (x: B, y: B) => O.compare(mapper(x), mapper(y)),
});
export const getSemigroup = <A>(): Semigroup<Ord<A>> => ({
append: (x: Ord<A>, y: Ord<A>) => ({
compare: (x1: A, y1: A) =>
Semigroup.ordering.append(x.compare(x1, y1), y.compare(x1, y1)),
}),
});
export const getMonoid = <A>(): Monoid<Ord<A>> => ({
empty: {
compare: (x: A, y: A) => Ordering.EQ,
},
...Ord.getSemigroup(),
});
export const invert = <A>(ord: Ord<A>): Ord<A> => ({
compare: (x: A, y: A) => ord.compare(y, x),
});
export const ascending = {
compare: (x: any, y: any): Ordering =>
x < y ? Ordering.LT : x > y ? Ordering.GT : Ordering.EQ,
} as Ord<any>;
export const descending = invert(ascending);
}
// sort(Ord.ascending, [2, 1, 3]);
export const sort = <A>(O: Ord<A>, as: A[]): A[] => as.sort(O.compare);
// sortWith(Ord.descending, [{ x: 1 }, { x: 2 }], (obj) => obj.x);
export const sortWith = <A, B>(O: Ord<A>, bs: B[], mapper: Fn<B, A>): B[] =>
bs.sort(Ord.contramap(mapper, O).compare);
//
// Monoids - Ways to combine things when you may have nothing
export type Monoid<A> = Semigroup<A> & {
empty: A;
};
// common monoid instances namespaced for easy discovery
export namespace Monoid {
export const sum = {
...Semigroup.sum,
empty: 0,
};
export const product = {
...Semigroup.product,
empty: 1,
};
export const all = {
...Semigroup.all,
empty: true,
};
export const any = {
...Semigroup.any,
empty: false,
};
export const stringConcat = {
...Semigroup.stringConcat,
empty: "",
};
export const arrayConcat = {
...Semigroup.arrayConcat,
empty: [],
};
export const max = {
...Semigroup.max,
empty: Number.NEGATIVE_INFINITY,
};
export const min = {
...Semigroup.min,
empty: Number.POSITIVE_INFINITY,
};
export const ordering = {
...Semigroup.ordering,
empty: Ordering.EQ,
};
export const fn = <B>(M: Monoid<B>) => ({
...Semigroup.fn,
empty: () => M.empty,
});
export const iso = {
...Semigroup.iso,
empty: id,
};
}
export type Foldable<B> = {
reduce<A>(agg: Fn2<A, B, A>, empty: A): A;
};
// foldS(Semigroup.first, [1,2], 0) => 1
// foldS(Semigroup.first, [], 0) => 0
export const foldS = <A>(S: Semigroup<A>, as: Foldable<A>, start: A): A =>
as.reduce(S.append, start);
export const foldMapS = <A, B>(
S: Semigroup<A>,
bs: Foldable<B>,
mapper: Fn<B, A>,
start: A
): A => bs.reduce((p, c) => S.append(p, mapper(c)), start);
// fold(Monoid.sum, [1,2,3]) => 6
export const fold = <A>(M: Monoid<A>, as: Foldable<A>): A =>
as.reduce(M.append, M.empty);
// foldArray(Monoid.sum, 1, 2, 3) => 6
export const foldArray = <A>(M: Monoid<A>, ...as: A[]): A =>
as.reduce(M.append, M.empty);
// foldMap(Monoid.sum, [{ item: 'Large Coffee', qty: 2 }], x => x.qty)
export const foldMap = <A, B>(
M: Monoid<A>,
bs: Foldable<B>,
mapper: Fn<B, A>
): A => bs.reduce((p, c) => M.append(p, mapper(c)), M.empty);
type Indexer = string | number | symbol;
type GBResult<T> = {
[k: Indexer]: T[];
};
// groupBy([{ foo: 1, bar: 2 }, { foo: 1, bar: 3 }, { foo: 2, bar: 3 }], x => x.foo)
// {
// 1: [ { foo: 1, bar: 2 }, { foo: 1, bar: 3 } ],
// 2: [ { foo: 2, bar: 3 } ]
// }
export const groupBy = <T>(
arr: Foldable<T>,
fn: (t: T) => Indexer
): GBResult<T> =>
arr.reduce((p: GBResult<T>, c: T) => {
const key = fn(c);
const val = p[key];
return {
...p,
[key]: val ? [...val, c] : [c],
};
}, {});
//
// Lenses - Immutable property access and assignment
// https://medium.com/@reidev275/composable-immutable-property-access-with-lenses-in-typescript-798da4ddc30e
export type Lens<A, B> = {
get: (a: A) => B;
set: (b: B, a: A) => A;
};
export const lensProp = <T, K extends keyof T>(field: K): Lens<T, T[K]> => ({
get: (a: T) => a[field],
set: (b: T[K], a: T) => ({ ...a, [field]: b }),
});
export namespace Lens {
export const compose = <A, B, C>(
x: Lens<A, B>,
y: Lens<B, C>
): Lens<A, C> => ({
get: (a: A) => y.get(x.get(a)),
set: (c: C, a: A) => {
const b: B = x.get(a);
const b2: B = y.set(c, b);
return x.set(b2, a);
},
});
export const compose3 = <A, B, C, D>(
a: Lens<A, B>,
b: Lens<B, C>,
c: Lens<C, D>
): Lens<A, D> => Lens.compose(a, Lens.compose(b, c));
export const compose4 = <A, B, C, D, E>(
a: Lens<A, B>,
b: Lens<B, C>,
c: Lens<C, D>,
d: Lens<D, E>
): Lens<A, E> => Lens.compose3(a, b, Lens.compose(c, d));
export const compose5 = <A, B, C, D, E, F>(
a: Lens<A, B>,
b: Lens<B, C>,
c: Lens<C, D>,
d: Lens<D, E>,
e: Lens<E, F>
): Lens<A, F> => Lens.compose4(a, b, c, Lens.compose(d, e));
export const prop = <T, K extends keyof T>(field: K): Lens<T, T[K]> => ({
get: (a: T) => a[field],
set: (b: T[K], a: T) => ({ ...a, [field]: b }),
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment