From 0b262daf7f9a489775e4bff5b632f1c02d83ecd3 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Thu, 17 Apr 2025 16:10:00 +0200 Subject: [PATCH] add enum type (generalization of sum-type) --- primitives/types.js | 7 +++- structures/enum.js | 88 +++++++++++++++++++++++++++++++++++++++++++ structures/product.js | 1 + structures/struct.js | 5 +-- structures/sum.js | 4 +- util/util.js | 4 ++ 6 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 structures/enum.js diff --git a/primitives/types.js b/primitives/types.js index 2017940..7bf5bdb 100644 --- a/primitives/types.js +++ b/primitives/types.js @@ -8,9 +8,10 @@ const SymbolDouble = Symbol('Double'); const SymbolByte = Symbol('Byte'); const SymbolChar = Symbol('Char'); const SymbolUnit = Symbol('Unit'); +const SymbolBottom = Symbol('⊥'); const SymbolSymbol = Symbol('Symbol'); const SymbolType = Symbol('Type'); -const symbolAny = Symbol('Any'); +const symbolAny = Symbol('Any'); const SymbolGenericType = Symbol('GenericType'); export const Int = makeTypeConstructor(SymbolInt)(0); @@ -22,6 +23,9 @@ export const Char = makeTypeConstructor(SymbolChar)(0); // Unit type has only 1 instance, the empty tuple. export const Unit = makeTypeConstructor(SymbolUnit)(0); +// Bottom type has no instances. +export const Bottom = makeTypeConstructor(SymbolBottom)(0); + export const SymbolT = makeTypeConstructor(SymbolSymbol)(0); // Types are typed by Any @@ -39,6 +43,7 @@ export const ModuleSymbols = {l:[ {i: SymbolByte , t: SymbolT}, {i: SymbolChar , t: SymbolT}, {i: SymbolUnit , t: SymbolT}, + {i: SymbolBottom, t: SymbolT}, {i: SymbolSymbol, t: SymbolT}, {i: SymbolType , t: SymbolT}, {i: SymbolGenericType, t: SymbolT}, diff --git a/structures/enum.js b/structures/enum.js new file mode 100644 index 0000000..93be526 --- /dev/null +++ b/structures/enum.js @@ -0,0 +1,88 @@ +import { Bottom, Int, Unit } from "../primitives/types.js"; +import { unit } from "../primitives/unit.js"; +import { capitalizeFirstLetter } from "../util/util.js"; +import { constructorProduct, getLeft, getRight } from "./product.js"; +import { constructorLeft, constructorRight, match } from "./sum.js"; +import { lsType, prettyT, sumType } from "./types.js"; + +// 'variants' is an array of (name: string, type: Type) pairs. +// e.g., the list of variants: +// [ { l: "price" , r: Int }, +// { l: "prices" , r: [Int] }, +// { l: "not_found", r: Unit } ] +// results in the type: +// (Int | ([Int] | (Unit | ⊥))) +export const enumType = variants => { + if (variants.length === 0) { + return Bottom; // empty enum is equal to Bottom-type (cannot be instantiated) + } + const [variant, ...rest] = variants; + const variantType = getRight(variant); + return sumType(variantType)(enumType(rest)); +}; + +const eatParameters = (numParams, result) => { + if (numParams === 0) { + return result; + } + else return () => eatParameters(numParams-1, result); +} + +export const makeMatchFn = variants => { + if (variants.length === 0) { + return undefined; + } + const [_, ...remainingVariants] = variants; + return sum => handler => { + return ( + match(sum)(constructorProduct + (leftValue => eatParameters(remainingVariants.length, handler(leftValue))) + (rightValue => makeMatchFn(remainingVariants)(rightValue)) + )); + }; +}; + +export const makeConstructors = variants => { + if (variants.length === 0) { + return []; + } + const [variant, ...remainingVariants] = variants; + const name = getLeft(variant); + const ctorName = `new${capitalizeFirstLetter(name)}`; + const constructor = { [ctorName]: val => constructorLeft(val) }[ctorName]; + return [ + constructor, + ...makeConstructors(remainingVariants).map(ctor => + ({[ctor.name]: val => constructorRight(ctor(val))}[ctor.name])), + ]; +} + + +/////////////////////////////////////////////////////////////////////////////// + +const variants = [ + constructorProduct("price")(Int), + constructorProduct("prices")(lsType(Int)), + constructorProduct("not_found")(Unit), +]; + +const myEnum = enumType(variants); + +console.log(prettyT(myEnum)); + +const [newPrice, newPrices, newNotFound] = makeConstructors(variants); + +console.log(newPrice(10)); +console.log(newPrices([20,30])); +console.log(newNotFound(unit)); + +const myMatchFn = makeMatchFn(variants); + +const matchVariant = x => myMatchFn(x) + (price => `Price: ${price}`) + (prices => `Prices: ${prices}`) + (() => "Not found!"); + +console.log(matchVariant(newPrice(10))); +console.log(matchVariant(newPrices([20,30]))); +console.log(matchVariant(newNotFound(unit))); diff --git a/structures/product.js b/structures/product.js index 0e4e5e8..af65c09 100644 --- a/structures/product.js +++ b/structures/product.js @@ -1,4 +1,5 @@ // Product-type (also called: pair, tuple) + // A Product-type always has only two fields, called "left" and "right". // Product-types of more fields (called Structs) can be constructed by nesting Product-types. diff --git a/structures/struct.js b/structures/struct.js index dd91db4..649d54c 100644 --- a/structures/struct.js +++ b/structures/struct.js @@ -1,12 +1,9 @@ import { Unit } from "../primitives/types.js"; import { unit } from "../primitives/unit.js"; +import { capitalizeFirstLetter } from "../util/util.js"; import { constructorProduct, getLeft, getRight } from "./product.js"; import { fnType, prodType } from "./types.js"; -function capitalizeFirstLetter(val) { - return String(val).charAt(0).toUpperCase() + String(val).slice(1); -} - // 'fields' is an array of (name: string, type: Type) pairs. // e.g.: // [{l: "x", r: Double}, {l: "y", r: Double}] diff --git a/structures/sum.js b/structures/sum.js index bb02ff3..274588f 100644 --- a/structures/sum.js +++ b/structures/sum.js @@ -15,8 +15,8 @@ export const constructorRight = right => ({t: "R", v: right}); // sum-type -> (leftType -> resultType, rightType -> resultType) -> resultType export const match = sum => handlers => sum.t === "L" - ? handlers.left(sum.v) - : handlers.right(sum.v); + ? handlers.l(sum.v) + : handlers.r(sum.v); export const ModuleSum = {l:[ // binary type constructor diff --git a/util/util.js b/util/util.js index db37406..d1ab25f 100644 --- a/util/util.js +++ b/util/util.js @@ -42,4 +42,8 @@ export function zip(a, b) { return a.map((k, i) => [k, b[i]]); } +export function capitalizeFirstLetter(val) { + return String(val).charAt(0).toUpperCase() + String(val).slice(1); +} +