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