Last active
January 7, 2020 02:43
-
-
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.
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
/** | |
* 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