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.
}
}