-
-
Save dejanr/9bc846296e36ce97a1252c8c260f6152 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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 arity 1 | |
export type Fn<A, B> = (a: A) => B; | |
// alias for a function with arity 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; | |
}; | |
// foldSemigroup(Semigroup.first, [1,2], 0) => 1 | |
// foldSemigroup(Semigroup.first, [], 0) => 0 | |
export const foldSemigroup = <A>( | |
S: Semigroup<A>, | |
as: Foldable<A>, | |
start: A | |
): A => as.reduce(S.append, 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