Skip to content

Instantly share code, notes, and snippets.

@rbuckton
Last active August 11, 2021 18:44
Show Gist options
  • Save rbuckton/4a5108fab40ac90551bf82d9884711b5 to your computer and use it in GitHub Desktop.
Save rbuckton/4a5108fab40ac90551bf82d9884711b5 to your computer and use it in GitHub Desktop.
Example of how ADT enums could be implemented using https://github.com/rbuckton/proposal-struct

If I were to change the definition of @@toEnum slightly, we could build ADT-style enums into https://github.com/rbuckton/proposal-struct.

Currently, @@toEnum takes three arguments: key, value and autoValue. If we change this to take a more complex context argument, we have more flexibility:

interface ValueEnumFactoryContext {
    base: typeof Enum;
    name: string | symbol;
    kind: "value";
    lastValue?: unknown;
    lastAutoValue?: unknown;
}

interface TupleEnumFactoryContext {
    base: typeof Enum;
    name: string | symbol;
    kind: "tuple";
    arguments: DataType[];
    lastValue?: unknown;
    lastAutoValue?: unknown;
}

interface RecordEnumFactoryContext {
    base: typeof Enum;
    name: string | symbol;
    kind: "record";
    // NOTE: DataType is any valid struct data type (i.e., `i32`, `f64`, etc., TBD)
    arguments: Record<string | symbol, DataType>;
    lastValue?: unknown;
    lastAutoValue?: unknown;
}

type EnumFactoryContext =
    | ValueEnumFactoryContext
    | TupleEnumFactoryContext
    | RecordEnumFactoryContext
    ;

Then we could possibly define an ADT enum mapper roughly as follows (with some hand waving around struct):

const ADT = {
    kind: Symbol.for("ADT.kind"),
    Value() {
        return target => {
            Object.defineProperty(target.prototype, ADT.kind, {
                value: "value"
            });
            return target;
        };
    },
    Tuple(elementTypes: DataType[]) {
        return target => {
            for (let i = 0; i < elementTypes.length; i++) {
                Struct.defineField(target.prototype, i, {
                    enumerable: true,
                    writable: true,
                    configurable: true,
                    type: elementTypes[i]
                });
            }
            Object.defineProperty(target.prototype, "length", {
                value: elementTypes.length
            });
            Object.defineProperty(target.prototype, ADT.kind, {
                value: "tuple"
            });
            return target;
        };
    },
    Record(propertyTypes: Record<string | symbol, any, DataType>) {
        return target => {
            for (let key of Reflect.ownKeys(propertyTypes)) {
                Struct.defineField(target.prototype, key, {
                    enumerable: true,
                    writable: true,
                    configurable: true,
                    type: propertyTypes[key]
                });
            }
            Object.defineProperty(target.prototype, ADT.kind, {
                value: "record"
            });
            return target;
        };
    },
    [Symbol.toEnum](context: EnumFactoryContext) {
        const value =
            context.kind === "value" ?
                @ADT.Value()
                struct extends context.base {
                    toString() {
                        return context.name;
                    }
                } :
            context.kind === "tuple" ?
                @ADT.Tuple(context.arguments)
                struct extends context.base {
                    constructor(...values) {
                        super();
                        for (let i = 0; i < context.arguments.length; i++) {
                            this[i] = i < values.length ? values[i] : context.arguments[i]();
                        }
                    }
                    toString() {
                        let s = "";
                        for (let i = 0; i < context.arguments.length; i++) {
                            if (s) s += ", ";
                            let value = this[i];
                            if (typeof value === "box") value = value.unbox();
                            s += value === undefined ? "undefined" :
                                value === null ? "null" :
                                value.toString();
                        }
                        return context.name + "(" + s + ")";
                    }
                } :
            context.kind === "record" ?
                @ADT.Record(context.arguments)
                struct extends context.base {
                    constructor(values) {
                        super();
                        for (const key of Reflect.ownKeys(context.arguments)) {
                            this[key] = key in values ? values[key] : context.arguments[key]();
                        }
                    }
                    toString() {
                        let s = "";
                        for (let key of Reflect.ownKeys(context.arguments)) {
                            if (s) s += ", ";
                            s += typeof key === "symbol" ? key.toString() : JSON.stringify(key);
                            s += ": ";
                            let value = this[key];
                            if (typeof value === "box") value = value.unbox();
                            s += value === undefined ? "undefined" :
                                value === null ? "null" :
                                value.toString();
                        }
                        return context.name + "{" + s + "}";
                    }
                } :
            undefined;
        if (value === undefined) throw new TypeError();
        Object.defineProperty(value, "name", {
            ...Object.getOwnPropertyDescriptor(value, "name"),
            value: context.name
        });
        return value;
    },
    [Symbol.formatEnum](enumValue) {
        return enumValue.toString();
    },
    [Symbol.parseEnum](enumValue) {
        // TBD
        throw new TypeError("Not implemented.");
    }
};

And then implement an ADT-style enum like this:

enum Option of ADT {
  Some{value: box},
  None
}

(or even possibly make that the default behavior)

Which would be translated to something like:

struct Option extends Enum {
  static Some = ADT[Symbol.toEnum]({ base: Option, kind: "record", arguments: { value: box } });
  static None = ADT[Symbol.toEnum]({ base: Option, kind: "value" });
  constructor(value) {
    super();
    // something here to coerce `value` to an `Option`, if that's feasible.
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment