You can cut and paste this big fat code block into the TS editor of your choice and have a play with the Solid monad. Go on. It's fun!
export type Scope<Keys extends string> = {
[K in Keys]: any;
};
export type ExtendedScope<
OldScope extends Scope<any>,
NewField extends string,
NewValue
> = OldScope extends any
? {
[K in keyof OldScope | NewField]: K extends NewField
? NewValue
: K extends keyof OldScope
? OldScope[K]
: never;
}
: never;
export const extendScope = <
OldScope extends Scope<any>,
NewField extends string,
NewValue
>(
oldScope: OldScope,
newField: NewField,
newValue: NewValue
) => {
const addToScope = {
[newField]: newValue,
};
return {
...oldScope,
...addToScope,
} as ExtendedScope<OldScope, NewField, NewValue>;
};
export type AsyncMaybe<T> = Promise<T | null | undefined>;
export namespace AsyncMaybe {
export const bind = async <A, B>(
prevAsync: AsyncMaybe<A>,
func: (prev: A) => AsyncMaybe<B>
): AsyncMaybe<B> => {
const prev = await prevAsync;
if (prev === null || prev === undefined) {
return null;
} else {
return func(prev);
}
};
export const pure = <A>(value: A): AsyncMaybe<A> => Promise.resolve(value);
}
export class AsyncMaybeChain<Input, OutputScope extends Scope<any>> {
private readonly operation: (input: Input) => AsyncMaybe<OutputScope>;
constructor(starter: (input: Input) => AsyncMaybe<OutputScope>) {
this.operation = starter;
}
execute(input: Input): AsyncMaybe<OutputScope> {
return this.operation(input);
}
chain<NextOutput, ResultName extends string>(
resultName: ResultName,
next: (previous: OutputScope) => AsyncMaybe<NextOutput>
): AsyncMaybeChain<
Input,
ExtendedScope<OutputScope, ResultName, NextOutput>
> {
const chainedOperation = (
input: Input
): AsyncMaybe<ExtendedScope<OutputScope, ResultName, NextOutput>> => {
const previousResult = this.operation(input);
return AsyncMaybe.bind(previousResult, (output: OutputScope) => {
return AsyncMaybe.bind(next(output), (nextOutput) => {
return AsyncMaybe.pure(extendScope(output, resultName, nextOutput));
});
});
};
return new AsyncMaybeChain(chainedOperation);
}
map<NextOutput, ResultName extends string>(
resultName: ResultName,
next: (previous: OutputScope) => NextOutput
): AsyncMaybeChain<
Input,
ExtendedScope<OutputScope, ResultName, NextOutput>
> {
return this.chain(resultName, (scope) => AsyncMaybe.pure(next(scope)));
}
executeTarget<ResultName extends keyof OutputScope>(resultName: ResultName) {
return (input: Input) =>
AsyncMaybe.bind(this.execute(input), (outputScope) =>
AsyncMaybe.pure(outputScope[resultName])
);
}
static start<InputScope extends Scope<any>>(): AsyncMaybeChain<
InputScope,
InputScope
> {
return new AsyncMaybeChain(AsyncMaybe.pure<InputScope>);
}
}
export const maybeTest = AsyncMaybeChain.start<{ initialInput: string }>()
.chain("punctuation", () => AsyncMaybe.pure("!"))
.chain("finalGreeting", (scope) =>
AsyncMaybe.pure(`Hello ${scope.initialInput}${scope.punctuation}`)
).execute;
export interface MaybeData {
getName: (userId: string) => Promise<string | null>;
getPunctuation: (name: string) => Promise<string | undefined>;
}
export const before = async ({
userId,
maybeData,
}: {
userId: string;
maybeData: MaybeData;
}) => {
const name = await maybeData.getName(userId);
if (name === null) {
return null;
}
const punctuation = await maybeData.getPunctuation(name);
if (punctuation === undefined) {
return null;
}
return `Hello ${name}${punctuation}`;
};
export const after = AsyncMaybeChain.start<{
userId: string;
maybeData: MaybeData;
}>()
.chain("name", (s) => s.maybeData.getName(s.userId))
.chain("punctuation", (s) => s.maybeData.getPunctuation(s.name))
.map("result", (s) => `${s.name}{s.punctuation}`)
.executeTarget("result");
export const exhausted = (narrowedType: never) => narrowedType;
export type SolidSuccess<Success> = {
kind: "success";
value: Success;
};
export type SolidFailure<Failure> = {
kind: "failure";
failure: Failure;
};
export type Solid<Success, Failure, State extends Scope<any>> = (
state: Partial<State>
) => Promise<{
state: Partial<State>;
result: SolidSuccess<Success> | SolidFailure<Failure>;
}>;
export const Solid = {
pure: <Success, State extends Scope<any>>(
value: Success
): Solid<Success, never, State> => {
return async (state) => ({ state, result: { kind: "success", value } });
},
failure: <Failure, State extends Scope<any>>(
failure: Failure
): Solid<never, Failure, State> => {
return async (state) => ({ state, result: { kind: "failure", failure } });
},
bind: <Success, NextSuccess, Failure, NextFailure, State extends Scope<any>>(
prev: Solid<Success, Failure, State>,
func: (success: Success) => Solid<NextSuccess, NextFailure, State>
): Solid<NextSuccess, Failure | NextFailure, State> => {
return async (state: Partial<State>) => {
const awaitedPrevious = await prev(state);
const prevResult = awaitedPrevious.result;
if (prevResult.kind === "success") {
const next = await func(prevResult.value)(prevResult.state);
return next;
} else {
return { state: prevResult.state, result: prevResult };
}
};
},
get: <Failure, State extends Scope<any>>(): Solid<
Partial<State>,
Failure,
State
> => {
return async (state: Partial<State>) => ({
state,
result: { kind: "success", value: state },
});
},
modify: <State extends Scope<any>>(
func: (prev: Partial<State>) => Partial<State>
): Solid<Partial<State>, never, State> => {
return async (state: Partial<State>) => {
const newState = func(state);
return {
state: newState,
result: { kind: "success", value: newState },
};
};
},
set: <State extends Scope<any>, Key extends keyof State>(
key: Key,
value: State[Key]
): Solid<void, never, State> => {
return Solid.bind(
Solid.modify((prev) => ({ ...prev, [key]: value })),
() => Solid.pure(undefined)
);
},
noThrow: <Success, Failure, State extends Scope<any>>(
solid: Solid<Success, Failure, State>
): Solid<Success, Failure | UnhandledExceptionFailure, State> => {
return (state: Partial<State>) => {
try {
return solid(state);
} catch (e) {
return Solid.failure({
type: "UnhandledException" as const,
thrown: e,
})(state);
}
};
},
map: <Previous, Next, Failure, State extends Scope<any>>(
func: (previous: Previous) => Next,
prev: Solid<Previous, Failure, State>
) => {
return Solid.bind(prev, (success) => Solid.pure(func(success)));
},
apply: <Success, NextSuccess, Failure, NextFailure, State extends Scope<any>>(
funcInMonad: Solid<(prev: Success) => NextSuccess, NextFailure, State>,
prev: Solid<Success, Failure, State>
): Solid<NextSuccess, Failure | NextFailure, State> => {
return Solid.bind(funcInMonad, (func) => Solid.map(func, prev));
},
lift2: <
Left,
Right,
Result,
LeftFailure,
RightFailure,
State extends Scope<any>
>(
operation: (left: Left, right: Right) => Result,
left: Solid<Left, LeftFailure, State>,
right: Solid<Right, RightFailure, State>
): Solid<Result, LeftFailure | RightFailure, State> => {
return Solid.apply(
Solid.map((left: Left) => (right: Right) => operation(left, right), left),
right
);
},
traverse: <Input, Success, Failure, State extends Scope<any>>(
inputs: Input[],
func: (input: Input) => Solid<Success, Failure, State>
): Solid<Success[], Failure, State> => {
return inputs.reduce<Solid<Success[], Failure, State>>(
(acc, next) =>
Solid.lift2(
(results: Success[], next: Success) => {
results.push(next);
return results;
},
acc,
func(next)
),
Solid.pure([])
);
},
};
export type UnhandledExceptionFailure = {
type: "UnhandledException";
thrown: any;
};
export class SolidChain<
Input,
OutputScope extends Scope<any>,
Failure,
State extends Scope<any>
> {
private readonly operation: (
input: Input
) => Solid<OutputScope, Failure, State>;
constructor(starter: (input: Input) => Solid<OutputScope, Failure, State>) {
this.operation = starter;
}
execute(
input: Input
): Solid<OutputScope, Failure | UnhandledExceptionFailure, State> {
return Solid.noThrow(this.operation(input));
}
chain<NextOutput, NextFailure, ResultName extends string>(
resultName: ResultName,
next: (previous: OutputScope) => Solid<NextOutput, NextFailure, State>
): SolidChain<
Input,
ExtendedScope<OutputScope, ResultName, NextOutput>,
Failure | NextFailure,
State
> {
const chainedOperation = (
input: Input
): Solid<
ExtendedScope<OutputScope, ResultName, NextOutput>,
Failure | NextFailure,
State
> => {
const previousResult = this.operation(input);
return Solid.bind(previousResult, (output: OutputScope) => {
return Solid.bind(next(output), (nextOutput) => {
return Solid.pure(extendScope(output, resultName, nextOutput));
});
});
};
return new SolidChain(chainedOperation);
}
map<NextOutput, ResultName extends string>(
resultName: ResultName,
next: (previous: OutputScope) => NextOutput
): SolidChain<
Input,
ExtendedScope<OutputScope, ResultName, NextOutput>,
Failure,
State
> {
return this.chain(resultName, (scope) => Solid.pure(next(scope)));
}
tap<NextFailure>(
func: (previous: OutputScope) => Solid<void, NextFailure, State>
): SolidChain<Input, OutputScope, Failure | NextFailure, State> {
return new SolidChain((input: Input) =>
Solid.bind(this.operation(input), (scope) =>
Solid.bind(func(scope), () => Solid.pure(scope))
)
);
}
executeTarget<ResultName extends keyof OutputScope>(
resultName: ResultName
): (
input: Input
) => Solid<
OutputScope[ResultName],
Failure | UnhandledExceptionFailure,
State
> {
return (input: Input) =>
Solid.noThrow(
Solid.bind(this.execute(input), (outputScope) =>
Solid.pure(outputScope[resultName])
)
);
}
static start<
InputScope extends Scope<any>,
State extends Partial<Scope<any>>
>(): SolidChain<InputScope, InputScope, never, State> {
return new SolidChain(Solid.pure<InputScope, State>);
}
}
type HttpRequest = {};
type User = {};
type Organization = {};
type Command = {};
type Events = {};
type LaxMessage = {};
type LaxContext = {
laxUserId: string;
laxOrganizationId: string;
laxResponseUrl: URL;
actionPayload: any;
};
export interface LaxOperations {
checkSignature: <State extends Scope<any>>(scope: {
httpRequest: HttpRequest;
}) => Solid<
"laxSignatureOk",
{ type: "InvalidLaxSignature"; message: string },
State
>;
parseRequest: <State extends { laxContext: LaxContext }>(scope: {
laxSignatureCheck: "laxSignatureOk";
}) => Solid<
LaxContext,
{ type: "UnableToParseLaxContext"; message: string },
State
>;
reply: <State extends { laxContext: LaxContext }>(scope: {
laxContext: LaxContext;
reply: LaxMessage;
}) => Solid<
void,
{ type: "CouldNotContactLax" | "LaxRefusedReply"; message: string },
State
>;
findUserAndOrganization: <State extends Scope<any>>(scope: {
laxContext: LaxContext;
}) => Solid<
{ user: User; organization: Organization },
{ type: "OrganizationNotFound" | "UserNotFound"; message: string },
State
>;
}
export interface Commands {
parseUntrustedCommand: <State extends Scope<any>>(scope: {
untrustedCommand: any;
}) => Solid<Command, { type: "UnrecognizedCommand"; message: string }, State>;
executeCommand: <State extends Scope<any>>(args: {
userInfo: { user: User; organization: Organization };
command: Command;
}) => Solid<
Events,
{ type: "NotPermitted" | "ValidationFailed"; message: string },
State
>;
}
export interface TheseWouldBeLocalFunctions {
createSuccessResponse: (scope: {
command: Command;
userInfo: { user: User; organization: Organization };
eventsCaused: Events;
}) => LaxMessage;
}
export type LaxCallBackState = {
laxContext: LaxContext;
user: User;
organization: Organization;
};
export type LaxCallbackDependencies = {
laxOperations: LaxOperations;
commands: Commands;
localFunctions: TheseWouldBeLocalFunctions;
};
export const processLaxCallback = ({
laxOperations,
commands,
localFunctions,
}: LaxCallbackDependencies) =>
SolidChain.start<{ httpRequest: HttpRequest }, LaxCallBackState>()
.chain("laxSignatureCheck", laxOperations.checkSignature)
.chain("laxContext", laxOperations.parseRequest)
.tap(({ laxContext }) => Solid.set("laxContext", laxContext))
.chain("command", ({ laxContext }) =>
commands.parseUntrustedCommand({
untrustedCommand: laxContext.actionPayload,
})
)
.chain("userInfo", laxOperations.findUserAndOrganization)
.tap(({ userInfo }) => Solid.set("user", userInfo.user))
.tap(({ userInfo }) => Solid.set("organization", userInfo.organization))
.chain("eventsCaused", commands.executeCommand)
.map("reply", localFunctions.createSuccessResponse)
.tap(laxOperations.reply)
.executeTarget("eventsCaused");
export const laxCallbackHandler =
(deps: LaxCallbackDependencies) => async (httpRequest: HttpRequest) => {
const processResult = await processLaxCallback(deps)({ httpRequest })({});
if (processResult.result.kind === "success") {
console.log("Woot! Created events: ", processResult.result.value);
} else {
const reportError = async (message: string) => {
console.log(
"Always report errors internally with full info including stack trace for unhandled exceptions",
processResult
);
if (processResult.state.laxContext) {
await deps.laxOperations.reply({
laxContext: processResult.state.laxContext,
reply: message as any as LaxMessage,
})({});
}
};
switch (processResult.result.failure.type) {
case "NotPermitted":
await reportError("You can't do that");
break;
case "ValidationFailed":
await reportError("You sent the wrong information");
break;
case "UnrecognizedCommand":
await reportError(
"Something is wrong with the message we sent you, sorry!"
);
break;
case "OrganizationNotFound":
case "UserNotFound":
await reportError("You don't seem to be fully set up on Lax yet");
break;
case "UnableToParseLaxContext":
case "InvalidLaxSignature":
await reportError("Invalid callback");
break;
case "CouldNotContactLax":
await reportError(
"We tried to send you a message, but something went wrong at Lax's end."
);
break;
case "LaxRefusedReply":
await reportError(
"We tried to send you a message but something went wrong on our end."
);
break;
case "UnhandledException":
await reportError(
"Something went wrong, our support staff will look into it"
);
break;
default:
return exhausted(processResult.result.failure);
}
}
};
export const traverseExample =
(commandOperations: Commands) => (untrustedInput: any[]) =>
SolidChain.start<
{
untrustedInput: any[];
userInfo: { user: User; organization: Organization };
},
{}
>()
.chain("parsedCommands", ({ untrustedInput }) =>
Solid.traverse(untrustedInput, commandOperations.parseUntrustedCommand)
)
.chain("resultingEvents", ({ parsedCommands, userInfo }) =>
Solid.traverse(parsedCommands, (command) =>
commandOperations.executeCommand({ command, userInfo })
)
)
.executeTarget("resultingEvents");
const expandingErrors =
(commandOperations: Commands) =>
(untrustedInput: any, userInfo: { user: User; organization: Organization }) =>
Solid.bind(
commandOperations.parseUntrustedCommand({
untrustedCommand: untrustedInput,
}),
(command) => commandOperations.executeCommand({ command, userInfo })
);