From d512bfb74c901f06eca3cef727d56e81519f1768 Mon Sep 17 00:00:00 2001 From: mccraig mccraig of the clan mccraig Date: Fri, 3 Nov 2023 09:34:41 +0000 Subject: [PATCH] an effectful pipeline for building objects --- refine_map.ts | 193 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 refine_map.ts diff --git a/refine_map.ts b/refine_map.ts new file mode 100644 index 0000000..6acba98 --- /dev/null +++ b/refine_map.ts @@ -0,0 +1,193 @@ +// deno-lint-ignore-file no-explicit-any +import { Effect, Context } from "effect" + +// reference: +// https://dev.to/ecyrbe/how-to-use-advanced-typescript-to-define-a-pipe-function-381h + +// each Effectful step defines: +// - a key +// - a tag for an FxService +// - a fn which takes the output map of the previous step and +// returns the input type of the service +// +// - and the output of the step is a new map type combining the inmap +// with the new key and output value from the service + +// an Effectful Service interface which takes a single argument +export interface FxService { + readonly fx: { + (arg: D): Effect.Effect + } +} +export type FxServiceTag = Context.Tag> + +// data defining a single Effectful step towards building an Object. +// f transforms the Object-so-far:A into the FxService argument D, +// and the output of the FxService will then be added to the Object-so-far +// at {K: V} +export type StepSpec = { + // the key at which the service output will be added to the pipeline accumulator object A + readonly k: K + // a pure function which maps the accumulator to the service input D + readonly f: (arg: A) => D + // a service requiring data D to produce value V + readonly svc: FxServiceTag +} + +// extract the FxServiceTag from a StepSpec +export type ExtractFxServiceTag = T extends StepSpec + ? T["svc"] + : never + +// extract the value type from a StepSpec +export type ExtractValueType = ExtractFxServiceTag extends FxServiceTag ? V : never + +// recursively infer a tuple-type for an Effectful Object builder pipeline +// from a tuple of StepSpecs, building up the Obj type along the way +export type ObjectPipeline = + + // case: final spec - deliver final pipeline tuple type from StepAcc + Specs extends [StepSpec] + ? readonly [...StepAcc, StepSpec] + + // case: there are more specs - add to StepAcc and ObjAcc and recurse + : Specs extends [infer Head, ...infer Tail] + ? Head extends StepSpec + ? Tail extends [StepSpec, ...any] + ? ObjectPipeline]> + : ["ObjectPipelineFail", "C", Specs] // Tail + : ["ObjectPipelineFail", "B", Specs] // Head + : ["ObjectPipelineFail", "A", Specs] // Specs + +// builds a new Object type from an intersected ObjAcc type, +// which makes the intellisense much simpler +// https://stackoverflow.com/questions/57683303/how-can-i-see-the-full-expanded-contract-of-a-typescript-type +export type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never; + +// get the final Object type from a list of StepSpecs +export type FinalObjectType = + ObjectPipeline extends [...infer _Prev, infer Last] + ? Last extends StepSpec + ? Expand + : ["FinalObjectTypeFail", "B", ObjectPipeline] // Last + : ["FinalObjectTypeFail", "A", ObjectPipeline] // ObjectPipeline + +////////////////////////////////////////////////////////////////////////////// + +export type User = { + id: string + name: string +} +interface GetUserService { readonly _: unique symbol } +// the service interface +export interface GetUserServiceI extends FxService { + readonly fx: (id: string) => Effect.Effect +} +export const GetUserService = Context.Tag("GetUserService") + +export type Org = { + id: string + name: string +} +interface GetOrgService { readonly _: unique symbol } +export interface GetOrgServiceI extends FxService { + readonly fx: (id: string) => Effect.Effect +} +export const GetOrgService = Context.Tag("GetOrgService") + +// allow extra keys for the param fns +export type Extra = T extends Record ? T & Record : never; + +// as const is required to prevent the k from being widened to a string type +// and to ensure the specs array is interpreted as a tuple +const getOrgStepSpecObj = +{ + k: "org" as const, + svc: GetOrgService, + f: (d: { data: {org_id: string} }) => d.data.org_id +} +const getUserStepSpecObj = +{ + k: "user" as const, + svc: GetUserService, + f: (d: { data: {user_id: string} }) => d.data.user_id +} +export const specs = [ + getOrgStepSpecObj, + getUserStepSpecObj +] as const + +////////////////////////////////////////////////////////////////////////////////// + +// getting closer... + +// also constraints are not yet being applied so that the f inputs match +// the ObjAcc + +export declare function buildProg + > + (stepSpecs: readonly [...StepSpecs], + otherStepSpecs: ObjectPipeline) + : (arg: Init) => Effect.Effect> + +export const prog = buildProg(specs, specs) + + + + +///////////////////////////////////////////////////////////////////////////// + + +// an Event has a tag to identify a handler +export interface EventI { + readonly tag: string +} + +// a simple tag type for events +export interface EventTag { + readonly tag: EV['tag'] // a string name for the type +} + +// returns a function of EV returning an Effect which applys the steps specified +// in StepSpecs to build an Object with all the {K: V} from each step's service +export declare function buildObjectProg + (stepSpecs: readonly [...StepSpecs]) + : (arg: Init) => Effect.Effect> + +export const objProg = buildObjectProg(specs) + +//////////////////////////////////////////// + +type AnyFunc = (...arg: any) => any; + +type PipeArgs = + F extends [(...args: infer A) => infer B] ? [...Acc, (...args: A) => B] + : F extends [(...args: infer A) => any, ...infer Tail] + ? Tail extends [(arg: infer B) => any, ...any[]] + ? PipeArgs B]> + : Acc + : Acc; + +type LastFnReturnType, Else = never> = F extends [ + ...any[], + (...arg: any) => infer R +] ? R : Else; + +export function pipe( + arg: Parameters[0], + firstFn: FirstFn, + ...fns: PipeArgs extends F ? F : PipeArgs +): LastFnReturnType> { + return (fns as AnyFunc[]).reduce((acc, fn) => fn(acc), firstFn(arg)); +} + + +export const x = pipe(0, (n: number)=>n+1, (p: number)=>p.toString()) \ No newline at end of file