Last active
April 22, 2024 14:42
-
-
Save reidev275/dd8dac76d26ef27907ad5a4c740bc9ad 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 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