From 574651ccb7d2c231b2f33ea0d49126cfb5b08b25 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Mon, 17 Mar 2025 17:54:42 +0100 Subject: [PATCH] Add product and sum types --- function_registry.js | 15 ------- interfaces/serializable.js | 27 +++++++++++ lib/id.js | 2 +- lib/literals.js | 30 ------------- lib/square.js | 2 +- lib/values.js | 33 ++++++++++++++ main.js | 92 +++----------------------------------- metacircular.js | 2 +- primitives/bool.js | 2 +- primitives/double.js | 2 +- primitives/int.js | 2 +- structures/list.js | 3 +- structures/list_common.js | 17 +------ structures/module.js | 3 +- structures/product.js | 29 ++++++++++++ structures/sum.js | 41 +++++++++++++++++ type_registry.js | 62 +++++++++++++++++++++++++ typed.js | 2 +- 18 files changed, 209 insertions(+), 157 deletions(-) delete mode 100644 function_registry.js create mode 100644 interfaces/serializable.js delete mode 100644 lib/literals.js create mode 100644 lib/values.js create mode 100644 structures/product.js create mode 100644 structures/sum.js create mode 100644 type_registry.js diff --git a/function_registry.js b/function_registry.js deleted file mode 100644 index b23d786..0000000 --- a/function_registry.js +++ /dev/null @@ -1,15 +0,0 @@ -import { DefaultMap } from "./util.js"; - -const mapping = new DefaultMap(() => new Map()); - -export const fnType = ({in: inType, out: outType}) => { - const m2 = mapping.getdefault(inType); - if (m2.has(outType)) { - return m2.get(outType); - } - else { - const fnType = {in: inType, out: outType}; - m2.set(outType, fnType); - return fnType; - } -} \ No newline at end of file diff --git a/interfaces/serializable.js b/interfaces/serializable.js new file mode 100644 index 0000000..391c1d3 --- /dev/null +++ b/interfaces/serializable.js @@ -0,0 +1,27 @@ +import { fnType } from "../type_registry.js"; +import {Type, Function} from "../metacircular.js"; + +import {Bool, Int} from "./symbols.js"; + +export const addInt = x => y => x + y; +export const mulInt = x => y => x * y; +export const eqInt = x => y => x === y; + +const Int_to_Int = fnType({in: Int, out: Int }); +const Int_to_Bool = fnType({in: Int, out: Bool}); +export const Int_to_Int_to_Int = fnType({in: Int, out: Int_to_Int}); +export const Int_to_Int_to_Bool = fnType({in: Int, out: Int_to_Bool}); + + +export const ModuleInt = [ + {i: Int , t: Type }, + + {i: Int_to_Int_to_Int , t: Function }, + {i: Int_to_Int_to_Bool , t: Function }, + {i: Int_to_Int , t: Function }, + {i: Int_to_Bool , t: Function }, + + {i: addInt , t: Int_to_Int_to_Int }, + {i: mulInt , t: Int_to_Int_to_Int }, + {i: eqInt , t: Int_to_Int_to_Bool }, +]; diff --git a/lib/id.js b/lib/id.js index 4fedbe8..ba19d5a 100644 --- a/lib/id.js +++ b/lib/id.js @@ -1,4 +1,4 @@ -import { fnType } from "../function_registry.js"; +import { fnType } from "../type_registry.js"; import {Function} from "../metacircular.js"; // import {Typed} from "../typed.js"; diff --git a/lib/literals.js b/lib/literals.js deleted file mode 100644 index c972ba9..0000000 --- a/lib/literals.js +++ /dev/null @@ -1,30 +0,0 @@ -import {Int, Bool, Double} from "../primitives/symbols.js"; -import { getListType, makeListModule } from "../structures/list_common.js"; - -const ListOfBool = getListType(Bool); -const ListOfBoolModule = makeListModule(Bool); - -const ListOfInt = getListType(Int); -const ListOfIntModule = makeListModule(Int); - -const ListOfListOfInt = getListType(ListOfInt); -const ListOfListOfIntModule = makeListModule(ListOfInt); - -export const ModuleLiterals = [ - {i: 0n, t: Int}, - {i: 42n, t: Int}, - {i: false, t: Bool}, - {i: 3.14159265359, t: Double}, - - {i: {l:[42n, 43n]}, t: ListOfInt}, - - // {i: [[42n, 43n]], t: ListOfListOfInt}, - - // i'm lazy - ...ListOfIntModule, - - - // ...ListOfBoolModule, - - // ...ListOfListOfIntModule, -]; diff --git a/lib/square.js b/lib/square.js index 6328aa2..7510a39 100644 --- a/lib/square.js +++ b/lib/square.js @@ -2,7 +2,7 @@ import {Function, getIn, getOut} from "../metacircular.js"; import {Module} from "../structures/module.js"; import { deepEqual } from "../util.js"; import { Typed } from "../typed.js"; -import { fnType } from "../function_registry.js"; +import { fnType } from "../type_registry.js"; // import {Num, NumDict, getType, getMul} from "../typeclasses/num.js"; diff --git a/lib/values.js b/lib/values.js new file mode 100644 index 0000000..d548dff --- /dev/null +++ b/lib/values.js @@ -0,0 +1,33 @@ +import {Int, Bool, Double} from "../primitives/symbols.js"; +import { makeListModule } from "../structures/list_common.js"; +import { makeProductType } from "../structures/product.js"; +import { makeSumType } from "../structures/sum.js"; +import { getListType, prodType, sumType } from "../type_registry.js"; + +const ListOfInt = getListType(Int); +const ListOfIntModule = makeListModule(Int); + +const ListOfListOfInt = getListType(ListOfInt); +const ListOfListOfIntModule = makeListModule(ListOfInt); + +export const ModuleValues = [ + {i: 0n, t: Int}, + {i: 42n, t: Int}, + {i: false, t: Bool}, + {i: 3.14159265359, t: Double}, + + {i: {l:[42n, 43n]} , t: ListOfInt}, + {i: {l:[{l:[42n, 43n]}, {l:[999n]}]}, t: ListOfListOfInt}, + + // i'm lazy + ...ListOfIntModule, + ...ListOfListOfIntModule, + + + ...makeProductType(Int, Bool), + + ...makeSumType(Int, Bool), + + {i: {left: 42n, right: true}, t: prodType(Int, Bool)}, + {i: {variant: "L", value: 100n}, t: sumType(Int, Bool)}, +]; diff --git a/main.js b/main.js index 9e9c545..e31dcc4 100644 --- a/main.js +++ b/main.js @@ -6,10 +6,11 @@ import {Int_to_Int_to_Int, ModuleInt, mulInt} from "./primitives/int.js"; import {Int} from "./primitives/symbols.js"; import { makeSquare } from "./lib/square.js"; import {makeIdFn} from "./lib/id.js"; -import {ModuleLiterals} from "./lib/literals.js"; +import {ModuleValues} from "./lib/values.js"; import {DefaultMap} from "./util.js"; import { ModuleModule } from "./structures/module.js"; +import { ModuleList } from "./structures/list.js"; class Context { constructor(mod) { @@ -24,46 +25,10 @@ class Context { this.addInstance({i, t}); } - // console.log(this.instances.m); - - - // const callEveryFunction = ({i,t}, indent=0) => { - // for (const fntype of this.functionsFrom.getdefault(t)) { - // for (const fn of this.instances.getdefault(fntype)) { - // console.log(" ", fn, ':', fntype); - // const result = fn(i); - // const resultType = getOut(fntype); - // console.log(" ".repeat(indent), i, '->', result); - // if (this.types.getdefault(resultType).includes(Function)) { - // if (indent < 2) { - // callEveryFunction({i: result, t: resultType}, indent+1) - // } - // } - // } - // } - // }; - - // const callFunction = (typ, fns) => { - // console.log("Functions callable on", typ, ":"); - // for (const fn of fns) { - // console.log(" ", fn); - // for (const FN of this.instances.getdefault(fn)) { - // console.log(" ", FN); - // for (const i of this.instances.getdefault(typ)) { - // const result = FN(i); - // const resultType = getOut(fn); - // console.log(" ", i, '->', result); - // if (this.types.getdefault(resultType).includes(Function)) { - // callFunction(resultType, ) - // } - // } - // } - // } - // }; - const callFunctionOnEveryValue = (fn, fnType, indent=1) => { const inType = getIn(fnType); const outType = getOut(fnType); + console.log(); console.log("--".repeat(indent), fn, ':', fnType); for (const i of this.instances.getdefault(inType)) { const result = fn(i); @@ -83,54 +48,6 @@ class Context { } } - // for (const [i, ts] of this.types.m.entries()) { - // for (const t of ts) { - // callEveryFunction({i,t}); - // } - // } - - - // // Conformance check - // for (const fn of this.instances.getdefault(conformanceCheck)) { - // const t = fn.inType; - // for (const i of this.instances.getdefault(t)) { - // if (!fn.fn(i)) { - // console.warn(i, 'is not conform', 'to', t); - // } - // else { - // console.info('👍', i, 'conforms to', t); - // } - // } - // } - - // const MAXDEPTH = 0; - - // let calls = 0; - // const tryEveryFunction = ({i, t}, maxDepth) => { - // calls++; - // for (const fn of this.functionsFrom.getdefault(t)) { - // const outType = getOut(fn); - // // console.log(" ".repeat(MAXDEPTH-maxDepth) + 'calling', fn, 'on', i); - // const result = fn(i); - // console.log(" ".repeat(MAXDEPTH-maxDepth), result, 'should be of type', outType); - // if (outType === TypeLink && result.t === undefined) { - // console.warn(" ".repeat(MAXDEPTH-maxDepth) + ' ^ invalid') - // } - // if (outType === Int && (typeof result) !== "number") { - // console.warn(" ".repeat(MAXDEPTH-maxDepth) + ' ^ invalid') - // } - // this.addInstance({i: result, t: outType}); - // if (maxDepth > 0) { - // tryEveryFunction({i: result, t: outType}, maxDepth-1); - // } - // } - // } - - // for (const {i, t} of mod) { - // tryEveryFunction({i, t}, MAXDEPTH); - // } - - // console.log("calls:", calls); } addInstance({i, t}) { @@ -165,6 +82,7 @@ const ctx = new Context([ ...makeIdFn(Int), ...makeSquare({i: mulInt, t: Int_to_Int_to_Int}), ...makeSquare({i: mulDouble, t: Double_to_Double_to_Double}), - ...ModuleLiterals, + ...ModuleList, + ...ModuleValues, ...ModuleModule, ]); diff --git a/metacircular.js b/metacircular.js index e17059a..3b43d8a 100644 --- a/metacircular.js +++ b/metacircular.js @@ -1,4 +1,4 @@ -import { fnType } from "./function_registry.js"; +import { fnType } from "./type_registry.js"; export const Type = Symbol('Type'); export const Function = Symbol('Function'); diff --git a/primitives/bool.js b/primitives/bool.js index e6190a1..b5cdca9 100644 --- a/primitives/bool.js +++ b/primitives/bool.js @@ -1,4 +1,4 @@ -import { fnType } from "../function_registry.js"; +import { fnType } from "../type_registry.js"; import {Type, Function} from "../metacircular.js"; import {Bool} from "./symbols.js"; diff --git a/primitives/double.js b/primitives/double.js index 2209dda..defecdb 100644 --- a/primitives/double.js +++ b/primitives/double.js @@ -1,4 +1,4 @@ -import { fnType } from "../function_registry.js"; +import { fnType } from "../type_registry.js"; import {Type, Function} from "../metacircular.js"; import {Bool, Double} from "./symbols.js"; diff --git a/primitives/int.js b/primitives/int.js index e566384..391c1d3 100644 --- a/primitives/int.js +++ b/primitives/int.js @@ -1,4 +1,4 @@ -import { fnType } from "../function_registry.js"; +import { fnType } from "../type_registry.js"; import {Type, Function} from "../metacircular.js"; import {Bool, Int} from "./symbols.js"; diff --git a/structures/list.js b/structures/list.js index 45f8ef7..b2f528a 100644 --- a/structures/list.js +++ b/structures/list.js @@ -1,6 +1,7 @@ -import { fnType } from "../function_registry.js"; +import { fnType, getListType } from "../type_registry.js"; import {Type, Function} from "../metacircular.js"; import { makeListModule } from "./list_common.js"; +import { Module } from "./module.js"; const Type_to_Type = fnType({in: Type, out: Type}); const Type_to_Module = fnType({in: Type, out: Module}); diff --git a/structures/list_common.js b/structures/list_common.js index cc713f3..3a90ebe 100644 --- a/structures/list_common.js +++ b/structures/list_common.js @@ -1,22 +1,7 @@ -import { fnType } from "../function_registry.js"; +import { fnType, getListType } from "../type_registry.js"; import {Type, Function} from "../metacircular.js"; import {Int} from "../primitives/symbols.js"; -// Global store of list types: -const listTypes = new Map(); -export function getListType(elementType) { - if (listTypes.has(elementType)) { - // only generate each list type once - // this would not be necessary if we could define our own equality and hash functions on objects in JavaScript. - return listTypes.get(elementType); - } - else { - const type = Symbol('ListOf:'+elementType.toString()); - listTypes.set(elementType, type); - return type; - } -} - export const makeListModule = elementType => { // List type depends on elementType // generating it another time, will give the same type (structurally equivalent): diff --git a/structures/module.js b/structures/module.js index a36159f..5f1e6ac 100644 --- a/structures/module.js +++ b/structures/module.js @@ -1,5 +1,6 @@ -import { getListType, makeListModule } from "./list_common.js"; +import { makeListModule } from "./list_common.js"; import { Typed } from "../typed.js"; +import { getListType } from "../type_registry.js"; export const Module = getListType(Typed); // a Module is a list of Typeds diff --git a/structures/product.js b/structures/product.js new file mode 100644 index 0000000..3d485fc --- /dev/null +++ b/structures/product.js @@ -0,0 +1,29 @@ +import { fnType, prodType } from "../type_registry.js"; +import { Function } from "../metacircular.js"; + +// In JS, all products are encoded in the same way: +const constructor = left => right => ({left, right}); +const left = product => product.left; +const right = product => product.right; + +// Given two types A and B, create the type (A × B). +export const makeProductType = (leftType, rightType) => { + const productType = prodType(leftType, rightType); + + const leftFnType = fnType({in: productType, out: leftType}); + const rightFnType = fnType({in: productType, out: rightType}); + + const constructorBoundType = fnType({in: rightType, out: productType}); + const constructorType = fnType({in: leftType, out: constructorBoundType}); + + return [ + {i: left , t: leftFnType}, + {i: right , t: rightFnType}, + {i: leftFnType , t: Function}, + {i: rightFnType, t: Function}, + + {i: constructor , t: constructorType}, + {i: constructorType , t: Function}, + {i: constructorBoundType, t: Function}, + ]; +}; diff --git a/structures/sum.js b/structures/sum.js new file mode 100644 index 0000000..85bbf27 --- /dev/null +++ b/structures/sum.js @@ -0,0 +1,41 @@ +import { fnType, prodType, sumType } from "../type_registry.js"; +import { Function, Type } from "../metacircular.js"; +import { Module } from "./module.js"; + +const constructorLeft = left => ({variant: "L", value: left }); +const constructorRight = right => ({variant: "R", value: right}); + +const match = sum => handlers => sum.variant === "L" + ? handlers.left(sum.value) + : handlers.right(sum.value); + +// Given two types A and B, create the type (A + B). +export const makeSumType = (leftType, rightType) => { + const sType = sumType(leftType, rightType); + + const constructorLeftType = fnType({in: leftType , out: sType}); + const constructorRightType = fnType({in: rightType, out: sType}); + + // For each possible return type, the match function has a different signature. + // We thus define a function that creates a properly typed match function. + const makeMatchFn = returnType => { + const handlersType = prodType( + fnType({in: leftType , out: returnType}), // handler function for left variant + fnType({in: rightType, out: returnType}), // handler function for right variant + ); + const matchFnType = fnType({in: sType, out: fnType({in: handlersType, out: returnType})}); + return [ + {i: match , t: matchFnType}, + {i: matchFnType, t: Function}, + ]; + }; + + return [ + {i: constructorLeft , t: constructorLeftType}, + {i: constructorRight , t: constructorRightType}, + {i: constructorLeftType , t: Function}, + {i: constructorRightType, t: Function}, + + {i: makeMatchFn, t: fnType({in: Type, out: Module})}, + ]; +}; diff --git a/type_registry.js b/type_registry.js new file mode 100644 index 0000000..dbc0ec8 --- /dev/null +++ b/type_registry.js @@ -0,0 +1,62 @@ +// This module ensures that we never accidentally create a Symbol for the same type twice. +// Maybe we shouldn't use Symbols for types, but we also cannot use primitive values for types because they may accidentally overlap with 'real' values (that are not types) +// We also cannot use objects for types because similar to Symbols, objects are only equal to themselves if we use them as Map keys. + +import { DefaultMap } from "./util.js"; + +// Global store of function types: +const fnTypes = new DefaultMap(() => new Map()); +export const fnType = ({in: inType, out: outType}) => { + const m2 = fnTypes.getdefault(inType, true); + if (m2.has(outType)) { + return m2.get(outType); + } + else { + const fnType = {in: inType, out: outType}; + m2.set(outType, fnType); + return fnType; + } +}; + +// Global store of list types: +const listTypes = new Map(); +export function getListType(elementType) { + if (listTypes.has(elementType)) { + // only generate each list type once + // this would not be necessary if we could define our own equality and hash functions on objects in JavaScript. + return listTypes.get(elementType); + } + else { + const type = Symbol('ListOf:' + elementType.toString()); + listTypes.set(elementType, type); + return type; + } +} + +// Global store of product types: +const productTypes = new DefaultMap(() => new Map()); +export const prodType = (leftType, rightType) => { + const m2 = productTypes.getdefault(leftType, true); + if (m2.has(rightType)) { + return m2.get(rightType); + } + else { + const pt = Symbol(`${leftType.toString()} × ${rightType.toString()}`); + m2.set(rightType, pt); + return pt; + } +} + +// Global store of sum types: +const sumTypes = new DefaultMap(() => new Map()); +export const sumType = (leftType, rightType) => { + const m2 = sumTypes.getdefault(leftType, true); + if (m2.has(rightType)) { + return m2.get(rightType); + } + else { + const st = Symbol(`${leftType.toString()} + ${rightType.toString()}`); + m2.set(rightType, st); + return st; + } +} diff --git a/typed.js b/typed.js index 6ea87a3..d4b936e 100644 --- a/typed.js +++ b/typed.js @@ -1,4 +1,4 @@ -import { fnType } from "./function_registry.js"; +import { fnType } from "./type_registry.js"; import {Type, Function} from "./metacircular.js"; export const Typed = Symbol('Typed');