Skip to content

Instantly share code, notes, and snippets.

@kasperpeulen
Last active May 29, 2021 22:23
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kasperpeulen/47242fd26a1f677a56f05490d8383cdb to your computer and use it in GitHub Desktop.
Save kasperpeulen/47242fd26a1f677a56f05490d8383cdb to your computer and use it in GitHub Desktop.
Reader monad in TS
import { Reader, use } from "./Reader";
import { DictionaryKeys, I18n, LocalDateOptions, Locale } from "../context";
import { DeliveryMoment, DeliveryMomentView } from "../domain";
import { isToday } from "../utils/date";
test("reader", () => {
const deliveryMoment: DeliveryMoment = {
deliveryDate: new Date(2020, 9, 9),
priceInCents: 99,
timeFrame: { from: "18:00", to: "22:00" },
};
const testContext = {
clock: {
now: () => new Date(2020, 9, 9),
},
i18n: {
translate: (locale: string, key: DictionaryKeys) => "Vandaag",
},
locale: "nl",
};
const deliveryMomentViewReader = Reader.fn(deliveryMomentView);
expect(deliveryMomentViewReader(deliveryMoment).provide(testContext)).toEqual(
{
title: "Vandaag",
subTitle: "9 oktober",
price: "€ 0,99",
}
);
});
function* deliveryMomentView(deliveryMoment: DeliveryMoment) {
return {
title: yield* deliveryMomentTitle(deliveryMoment),
subTitle: yield* deliveryMomentSubTitle(deliveryMoment),
price: yield* deliveryMomentPrice(deliveryMoment),
} as DeliveryMomentView;
}
export function* deliveryMomentTitle({ deliveryDate }: DeliveryMoment) {
if (yield* isToday(deliveryDate)) {
return yield* translate("today");
} else {
return yield* localeDateString(deliveryDate, {
day: "numeric",
month: "long",
});
}
}
export function* deliveryMomentSubTitle({ deliveryDate }: DeliveryMoment) {
return yield* localeDateString(deliveryDate, {
day: "numeric",
month: "long",
});
}
export function* deliveryMomentPrice({ priceInCents }: DeliveryMoment) {
const { locale } = yield* use<Locale>();
return (priceInCents / 100).toLocaleString(locale, {
style: "currency",
currency: "EUR",
useGrouping: false,
});
}
function* localeDateString(date: Date, options: LocalDateOptions) {
const { locale } = yield* use<Locale>();
return date.toLocaleString(locale, options);
}
function* translate(key: DictionaryKeys) {
const { i18n, locale } = yield* use<I18n & Locale>();
return i18n.translate(locale, key);
}
export interface ReaderI<C, T> {
reader: (c: C) => T;
}
export class Reader<C, T> implements ReaderI<C, T> {
static of<T>(value: T) {
return Reader.from(() => value);
}
static from<C, T>(reader: (context: C) => T) {
return new Reader(reader);
}
static fromGenerator<G extends Generator<ReaderI<any, any>, any>>(
generator: G
) {
return (Reader.from((context: any) => {
// @ts-ignore
return step(generator.next());
function step({ value, done }: IteratorResult<Reader<any, any>>) {
if (!done) {
// @ts-ignore
return step(generator.next(value.provide(context)));
} else {
return value;
}
}
}) as unknown) as InferReaderFromGeneratorFunction<typeof generator>;
// return step(generator.next());
// function step(result: IteratorResult<Reader<unknown, unknown>, unknown>): Reader<unknown> {
// if (!result.done) {
// return result.value.flatMap((value) => step(generator.next(value)))
// } else {
// return Reader.of(result.value);
// }
// }
}
static fn<G extends (...args: any[]) => Generator<ReaderI<any, any>, any>>(
generatorFn: G
) {
return (((...args: any[]) =>
Reader.fromGenerator(
generatorFn(...args)
)) as unknown) as InferReaderFromGeneratorFunction<typeof generatorFn>;
}
static use<C>() {
return Reader.from((context: C) => context);
}
constructor(value: (r: C) => T) {
this.reader = value;
}
readonly reader: (r: C) => T;
map<R>(transform: (t: T) => R): Reader<C, R> {
return Reader.from((context: C) => transform(this.reader(context)));
}
flatMap<C_, T_>(transform: (t: T) => Reader<C_, T_>): Reader<C & C_, T_> {
return Reader.from((context: C & C_) => {
return transform(this.provide(context)).provide(context);
});
}
provide(context: C): T {
return this.reader(context);
}
*gen(): Generator<Reader<C, T>, T> {
const value = yield this;
return value as T;
}
}
export function use<C>() {
return Reader.from((context: C) => context).gen();
}
declare function Do<
G extends (...args: any[]) => Generator<ReaderI<any, any>, any>
>(generatorFn: G): InferReaderFromGenerator<typeof generatorFn>;
type InferReaderFromGenerator<A> = A extends Generator<
ReaderI<infer C, any>,
infer R
>
? Reader<UnionToIntersection<C>, R>
: never;
type InferReaderFromGeneratorFunction<A> = A extends (
...args: infer P
) => Generator<ReaderI<infer C, any>, infer R>
? (...args: P) => Reader<UnionToIntersection<C>, R>
: never;
type UnionToIntersection<T> = (T extends any ? (x: T) => any : never) extends (
x: infer R
) => any
? R
: never;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment