From 29d20b227388bbfde2c76c4ded1422f0909d336d Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Sun, 23 Mar 2025 09:15:37 +0100 Subject: [PATCH] wip --- generics/generics.js | 240 ++++++++------------------------ generics/generics.test.js | 17 +-- generics/types.js | 2 - lib/id.js | 64 ++++----- lib/square.js | 48 +++---- metacircular.js | 64 +++------ primitives/bool.js | 6 +- primitives/byte.js | 4 +- primitives/char.js | 4 +- primitives/double.js | 5 +- primitives/int.js | 4 +- primitives/symbols.js | 12 +- progress.txt | 9 +- structures/function.js | 51 +++++++ structures/list.js | 57 ++++++-- structures/list_common.js | 75 ---------- structures/list_types/module.js | 5 +- structures/list_types/string.js | 5 +- structures/product.js | 62 +++++---- structures/sum.js | 77 +++++----- typeclasses/eq.js | 3 +- typeclasses/num.js | 3 +- typeclasses/num.test.js | 2 +- typed.js | 14 +- util.js | 5 + 25 files changed, 369 insertions(+), 469 deletions(-) delete mode 100644 generics/types.js create mode 100644 structures/function.js delete mode 100644 structures/list_common.js diff --git a/generics/generics.js b/generics/generics.js index 7f73206..f2ffb8c 100644 --- a/generics/generics.js +++ b/generics/generics.js @@ -1,16 +1,5 @@ -import { lsType } from "../structures/list_common.js"; -import { fnType } from "../metacircular.js"; -import { deepEqual, DefaultMap, pretty } from "../util.js"; -import { numDictType } from "../typeclasses/num_type.js"; - -const genericTypeRegistry = new DefaultMap(underlyingType => ({ generic: underlyingType })); - -// type constructor for generic kinds, -// for instance: -// a -> a -> Bool -// is typed by -// genericType(Function) -export const genericType = underlyingType => genericTypeRegistry.getdefault(underlyingType, true); +import { eqType } from "../metacircular.js"; +import { pretty, zip } from "../util.js"; // constructor for generic types // for instance, the type: @@ -19,14 +8,13 @@ export const genericType = underlyingType => genericTypeRegistry.getdefault(unde // makeGeneric(a => fnType({in: a, out: fnType({in: a, out: Bool})})) export const makeGeneric = callback => { // type variables to make available: - const a = Symbol('a'); - const b = Symbol('b'); - const c = Symbol('c'); - const d = Symbol('d'); - const e = Symbol('e'); - const type = callback(a, b, c, d, e); + const typeVars = ['a', 'b', 'c', 'd', 'e'].map(letter => ({ + symbol: Symbol(letter), + params: [], + })); + const type = callback(...typeVars); return { - typeVars: occurring(type, new Set([a, b, c, d, e])), + typeVars: occurring(type, new Set(typeVars)), type, }; }; @@ -34,60 +22,10 @@ export const makeGeneric = callback => { // From the given set of type variables, return only those that occur in the given type. export const occurring = (type, typeVars) => { if (typeVars.has(type)) { + // type IS a type variable: return new Set([type]); } - if (type.in !== undefined) { - // function type - return new Set([ - ...occurring(type.in, typeVars), - ...occurring(type.out, typeVars)]); - } - if (type.listOf !== undefined) { - return occurring(type.listOf, typeVars); - } - return new Set(); -} - -export const properUnify = eqDict => ( - {typeVars: formalTypeVars, type: formalType}, - {typeVars: actualTypeVars, type: actualType}, -) => { - if (getEq(eqDict)(formalType)(actualType)) { - return { - substitutions: new Map(), - typeVars: new Set([ - ...actualTypeVars, - // ...formalTypeVars, // <- i don't think we need these? - ]), - type: actualType, - } - } - - if (formalTypeVars.has(formalType)) { - // formalType is type variable -> substitute it by actualType - return { - substitutions: new Map([[formalType, actualType]]), - typeVars: new Set([ - ...actualTypeVars, - ...formalTypeVars, - ].filter(a => a !== formalType)), - type: actualType, - } - } - if (actualTypeVars.has(actualType)) { - // same as above, but in opposite direction: - // actualType is type variable -> substitute it by formalType - return { - substitutions: new Map([[actualType, formalType]]), - typeVars: new Set([ - ...actualTypeVars, - ...formalTypeVars, - ].filter(a => a !== actualType)), - type: formalType, - } - } - - // WIP... + return new Set(type.params.flatMap(p => [...occurring(p, typeVars)])); } const mergeOneWay = (m1, m2) => { @@ -103,7 +41,17 @@ const mergeOneWay = (m1, m2) => { return [true, m1copy, m2copy, new Set()]; // stable } -export const mergeSubstitutions = (m1, m2) => { +export const mergeTwoWay = (m1, m2) => { + // check for conflicts: + for (const [typeVar, actual] of m1) { + if (m2.has(typeVar)) { + const other = m2.get(typeVar); + if (!eqType(actual, other)) { + throw new Error(`conflicting substitution: ${pretty(actual)}vs. ${pretty(other)}`); + } + } + } + // actually merge let stable = false; let deletedTypeVars = new Set(); while (!stable) { @@ -115,150 +63,80 @@ export const mergeSubstitutions = (m1, m2) => { return [new Map([...m1, ...m2]), deletedTypeVars]; } -// Currently very ad-hoc. - // Thanks to Hans for pointing out that this algorithm exactly like "Unification" in Prolog (hence the function name): // https://www.dai.ed.ac.uk/groups/ssp/bookpages/quickprolog/node12.html export const unify = ( {typeVars: formalTypeVars, type: formalType}, {typeVars: actualTypeVars, type: actualType}, ) => { - if (deepEqual(formalType, actualType)) { - return { - substitutions: new Map(), - typeVars: new Set([ - ...actualTypeVars, - ...formalTypeVars]), - type: actualType, - } - } - + // console.log("unify", {formalTypeVars, formalType, actualTypeVars, actualType}); if (formalTypeVars.has(formalType)) { - // simplest case: substitute formal type param + // simplest case: formalType is a type paramater + // => substitute with actualType return { substitutions: new Map([[formalType, actualType]]), typeVars: new Set([ ...actualTypeVars, - ...formalTypeVars].filter(a => a !== formalType)), + ...[...formalTypeVars].filter(a => a !== formalType), + ]), type: actualType, }; } - if (actualTypeVars.has(actualType)) { // same as above, but in the other direction return { substitutions: new Map([[actualType, formalType]]), typeVars: new Set([ - ...actualTypeVars, - ...formalTypeVars].filter(a => a !== actualType)), + ...[...actualTypeVars].filter(a => a !== actualType), + ...formalTypeVars, + ]), type: formalType, }; } - - if (formalType.in !== undefined) { - // function type - if (actualType.in === undefined) { - throw new Error(`cannot assign ${pretty(actualType)} to ${pretty(formalType)}`); - } - else { - // both are function type - const inType = unify({typeVars: formalTypeVars, type: formalType.in}, {typeVars: actualTypeVars, type: actualType.in}); - const outType = unify({typeVars: formalTypeVars, type: formalType.out}, {typeVars: actualTypeVars, type: actualType.out}); - // check for conflicts between 'in' and 'out' subsitutions - for (const [typeVar, actual] of inType.substitutions) { - if (outType.substitutions.has(typeVar)) { - if (!deepEqual(actual, outType.substitutions.get(typeVar))) { - throw new Error(`conflicting assignment for ${pretty(typeVar)}: ${pretty(a)}`); - } - } - } - // merge substitutions - const [newSubstitutions, deletedTypeVars] = mergeSubstitutions( - inType.substitutions, outType.substitutions); - // const newSubstitutions = new Map([ - // ...inType.substitutions, - // ...outType.substitutions, - // ]); - const newTypeVars = new Set([ - ...actualTypeVars, - ...formalTypeVars].filter(a => !newSubstitutions.has(a) && !deletedTypeVars.has(a))); - return { - substitutions: newSubstitutions, - typeVars: newTypeVars, - type: fnType({in: inType.type, out: outType.type}), - }; - } + // recursively unify + if (formalType.symbol !== actualType.symbol) { + throw new Error(`cannot unify ${pretty(formalType.symbol)} and ${pretty(actualType.symbol)}`); } - - if (formalType.listOf !== undefined) { - // list type - if (actualType.listOf === undefined) { - throw new Error(`cannot assign ${pretty(actualType)} to ${pretty(formalType)}`); - } - else { - // both are list type - const elementType = unify( - {typeVars: formalTypeVars, type: formalType.listOf}, - {typeVars: actualTypeVars, type: actualType.listOf}); - return { - substitutions: elementType.substitutions, - typeVars: new Set([ - ...actualTypeVars, - ...formalTypeVars].filter(a => !elementType.substitutions.has(a))), - type: lsType(elementType.type), - }; - } + else { + const unifiedParams = zip(formalType.params, actualType.params).map(([formalParam, actualParam]) => unify({typeVars: formalTypeVars, type: formalParam}, {typeVars: actualTypeVars, type: actualParam})); + const [unifiedSubstitusions, deleted] = unifiedParams.reduce(([substitutionsSoFar, deletedSoFar], cur) => { + // console.log('merging', substitutionsSoFar, cur.substitutions); + const [newSubstitutions, deleted] = mergeTwoWay(substitutionsSoFar, cur.substitutions); + return [newSubstitutions, deletedSoFar.union(deleted)]; + }, [new Map(), new Set()]); + const unifiedTypeVars = new Set([ + ...actualTypeVars, + ...formalTypeVars, + ].filter(a => !unifiedSubstitusions.has(a) && !deleted.has(a))); + return { + substitutions: unifiedSubstitusions, + typeVars: unifiedTypeVars, + type: { + symbol: formalType.symbol, + params: unifiedParams.map(p => p.type), + }, + }; } - - if (formalType.numDict !== undefined) { - if (actualType.numDict === undefined) { - throw new Error(`cannot assign ${pretty(actualType)} to ${pretty(formalType)}`); - } - else { - // both are NumDict type - const underlyingType = unify( - {typeVars: formalTypeVars, type: formalType.numDict}, - {typeVars: actualTypeVars, type: actualType.numDict}); - return { - substitutions: underlyingType.substitutions, - typeVars: new Set([ - ...actualTypeVars, - ...formalTypeVars].filter(a => !underlyingType.substitutions.has(a))), - type: numDictType(underlyingType.type), - }; - } - } - throw new Error("i don't know what to do :("); }; -// export const matchConcrete = ({typeVars, type: formalType}, actualType) => { -// return unify({typeVars, type: formalType}, {typeVars: new Set(), type: actualType}); -// }; - export const substitute = (type, substitutions) => { if (substitutions.has(type)) { + // type IS a type var to be substituted: return substitutions.get(type); } - if (type.listOf !== undefined) { - // list type - return lsType(substitute(type.listOf, substitutions)); - } - if (type.in !== undefined) { - // function type - return fnType({ - in: substitute(type.in, substitutions), - out: substitute(type.out, substitutions), - }) - } - return type; + return { + symbol: type.symbol, + params: type.params.map(p => substitute(p, substitutions)), + }; } export const assign = (genFnType, paramType) => { + const [inType, outType] = genFnType.type.params; const matchedInType = unify({ typeVars: genFnType.typeVars, - type: genFnType.type.in, + type: inType, }, paramType); - const substitutedOutType = substitute(genFnType.type.out, matchedInType.substitutions); + const substitutedOutType = substitute(outType, matchedInType.substitutions); return { typeVars: matchedInType.typeVars, type: substitutedOutType, diff --git a/generics/generics.test.js b/generics/generics.test.js index 86df9c8..40b4532 100644 --- a/generics/generics.test.js +++ b/generics/generics.test.js @@ -1,21 +1,22 @@ import { Bool, Int } from "../primitives/symbols.js"; -import { lsType } from "../structures/list_common.js"; -import { fnType } from "../metacircular.js"; -import { assign, makeGeneric, mergeSubstitutions, unify } from "./generics.js"; +import { lsType } from "../structures/list.js"; +import { fnType } from "../structures/function.js"; +import { assign, makeGeneric, unify } from "./generics.js"; +import { pretty } from "../util.js"; // a -> Int const a_to_Int = makeGeneric(a => fnType({in: a, out: Int})); // Bool -> Int const Bool_to_Int = makeGeneric(() => fnType({in: lsType(Bool), out: Int})); -console.log("should be: Bool -> Int") -console.log(unify(a_to_Int, Bool_to_Int)); +console.log("should be: [Bool] -> Int") +console.log(pretty(unify(a_to_Int, Bool_to_Int))); // (a -> a) -> b const fnType2 = makeGeneric((a,b) => fnType({in: fnType({in: a, out: a}), out: b})); // (Bool -> Bool) -> a const fnType3 = makeGeneric(a => fnType({in: fnType({in: Bool, out: Bool}), out: a})); console.log("should be: (Bool -> Bool) -> a"); -console.log(unify(fnType2, fnType3)); +console.log(pretty(unify(fnType2, fnType3))); // (a -> b) -> [a] -> [b] const mapFnType = makeGeneric((a,b) => @@ -27,7 +28,7 @@ const mapFnType = makeGeneric((a,b) => const idFnType = makeGeneric(a => fnType({in: a, out: a})); console.log("should be: [a] -> [a]"); -console.log(assign(mapFnType, idFnType)); +console.log(pretty(assign(mapFnType, idFnType))); // (a -> Int) -> [a] -> a const weirdFnType = makeGeneric(a => @@ -36,4 +37,4 @@ const weirdFnType = makeGeneric(a => out: fnType({in: lsType(a), out: a}), })); console.log("should be: [Int] -> Int"); -console.log(assign(weirdFnType, idFnType)); \ No newline at end of file +console.log(pretty(assign(weirdFnType, idFnType))); \ No newline at end of file diff --git a/generics/types.js b/generics/types.js deleted file mode 100644 index 139597f..0000000 --- a/generics/types.js +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/lib/id.js b/lib/id.js index 6c27cf0..bc92227 100644 --- a/lib/id.js +++ b/lib/id.js @@ -1,39 +1,39 @@ -import { fnType } from "../metacircular.js"; -import {Function} from "../metacircular.js"; +// import { fnType } from "../metacircular.js"; +// import {Function} from "../metacircular.js"; -// import {Typed} from "../typed.js"; +// // import {Typed} from "../typed.js"; -// import {Bool} from "../primitives/symbols.js"; -// import {deepEqual} from "../util.js"; -// import {Conformable, conformanceCheck} from "../typeclasses/conformable.js"; +// // import {Bool} from "../primitives/symbols.js"; +// // import {deepEqual} from "../util.js"; +// // import {Conformable, conformanceCheck} from "../typeclasses/conformable.js"; -// // Identity function -// const idBoundToType = Symbol('idBoundToType'); +// // // Identity function +// // const idBoundToType = Symbol('idBoundToType'); -// const id = type => x => x; -// const idFn = {name: "id", inType: Type, outType: {inType: Type, outType: Type}, fn: id}; -// const idIsConform = { -// name: "isConform", -// inType: idBoundToType, -// outType: Bool, -// fn: fn => deepEqual(fnIn(fn), fnOut(fn)) -// } +// // const id = type => x => x; +// // const idFn = {name: "id", inType: Type, outType: {inType: Type, outType: Type}, fn: id}; +// // const idIsConform = { +// // name: "isConform", +// // inType: idBoundToType, +// // outType: Bool, +// // fn: fn => deepEqual(fnIn(fn), fnOut(fn)) +// // } -// export const ModuleId = [ -// {i: idBoundToType, t: Type}, -// {i: idFn, t: Function}, -// ... makeTypedFnInOut({f: idBoundToType, inType: Type, outType: Type}), +// // export const ModuleId = [ +// // {i: idBoundToType, t: Type}, +// // {i: idFn, t: Function}, +// // ... makeTypedFnInOut({f: idBoundToType, inType: Type, outType: Type}), -// {i: idBoundToType, t: Conformable}, -// {i: idIsConform, t: conformanceCheck}, -// ]; +// // {i: idBoundToType, t: Conformable}, +// // {i: idIsConform, t: conformanceCheck}, +// // ]; -// generates explicitly typed id-function -export const makeIdFn = typ => { - const Typ_to_Typ = fnType({in: typ, out: typ}); - const id = x => x; - return {l:[ - {i: id , t: Typ_to_Typ}, - {i: Typ_to_Typ, t: Function}, - ]}; -}; +// // generates explicitly typed id-function +// export const makeIdFn = typ => { +// const Typ_to_Typ = fnType({in: typ, out: typ}); +// const id = x => x; +// return {l:[ +// {i: id , t: Typ_to_Typ}, +// {i: Typ_to_Typ, t: Function}, +// ]}; +// }; diff --git a/lib/square.js b/lib/square.js index 353c9a3..8720952 100644 --- a/lib/square.js +++ b/lib/square.js @@ -1,8 +1,8 @@ -import {Function, getIn, getOut} from "../metacircular.js"; -import {Module} from "../structures/list_types/module.js"; -import { deepEqual } from "../util.js"; -import { Typed } from "../typed.js"; -import { fnType } from "../metacircular.js"; +// import { getIn, getOut } from "../metacircular.js"; +// import {Module} from "../structures/list_types/module.js"; +// import { deepEqual } from "../util.js"; +// import { Typed } from "../typed.js"; +// import { fnType } from "../metacircular.js"; // import {Num, NumDict, getType, getMul} from "../typeclasses/num.js"; @@ -27,24 +27,24 @@ import { fnType } from "../metacircular.js"; // ]; -export const makeSquare = ({i: mul, t: mulFunction}) => { - const numType = getIn(mulFunction); - const boundMulFunction = getOut(mulFunction); - if (!deepEqual(getOut(boundMulFunction), numType) || !deepEqual(getIn(boundMulFunction), numType)) { - console.log(getOut(boundMulFunction), getIn(boundMulFunction), numType); - throw new Error("invalid signature"); - } - const square = x => mul(x)(x); - const squareFunction = fnType({in: numType, out: numType}); - return {l:[ - {i: square , t: squareFunction}, - {i: squareFunction, t: Function}, - ]}; -}; +// export const makeSquare = ({i: mul, t: mulFunction}) => { +// const numType = getIn(mulFunction); +// const boundMulFunction = getOut(mulFunction); +// if (!deepEqual(getOut(boundMulFunction), numType) || !deepEqual(getIn(boundMulFunction), numType)) { +// console.log(getOut(boundMulFunction), getIn(boundMulFunction), numType); +// throw new Error("invalid signature"); +// } +// const square = x => mul(x)(x); +// const squareFunction = fnType({in: numType, out: numType}); +// return {l:[ +// {i: square , t: squareFunction}, +// {i: squareFunction, t: Function}, +// ]}; +// }; -const makeSquareType = fnType({in: Typed, out: Module}); +// const makeSquareType = fnType({in: Typed, out: Module}); -export const ModuleSquare = {l:[ - {i: makeSquare , t: makeSquareType}, - {i: makeSquareType, t: Function}, -]}; +// export const ModuleSquare = {l:[ +// {i: makeSquare , t: makeSquareType}, +// {i: makeSquareType, t: Function}, +// ]}; diff --git a/metacircular.js b/metacircular.js index 5601f13..0779d44 100644 --- a/metacircular.js +++ b/metacircular.js @@ -1,20 +1,16 @@ -import { DefaultMap } from "./util.js"; +import { Bool } from "./primitives/symbols.js"; +import { typedFnType } from "./structures/function.js"; +import { deepEqual } from "./util.js"; -// The registry ensures that we never accidentally create more than one JS object for the same function type. -// It is a cheap workaround for JS lacking customizable hash-functions and equality-testing-functions. -// This same pattern is repeated throughout the code for all non-nullary type constructors (list, sum, product, ...) -const fnTypeRegistry = new DefaultMap(inType => new DefaultMap(outType => ({ in: inType, out: outType }))); +// TODO: 'Type' (and its instances) are itself instances of (String,[Type]) (=the product type of String and list of Type) +// so is 'Type' just an alias for (String, [Type]) +export const Type = { symbol: Symbol('Type'), params: [] }; -// type constructor for function types -export const fnType = ({ in: inType, out: outType }) => fnTypeRegistry.getdefault(inType, true).getdefault(outType, true); +export const getSymbol = type => type.symbol; +export const getParams = type => type.params; -export const Type = Symbol('Type'); -export const Function = Symbol('Function'); - -// Implementation of 'in' and 'out' functions, -// to get input/output type of a function signature: -export const getIn = fn => fn.in; -export const getOut = fn => fn.out; +// we can test whether types are equal: +export const eqType = deepEqual; // a module is just a set of typed objects // each 'typed object' is implicitly an instance of TypeLink (defined below) @@ -28,38 +24,14 @@ export const ModuleMetaCircular = {l:[ // Type : Type {i: Type, t: Type}, - // Function : Type - {i: Function, t: Type}, + // ...typedFnType(getSymbol, fnType => fnType({in: Type, out: Int})), - // (Function -> Type) : Function - {i: fnType({in: Function, out: Type}), t: Function}, + // ...typedFnType(getParams, fnType => fnType({in: Type, out: lsType(Type)})), - {i: getIn , t: fnType({in: Function, out: Type})}, - {i: getOut, t: fnType({in: Function, out: Type})}, + ...typedFnType(eqType, fnType => fnType({ + in: Type, + out: fnType({ + in: Type, + out: Bool, + })})), ]}; - - -// Wrapper around function below. -export const typedFnType = (instance, callback) => { - const [t, typesOfFns] = typedFnType2(callback); - const res = [ - { i: instance, t }, - ...typesOfFns, - ]; - return res; -}; - -// Create a function type, and also create Type-links for the function type (being typed by Function) and for all the nested function types. Saves a lot of code writing. -export const typedFnType2 = callback => { - const fnTs = []; - const wrappedFnType = ({ in: inType, out: outType }) => { - const fnT = fnType({ in: inType, out: outType }); - fnTs.push(fnT); - return fnT; - }; - const t = callback(wrappedFnType); // force evaluation - return [ - t, - fnTs.map(fnT => ({ i: fnT, t: Function })), - ]; -}; \ No newline at end of file diff --git a/primitives/bool.js b/primitives/bool.js index c15ee67..9a5da56 100644 --- a/primitives/bool.js +++ b/primitives/bool.js @@ -1,6 +1,6 @@ -import { fnType } from "../metacircular.js"; -import {Type, Function} from "../metacircular.js"; -import {Bool} from "./symbols.js"; +import { fnType } from "../structures/function.js"; +import { Type } from "../metacircular.js"; +import { Bool } from "./symbols.js"; const eqBool = x => y => x === y; diff --git a/primitives/byte.js b/primitives/byte.js index 617a946..96ea66c 100644 --- a/primitives/byte.js +++ b/primitives/byte.js @@ -1,5 +1,5 @@ -import { fnType } from "../type_registry.js"; -import {Type, Function} from "../metacircular.js"; +import { fnType } from "../structures/function.js"; +import { Type } from "../metacircular.js"; import {Byte, Bool} from "./symbols.js"; const eqByte = x => y => x === y; diff --git a/primitives/char.js b/primitives/char.js index afc489f..565b8d0 100644 --- a/primitives/char.js +++ b/primitives/char.js @@ -1,5 +1,5 @@ -import { typedFnType } from "../type_registry.js"; -import {Type} from "../metacircular.js"; +import { typedFnType } from "../structures/function.js"; +import { Type } from "../metacircular.js"; import {Char, Bool} from "./symbols.js"; const eq = x => y => x === y; diff --git a/primitives/double.js b/primitives/double.js index cbfa543..e85e919 100644 --- a/primitives/double.js +++ b/primitives/double.js @@ -1,6 +1,5 @@ -import { fnType } from "../metacircular.js"; -import {Type, Function} from "../metacircular.js"; - +import { fnType } from "../structures/function.js"; +import { Type } from "../metacircular.js"; import {Bool, Double} from "./symbols.js"; export const addDouble = x => y => x + y; diff --git a/primitives/int.js b/primitives/int.js index 8367c87..c64d266 100644 --- a/primitives/int.js +++ b/primitives/int.js @@ -1,5 +1,5 @@ -import { fnType } from "../metacircular.js"; -import {Type, Function} from "../metacircular.js"; +import { fnType } from "../structures/function.js"; +import { Type } from "../metacircular.js"; import {Bool, Int} from "./symbols.js"; diff --git a/primitives/symbols.js b/primitives/symbols.js index d24622e..adb5d3e 100644 --- a/primitives/symbols.js +++ b/primitives/symbols.js @@ -1,7 +1,7 @@ -// to break up dependency cycles, symbols of primitive types have their own JS module +// to break up dependency cycles, primitive types are defined in their own JS module -export const Bool = Symbol('Bool'); -export const Int = Symbol('Int'); -export const Double = Symbol('Double'); -export const Byte = Symbol('Byte'); -export const Char = Symbol('Char'); \ No newline at end of file +export const Int = { symbol: Symbol('Int') , params: [] }; +export const Bool = { symbol: Symbol('Bool') , params: [] }; +export const Double = { symbol: Symbol('Double'), params: [] }; +export const Byte = { symbol: Symbol('Byte') , params: [] }; +export const Char = { symbol: Symbol('Char') , params: [] }; diff --git a/progress.txt b/progress.txt index 3fe64bb..dc9888b 100644 --- a/progress.txt +++ b/progress.txt @@ -58,8 +58,15 @@ wip: The sad(?) part about all of this, is that I'm converging with Haskell/Lean. - - treat all values as polymorphic? (non-polymorphic values simply have empty set of type variables) todo: + - rename Type to a NominalType? + const nominalType = (name, params) => ({left: name, right: params}); + type of every nominal type is (String, [Type]) + + - what about type links: they connect anything to its type... what is the type of 'anything'? + + - treat all values as polymorphic? (non-polymorphic values simply have empty set of type variables) + - type inferencing can be reduced to finding a graph isomorphism? diff --git a/structures/function.js b/structures/function.js new file mode 100644 index 0000000..95416d8 --- /dev/null +++ b/structures/function.js @@ -0,0 +1,51 @@ +import { DefaultMap } from "../util.js"; + +const symbolFunction = Symbol('Function'); + +// The registry ensures that we never accidentally create more than one JS object for the same function type. +// It is a cheap workaround for JS lacking customizable hash-functions and equality-testing-functions. +// This same pattern is repeated throughout the code for all non-nullary type constructors (list, sum, product, ...) +const fnTypeRegistry = new DefaultMap(inType => new DefaultMap(outType => ({ + symbol: symbolFunction, + params: [inType, outType], +}))); + +// type constructor for function types +export const fnType = inType => outType => fnTypeRegistry.getdefault(inType, true).getdefault(outType, true); + + +// Wrapper around function below. +export const typedFnType = (instance, callback) => { + const [t, typesOfFns] = typedFnType2(callback); + const res = [ + { i: instance, t }, + ...typesOfFns, + ]; + return res; +}; +// Create a function type, and also create Type-links for the function type (being typed by Function) and for all the nested function types. Saves a lot of code writing. +export const typedFnType2 = callback => { + const fnTs = []; + const wrappedFnType = ({ in: inType, out: outType }) => { + const fnT = fnType({ in: inType, out: outType }); + fnTs.push(fnT); + return fnT; + }; + const t = callback(wrappedFnType); // force evaluation + return [ + t, + fnTs.map(fnT => ({ i: fnT, t: Function })), + ]; +}; + + +export const ModuleFunction = {l:[ + // binary type constructor: Type -> Type -> Type + ...typedFnType(fnType, fnType => fnType + /* in */ (Type) + /* out */ (fnType + /* in */ (Type) + /* out */ (Type) + ) + ), +]}; \ No newline at end of file diff --git a/structures/list.js b/structures/list.js index 597021b..2b8f3a5 100644 --- a/structures/list.js +++ b/structures/list.js @@ -1,16 +1,51 @@ -import { lsType } from "./list_common.js"; -import { fnType } from "../metacircular.js"; -import {Type, Function} from "../metacircular.js"; -import { makeListModule } from "./list_common.js"; -import { Module } from "./list_types/module.js"; +import { typedFnType } from "./function.js"; +import { Type } from "../metacircular.js"; +import { Int } from "../primitives/symbols.js"; +import { DefaultMap } from "../util.js"; +import { makeGeneric } from "../generics/generics.js"; -const Type_to_Type = fnType({in: Type, out: Type}); -const Type_to_Module = fnType({in: Type, out: Module}); +const symbolList = Symbol('List'); + +const listTypeRegistry = new DefaultMap(elementType => ({ + symbol: symbolList, + params: [elementType], +})); + +// type constructor +export const lsType = elementType => listTypeRegistry.getdefault(elementType, true); + +// 'normal' implementation +const emptyList = {l:[]}; +const get = ls => i => ls.l[i]; +const put = ls => i => elem => ({l: ls.l.with(Number(i), elem)}); +// const push = ls => elem => ({l:ls.l.concat([elem])}); export const ModuleList = {l:[ - {i: lsType , t: Type_to_Type}, - {i: Type_to_Type , t: Function}, + // Type -> Type + ...typedFnType(lsType, fnType => fnType + /* in */ (Type) + /* out */ (Type) + ), - {i: makeListModule, t: Type_to_Module}, - {i: Type_to_Module, t: Function}, + // [a] + {i: emptyList, t: makeGeneric(a => lsType(a))}, + + // [a] -> Int -> a + ...typedFnType(get, fnType => makeGeneric(a => fnType + /* in */ (lsType(a)) + /* out */ (fnType + /* in */ (Int) + /* out */ (a) + ))), + + // [a] -> Int -> a -> [a] + ...typedFnType(put, fnType => makeGeneric(a => fnType + /* in */ (lsType(a)) + /* out */ (fnType + /* in */ (Int) + /* out */ (fnType + /* in */ (a) + /* out */ (lsType(a)) + ) + ))), ]}; diff --git a/structures/list_common.js b/structures/list_common.js deleted file mode 100644 index f7429ad..0000000 --- a/structures/list_common.js +++ /dev/null @@ -1,75 +0,0 @@ -import { fnType } from "../metacircular.js"; -import {Type, Function} from "../metacircular.js"; -import {Int, Byte} from "../primitives/symbols.js"; -import { DefaultMap } from "../util.js"; - -const listTypeRegistry = new DefaultMap(elementType => ({ listOf: elementType })); - -// type constructor -export const lsType = elementType => listTypeRegistry.getdefault(elementType, true); - -// 'normal' implementation -const emptyList = {l:[]}; -const get = ls => i => ls.l[i]; -const put = ls => i => elem => ({l: ls.l.with(Number(i), elem)}); -// const push = ls => elem => ({l:ls.l.concat([elem])}); - -const byteListImpl = { - // specialization - emptyList: new Uint8Array(), - get: ls => i => ls[i], - put: ls => i => elem => { - res = new Uint8Array(ls); // creates copy - res[i] = elem; - return res; - } -} - -export const makeListModule = elementType => { - // List type depends on elementType - // generating it another time, will give the same type (structurally equivalent): - const ListOfElement = lsType(elementType); - - const getFnType1 = fnType({in: Int , out: elementType}); - const getFnType = fnType({in: ListOfElement, out: getFnType1}); - - const putFnType2 = fnType({in: elementType , out: ListOfElement}); - const putFnType1 = fnType({in: Int , out: putFnType2}); - const putFnType = fnType({in: ListOfElement, out: putFnType1}); - - // const pushFnType1 = fnType({in: elementType , out: ListOfElement}); - // const pushFnType = fnType({in: ListOfElement, out: pushFnType1}); - - const common = [ - {i: ListOfElement, t: Type}, - - {i: getFnType , t: Function}, - {i: getFnType1 , t: Function}, - - {i: putFnType , t: Function}, - {i: putFnType1, t: Function}, - {i: putFnType2, t: Function}, - - // {i: pushFnType , t: Function}, - // {i: pushFnType1, t: Function}, - ]; - - if (elementType === Byte) { - // specialization: use Uint8Array instead of JS array - return {l:[ - ...common, - {i: byteListImpl.emptyList , t: ListOfElement}, - {i: byteListImpl.get , t: getFnType}, - {i: byteListImpl.put , t: putFnType}, - ]}; - } - else { - return {l:[ - ...common, - {i: emptyList , t: ListOfElement}, - {i: get , t: getFnType}, - {i: put , t: putFnType}, - // {i: push , t: pushFnType}, - ]}; - } -}; \ No newline at end of file diff --git a/structures/list_types/module.js b/structures/list_types/module.js index 3d5ae3a..a2c72bf 100644 --- a/structures/list_types/module.js +++ b/structures/list_types/module.js @@ -1,7 +1,8 @@ -import { makeListModule } from "../list_common.js"; +import { makeListModule } from "../list.js"; import { Typed } from "../../typed.js"; -import { lsType } from "../list_common.js"; +import { lsType } from "../list.js"; +// just an alias export const Module = lsType(Typed); // a Module is a list of Typeds export const ModuleModule = makeListModule(Typed); // the module containing operations on Module diff --git a/structures/list_types/string.js b/structures/list_types/string.js index fcd1b92..a17465a 100644 --- a/structures/list_types/string.js +++ b/structures/list_types/string.js @@ -1,7 +1,8 @@ import { Char } from "../../primitives/symbols.js"; -import { lsType } from "../list_common.js"; -import { makeListModule } from "../list_common.js"; +import { lsType } from "../list.js"; +import { makeListModule } from "../list.js"; +// just an alias export const String = lsType(Char); export const ModuleString = makeListModule(Char); \ No newline at end of file diff --git a/structures/product.js b/structures/product.js index f7f3f1e..6d5e20e 100644 --- a/structures/product.js +++ b/structures/product.js @@ -1,37 +1,51 @@ -import { fnType } from "../metacircular.js"; -import { Function, Type } from "../metacircular.js"; +import { makeGeneric } from "../generics/generics.js"; +import { Type } from "../metacircular.js"; import { DefaultMap } from "../util.js"; +import { typedFnType } from "./function.js"; -const productTypeRegistry = new DefaultMap(leftType => new DefaultMap(rightType => ({ operator: "product", leftType, rightType }))); +const symbolProduct = Symbol("Product"); +const productTypeRegistry = new DefaultMap(leftType => new DefaultMap(rightType => ({ + symbol: symbolProduct, + params: [leftType, rightType], + }))); // type constructor -export const prodType = (leftType, rightType) => productTypeRegistry.getdefault(leftType, true).getdefault(rightType, true); +export const prodType = leftType => rightType => productTypeRegistry.getdefault(leftType, true).getdefault(rightType, true); // In JS, all products are encoded in the same way: const constructor = left => right => ({left, right}); const getLeft = product => product.left; const getRight = product => product.right; -// Given two types A and B, create the type (A × B). -export const makeProductType = (leftType, rightType) => { - const pType = prodType(leftType, rightType); +export const ModuleProduct = {l: [ + // binary type constructor + // Type -> Type -> Type + ...typedFnType(prodType, fnType => fnType + (Type) + (fnType + (Type) + (Type) + ) + ), - const leftFnType = fnType({in: pType, out: leftType}); - const rightFnType = fnType({in: pType, out: rightType}); + // a -> b -> (a, b) + ...typedFnType(constructor, fnType => makeGeneric((a, b) => fnType + (a) + (fnType + (b) + (prodType(a)(b)) + ) + )), - const constructorBoundType = fnType({in: rightType, out: pType}); - const constructorType = fnType({in: leftType , out: constructorBoundType}); + // (a, b) -> a + ...typedFnType(getLeft, fnType => makeGeneric((a, b) => fnType + (prodType(a)(b)) + (a) + )), - return {l:[ - {i: pType, t: Type}, - - {i: getLeft , t: leftFnType}, - {i: getRight , t: rightFnType}, - {i: leftFnType , t: Function}, - {i: rightFnType, t: Function}, - - {i: constructor , t: constructorType}, - {i: constructorType , t: Function}, - {i: constructorBoundType, t: Function}, - ]}; -}; \ No newline at end of file + // (a, b) -> b + ...typedFnType(getRight, fnType => makeGeneric((a, b) => fnType + (prodType(a)(b)) + (b) + )), +]}; diff --git a/structures/sum.js b/structures/sum.js index 0c1dcf1..62a5e90 100644 --- a/structures/sum.js +++ b/structures/sum.js @@ -1,53 +1,60 @@ import { prodType } from "./product.js"; -import { fnType } from "../metacircular.js"; -import { Function, Type } from "../metacircular.js"; -import { Module } from "./list_types/module.js"; +import { Type } from "../metacircular.js"; import { DefaultMap } from "../util.js"; +import { typedFnType } from "./function.js"; +import { makeGeneric } from "../generics/generics.js"; -const sumTypeRegistry = new DefaultMap(leftType => new DefaultMap(rightType => ({ operator: "sum", leftType, rightType }))); +const symbolSum = Symbol("Sum"); + +const sumTypeRegistry = new DefaultMap(leftType => new DefaultMap(rightType => ({ + symbol: symbolSum, + params: [leftType, rightType], +}))); // type constructor -export const sumType = (leftType, rightType) => sumTypeRegistry.getdefault(leftType, true).getdefault(rightType, true); +export const sumType = leftType => rightType => sumTypeRegistry.getdefault(leftType, true).getdefault(rightType, true); const constructorLeft = left => ({variant: "L", value: left }); const constructorRight = right => ({variant: "R", value: right}); -// (, product(double, double)): product(int, type) - // signature: // sum-type -> (leftType -> resultType, rightType -> resultType) -> resultType 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); +export const ModuleSum = {l:[ + // binary type constructor + // Type -> Type -> Type + ...typedFnType(sumType, fnType => fnType + (Type) + (fnType + (Type) + (Type) + ), + ), - const constructorLeftType = fnType({in: leftType , out: sType}); - const constructorRightType = fnType({in: rightType, out: sType}); + // a -> a | b + ...typedFnType(constructorLeft, fnType => makeGeneric((a, b) => fnType + (a) + (sumType(a)(b)) + )), - // 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 {l:[ - {i: match , t: matchFnType}, - {i: matchFnType, t: Function}, - ]}; - }; + // b -> a | b + ...typedFnType(constructorRight, fnType => makeGeneric((a, b) => fnType + (b) + (sumType(a)(b)) + )), - return {l:[ - {i: sType , t: Type}, - {i: constructorLeft , t: constructorLeftType}, - {i: constructorRight , t: constructorRightType}, - {i: constructorLeftType , t: Function}, - {i: constructorRightType, t: Function}, - - {i: makeMatchFn, t: fnType({in: Type, out: Module})}, - ]}; -}; \ No newline at end of file + // a | b -> (a -> c, b-> c) -> c + ...typedFnType(match, fnType => makeGeneric((a, b, c) => fnType + (sumType(a)(b)) + (fnType + (prodType + (fnType(a)(c)) + (fnType(b)(c)) + ) + (c) + ) + )), +]}; diff --git a/typeclasses/eq.js b/typeclasses/eq.js index 12efd21..12c140d 100644 --- a/typeclasses/eq.js +++ b/typeclasses/eq.js @@ -1,5 +1,6 @@ import { makeGeneric } from "../generics/generics"; -import { Type, typedFnType } from "../metacircular"; +import { Type } from "../metacircular"; +import { typedFnType } from "../structures/function"; import { Bool, Byte, Char, Double, Int } from "../primitives/symbols"; import { deepEqual } from "../util"; import { eqDictType } from "./eq_type"; diff --git a/typeclasses/num.js b/typeclasses/num.js index c68cd60..683120a 100644 --- a/typeclasses/num.js +++ b/typeclasses/num.js @@ -1,7 +1,8 @@ import { makeGeneric } from "../generics/generics.js"; import { addDouble, mulDouble } from "../primitives/double.js"; import { addInt, mulInt } from "../primitives/int.js"; -import { Type, typedFnType, typedFnType2 } from "../metacircular.js"; +import { Type } from "../metacircular.js"; +import { typedFnType, typedFnType2 } from "../structures/function.js"; import { Double, Int } from "../primitives/symbols.js"; import { numDictType } from "./num_type.js"; diff --git a/typeclasses/num.test.js b/typeclasses/num.test.js index ffbb70c..16405f9 100644 --- a/typeclasses/num.test.js +++ b/typeclasses/num.test.js @@ -1,6 +1,6 @@ import { assign } from "../generics/generics.js"; import { makeGeneric } from "../generics/generics.js"; -import { fnType } from "../metacircular.js"; +import { fnType } from "../structures/function.js"; import { Double, Int } from "../primitives/symbols.js"; import { getMul, NumInstances } from "./num.js"; import { numDictType } from "./num_type.js"; diff --git a/typed.js b/typed.js index 0fb5e47..f3deebe 100644 --- a/typed.js +++ b/typed.js @@ -1,17 +1,21 @@ -import { fnType } from "./metacircular.js"; -import {Type, Function} from "./metacircular.js"; +import { fnType } from "./structures/function.js"; +import { Type } from "./metacircular.js"; -export const Typed = Symbol('Typed'); +// Everything is (implicitly) typed by the Any type. +export const Any = { symbol: Symbol('Any'), params: [] }; + +// A type-link, connecting a value to its Type. +export const Typed = { symbol: Symbol('Typed'), params: [] }; const getInst = lnk => lnk.i; const getType = lnk => lnk.t; -const Typed_to_Type = fnType({in: Typed, out: Type}); +const Typed_to_Type = fnType({in: Any, out: Type}); export const ModuleTyped = {l:[ {i: Typed, t: Type}, - {i: Typed_to_Type, t: Function}, + {i: Typed_to_Type, t: Type}, {i: getInst, t: Typed_to_Type}, {i: getType, t: Typed_to_Type}, diff --git a/util.js b/util.js index 0a49223..4fd92fb 100644 --- a/util.js +++ b/util.js @@ -22,6 +22,11 @@ export function deepEqual(a, b) { return true; } +// zip two arrays +export function zip(a, b) { + return a.map((k, i) => [k, b[i]]); +} + export class DefaultMap { constructor(defaultValue, ...rest) { this.defaultValue = defaultValue;