Skip to content

Instantly share code, notes, and snippets.

@JSuder-xx
Last active January 7, 2020 02:43
Show Gist options
  • Save JSuder-xx/9ca99853a3093148e296cd15adc48bd0 to your computer and use it in GitHub Desktop.
Save JSuder-xx/9ca99853a3093148e296cd15adc48bd0 to your computer and use it in GitHub Desktop.
An application of TypeScript conditional/mapped/literal types to Expression construction which yields an inferred static type for the Environment that would satisfy the needs of the Expression. This is magic.
/**
* When implementing the evaluation of a tree of expressions, an environment/context/memory representation is typically passed as an
* argument (or injected into the constructor of the interpreter class for OO realizations of the pattern) in order to give the
* expressions access to a memory store.
*
* The *Type* of the memory does not normally relate to any specific constructed expression ex. the memory may be implemented as an
* _arbitrary_ dictionary of key/value pairs. As such, there is no way to know if a type or instance of a dictionary of values can
* satisfy the needs to a specific constructed expression.
*
* For example, the following expression reads a number named 'x' from the environment and therefore the expression _requires_ an x of type number
* in order to run without throwing a run-time fault.
*
* const add10ToX = add(readNumberFromEnvironment("x"), literal(10));
*
* This gist provides a twist compliments of the unique abilities of TypeScript type system!
*
* The code below demonstrates a classic expression system where the act of composing an expression refines the type of the Environment
* object that would satisfy the needs of the expression.
*
* This gist can be copy/pasted into the TypeScript playground.
*/
module ReadMe {}
module ExpressionType {
/** Representation of the environment/context/memory passed to each expression. */
export type Environment = {};
/** An expression is a function which accepts an environment object and returns a value. */
export type Expression<environment extends Environment, expressionResult> = (env: environment) => expressionResult;
/** Extract the return type of the expression. */
export type ExpressionReturnType<expression> =
expression extends Expression<any, infer resultType>
? resultType
: never;
/** Extract the environment type of the expression. */
export type ExpressionEnvironment<expression> =
expression extends Expression<infer environment, any>
? environment
: never;
/** A tuple of expression (1, 2, 3, or 4) */
export type ExpressionTuple =
[Expression<any, any>]
| [Expression<any, any>, Expression<any, any>]
| [Expression<any, any>, Expression<any, any>, Expression<any, any>]
| [Expression<any, any>, Expression<any, any>, Expression<any, any>, Expression<any, any>];
/** Lift a plain single argument function into the Expression space. */
export const lift1 = <original, result>(fn: (value: original) => result) =>
<environment extends Environment>(value: Expression<environment, original>): Expression<environment, result> =>
(env: environment) =>
fn(value(env));
/** Lift a plain two argument function into the Expression space (good for binary operators). */
export const lift2 = <original, result>
(fn: (left: original, right: original) => result) =>
<leftEnvironment, rightEnvironment, specificOriginal extends original>(
left: Expression<leftEnvironment, specificOriginal>
, right: Expression<rightEnvironment, specificOriginal>
): Expression<leftEnvironment & rightEnvironment, result> =>
(env: leftEnvironment & rightEnvironment) =>
fn(left(env), right(env));
/** The unit/return/constant for the Expression type. */
export const unit = <TResult>(val: TResult) => (env: Environment) => val;
}
/** Operators which can be used to compose/construct rudimentary functional programs as Expressions. */
module ExpressionOperators {
const throwError = <T>(errorMessage: string): T => { throw new Error(errorMessage); }
const evaluateRefined = <T, TResult>(val: Object, klass: { new (...args: any[]): T }, errorMessage: string, fn: (val: T) => TResult) =>
(val instanceof klass) ? fn(val) : throwError<TResult>(errorMessage);
const {lift1, lift2, unit} = ExpressionType;
import Expression = ExpressionType.Expression;
import Environment = ExpressionType.Environment;
import ExpressionTuple = ExpressionType.ExpressionTuple;
import ExpressionReturnType = ExpressionType.ExpressionReturnType;
import ExpressionEnvironment = ExpressionType.ExpressionEnvironment;
// Number Operators
export const add = lift2((left: number, right: number) => left + right);
export const subtract = lift2((left: number, right: number) => left - right);
export const multiply = lift2((left: number, right: number) => left * right);
export const divide = lift2((left: number, right: number) => left / right);
// Boolean Operators
export const and = lift2((left: boolean, right: boolean) => left && right);
export const or = lift2((left: boolean, right: boolean) => left || right);
export const not = lift1((val: boolean) => !val);
// String
export const stringConcat = lift2((left: string, right: string) => left + right);
export const convertNumberToString = lift1((val: number) => val.toString());
// Equality
export const equal = lift2(<T>(left: T, right: T) => left === right);
// Inequality
type Comparable = string | number | Date;
export const lessThan = lift2((left: Comparable, right: Comparable) => left < right);
export const lessThanEqualTo = lift2((left: Comparable, right: Comparable) => left <= right);
export const greaterThan = lift2((left: Comparable, right: Comparable) => left > right);
export const greaterThanEqualTo = lift2((left: Comparable, right: Comparable) => left >= right);
// Literal
export const literal = unit;
// Read from the environment (Type Effects: Refines the Environment)
export const readValueFromEnvironment =
<resultType>() =>
<variableName extends string>(variableName: variableName) =>
(env: Record<variableName, resultType>): resultType =>
env[variableName];
export const readNumberFromEnvironment = readValueFromEnvironment<number>();
export const readStringFromEnvironment = readValueFromEnvironment<string>();
export const readBooleanFromEnvironment = readValueFromEnvironment<boolean>();
export const readDateFromEnvironment = readValueFromEnvironment<Date>();
/**
* Given a return type and a tuple of Expression, return a function type for a function that returns the specified type and which accepts positional argument
* corresponding to the expression result types.
*/
type FunctionTypeFromArguments<returnType, tupleOfExpression> =
tupleOfExpression extends [infer first, infer second, infer third, infer fourth] ? (one: ExpressionReturnType<first>, two: ExpressionReturnType<second>, three: ExpressionReturnType<third>, fourth: ExpressionReturnType<fourth>) => returnType
: tupleOfExpression extends [infer first, infer second, infer third] ? (one: ExpressionReturnType<first>, two: ExpressionReturnType<second>, three: ExpressionReturnType<third>) => returnType
: tupleOfExpression extends [infer first, infer second] ? (one: ExpressionReturnType<first>, two: ExpressionReturnType<second>) => returnType
: tupleOfExpression extends [infer first] ? (one: ExpressionReturnType<first>) => returnType
: never;
/**
* Given a tuple of Expression, return the Environment type that would be required to satisfy the needs of all the expressions.
*/
type EnvironmentRequiredForExpressionTuple<tupleOfExpression> =
tupleOfExpression extends [infer first, infer second, infer third, infer fourth] ? ExpressionEnvironment<first> & ExpressionEnvironment<second> & ExpressionEnvironment<third> & ExpressionEnvironment<fourth>
: tupleOfExpression extends [infer first, infer second, infer third] ? ExpressionEnvironment<first> & ExpressionEnvironment<second> & ExpressionEnvironment<third>
: tupleOfExpression extends [infer first, infer second] ? ExpressionEnvironment<first> & ExpressionEnvironment<second>
: tupleOfExpression extends [infer first] ? ExpressionEnvironment<first>
: never;
export const callFunctionFromEnvironment =
<functionName extends string>(functionName: functionName) =>
<resultType>() =>
<functionArguments extends ExpressionTuple>(functionArguments: functionArguments) =>
(env: Record<functionName, FunctionTypeFromArguments<resultType, functionArguments>> & EnvironmentRequiredForExpressionTuple<functionArguments>): resultType =>
evaluateRefined(
env[functionName]
, Function
, `Symbol '${functionName}' is not a function.`
, (fun) => {
if (fun.length !== functionArguments.length)
throw new Error(`Function '${functionName}' expects ${fun.length} arguments but was given ${functionArguments.length}`);
return <resultType>fun.apply(undefined, functionArguments.map(it => it(env)));
}
);
/** let - introduce a symbol. */
export const letBind =
<symbolName extends string, symbolType, symbolExpressionEnvironment extends Environment, bodyExpressionEnvironment extends Environment, resultType>(
/** Name of the symbol for the let binding. Introduced into the environment. */
symbolName: symbolName
/** Expression which determines the value that will be bound. */
, symbolExpression: Expression<symbolExpressionEnvironment, symbolType>
/** Expression which will be evaluated with the newly bound symbol available. */
, bodyExpression: Expression<Record<symbolName, symbolType> & bodyExpressionEnvironment, resultType>
) =>
<env extends bodyExpressionEnvironment & symbolExpressionEnvironment>(env: Pick<env, Exclude<keyof env, symbolName>>): resultType => {
const letEnvironment = {
...env
, [symbolName]: symbolExpression(<any>env)
};
return bodyExpression(<any>letEnvironment);
};
/** Ternary if/then/else expression. */
export const ifThenElse = <booleanEnvironment extends {}, thenEnvironment extends {}, elseEnvironment extends {}, result>(
booleanExpr: Expression<booleanEnvironment, boolean>
, thenExpr: Expression<thenEnvironment, result>
, elseExpr: Expression<elseEnvironment, result>
) =>
(env: (booleanEnvironment & thenEnvironment & elseEnvironment)): result =>
booleanExpr(env)
? thenExpr(env)
: elseExpr(env);
}
module Example {
const {
add
, greaterThan
, literal, readNumberFromEnvironment, callFunctionFromEnvironment
, stringConcat, convertNumberToString
, letBind
, ifThenElse
} = ExpressionOperators;
try {
const myExpression =
letBind(
// TRY: Change the name of the binding and observe the error on the assignment to environment below.
"firstUserNumber"
// TRY: Change <number> to <string> and observe the error in the assignment.
, callFunctionFromEnvironment('getNumberFromUser')<number>()([literal("Please provide a number")])
, stringConcat(
literal("Your first number ")
, stringConcat(
convertNumberToString(readNumberFromEnvironment("firstUserNumber"))
, stringConcat(
literal(" is ")
, stringConcat(
ifThenElse(
greaterThan(
readNumberFromEnvironment("firstUserNumber")
// TRY: Comment out the readNumberVariable and replace with literal("Hi")
, add(readNumberFromEnvironment("injectedNumber"), literal(10))
// literal("Hi")
)
, literal("")
, literal("NOT ")
),
stringConcat(
literal("greater than (injected number ")
, stringConcat(
convertNumberToString(readNumberFromEnvironment("injectedNumber"))
, literal(") + 10")
)
)
)
)
)
)
);
// TRY: Mouse over MyExpressionEnvironment to inspect the type of the environment inferred from the expression.
type MyExpressionEnvironment = ExpressionType.ExpressionEnvironment<typeof myExpression>;
let environment: MyExpressionEnvironment = {
// TRY: Commenting out a member.
injectedNumber: 20
, getNumberFromUser: (msg: string) => {
const result = prompt(msg, "0");
if (result === null)
return 0;
else
return Number(result);
}
};
alert(myExpression(environment));
} catch (ex) {
alert(`Error: ${ex instanceof Error ? ex.message : (ex + '')}`);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment