Skip to content

Instantly share code, notes, and snippets.

@alxrocha
Forked from reidev275/prelude.ts
Created November 26, 2020 08:48
Show Gist options
  • Save alxrocha/0640d08acdf25be6964db199fdad4f51 to your computer and use it in GitHub Desktop.
Save alxrocha/0640d08acdf25be6964db199fdad4f51 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));
//
// Promise retry policies
// https://medium.com/@reidev275/type-safe-composable-promise-policies-716ea53ff356
export type Policy<A> = () => Promise<A>;
// Common Policies
// Because they are all functions from Policy<A> to Policy<A> they can be combined using the Iso monoid
// The following would result in a policy that retries up to 4 times and delays 1 second between each retry
// foldArray(Monoid.iso, delay(1000), retryNTimes(4))
export namespace Policy {
// Retries forever
export const forever = <A>(policy: Policy<A>): Policy<A> => () =>
policy().catch(() => Policy.forever(policy)());
// Retries a specified number of times
export const retryNTimes = (times: number) => <A>(
policy: Policy<A>
): Policy<A> => () =>
policy().catch((e) =>
times === 1 ? Promise.reject(e) : Policy.retryNTimes(times - 1)(policy)()
);
// Delays running a policy - useful when combined with a retry scheme
export const delay = (ms: number) => <A>(
policy: Policy<A>
): Policy<A> => async () => {
await new Promise((res) => {
setTimeout(() => res(), ms);
});
return policy();
};
// Simple console log that can be added to the chain - useful for debugging / logging failures
export const log = (msg: string) => <A>(
policy: Policy<A>
): Policy<A> => () => {
console.log(msg);
return policy();
};
}
//
// 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 = <Ord<any>>{
compare: (x: any, y: any): Ordering =>
x < y ? Ordering.LT : x > y ? Ordering.GT : Ordering.EQ,
};
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);
//
// 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 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);
},
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment