Skip to content

Instantly share code, notes, and snippets.

@Willmo36
Created December 14, 2020 18:24
Show Gist options
  • Save Willmo36/bfc5d0f5ed1b56141a2001cfed9e3399 to your computer and use it in GitHub Desktop.
Save Willmo36/bfc5d0f5ed1b56141a2001cfed9e3399 to your computer and use it in GitHub Desktop.
fp-ts comonad with example
import { Comonad2C } from 'fp-ts/lib/Comonad';
import { pipe } from 'fp-ts/lib/function';
import { FunctionN } from 'fp-ts/lib/function';
import { Monoid } from 'fp-ts/lib/Monoid';
import { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray';
import { pipeable } from 'fp-ts/lib/pipeable';
/**
* OOP style builder pattern but in FP
* References:
* http://www.haskellforall.com/2013/02/you-could-have-invented-comonads.html
* https://kodimensional.dev/posts/2019-03-25-comonadic-builders
* https://github.com/thma/LtuPatternFactory#fluent-api--comonad
*
* In Builder pattern we have several pieces:
* A data type for the configuration.
* A data type for the value created from the configuration.
* A function that creates value from the configuration.
* A way to compose builders.
*/
declare module 'fp-ts/lib/HKT' {
interface URItoKind2<E, A> {
Builder: Builder<E, A>;
}
}
export const URI = 'Builder';
export type URI = typeof URI;
export interface Builder<Config, Value> {
_tag: URI;
(opts: Config): Value;
}
export type BuildWith<Config, Value> = (b: Builder<Config, Value>) => Value;
export const makeBuilder = <Config, Value>(fn: FunctionN<[Config], Value>): Builder<Config, Value> => {
const b = fn as Builder<Config, Value>;
b._tag = URI;
return b;
};
export const getComonadBuilder = <Config>(M: Monoid<Config>): Comonad2C<URI, Config> => ({
URI,
_E: M.empty,
map: (fa, f) => makeBuilder((pa) => f(fa(pa))),
extract: (builder) => builder(M.empty),
extend: (builder, setter) => makeBuilder((opts2) => setter(makeBuilder((opts1) => builder(M.concat(opts2, opts1))))),
});
type User = {
age: number;
phone: string;
address: string;
};
type UserConfig = Partial<User>;
const monoidUserConfig: Monoid<Partial<User>> = {
concat: (userA, userB) => ({
...userA,
...userB,
}),
empty: {},
};
const comonad = getComonadBuilder(monoidUserConfig);
const { extend } = pipeable(comonad);
const buildUser = makeBuilder<UserConfig, User>((config) => ({ age: 0, phone: '', address: '', ...config }));
const withAge: BuildWith<UserConfig, User> = (builder) => builder({ age: 30 });
const withPhone: BuildWith<UserConfig, User> = (builder) => builder({ phone: '123123' });
const withAddress: BuildWith<UserConfig, User> = (builder) => builder({ address: '123 skip street' });
const withAdd10Years: BuildWith<UserConfig, User> = (builder) => {
const user = comonad.extract(builder);
return { ...user, age: user.age + 10 };
};
const user1 = pipe(
buildUser,
extend(withAge),
extend(withPhone),
extend(withAddress),
extend(withAdd10Years),
comonad.extract,
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment