-
-
Save kasperpeulen/47242fd26a1f677a56f05490d8383cdb to your computer and use it in GitHub Desktop.
Reader monad in TS
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
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); | |
} |
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
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