From 4a4983f693079ae5ed57d86abf65b07298110604 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Wed, 2 Apr 2025 15:49:43 +0200 Subject: [PATCH] interactive prompt can handle polymorphic types --- generics/generics.js | 190 ++++++++++++---------- generics/generics.test.js | 17 +- lib/symbol.js | 11 +- primitives/generic_type.js | 21 +++ type.js => primitives/type.js | 10 +- primitives/types.js | 6 +- primitives/unit.js | 6 +- scripts/main.js | 298 ++++++++++++++++++++++------------ stdlib.js | 8 +- structures/list.js | 17 +- structures/nominal.js | 81 ++++++++- structures/product.js | 8 +- structures/set.js | 13 +- structures/sum.js | 8 +- structures/types.js | 32 ++-- structures/versioned.js | 11 +- type_constructor.js | 7 +- typeclasses/eq.js | 4 +- typed.js | 12 +- util/pretty.js | 1 - 20 files changed, 485 insertions(+), 276 deletions(-) create mode 100644 primitives/generic_type.js rename type.js => primitives/type.js (76%) diff --git a/generics/generics.js b/generics/generics.js index 400f639..a634c33 100644 --- a/generics/generics.js +++ b/generics/generics.js @@ -1,160 +1,180 @@ -import { eqType } from "../type.js"; +import { eqType } from "../primitives/type.js"; import { zip } from "../util/util.js"; import { pretty } from '../util/pretty.js'; +import { prettyT } from "../structures/types.js"; // constructor for generic types // for instance, the type: -// a -> a -> Bool +// ∀a: a -> a -> Bool // is created by -// makeGeneric(a => fnType({in: a, out: fnType({in: a, out: Bool})})) +// makeGeneric(a => fnType(a)(fnType(a)(Bool))) export const makeGeneric = callback => { // type variables to make available: - const typeVars = ['a', 'b', 'c', 'd', 'e'].map( - letter => ({ - symbol: Symbol(letter), - params: [], - })); + const typeVars = ['a', 'b', 'c', 'd', 'e'].map(Symbol); const type = callback(...typeVars); - return { - typeVars: occurring(type, new Set(typeVars)), - type, - }; + return onlyOccurring(type, new Set(typeVars)); }; +export const onlyOccurring = (type, typeVars) => ({ + typeVars: occurring(type, typeVars), + type, +}); + // From the given set of type variables, return only those that occur in the given type. export const occurring = (type, typeVars) => { // console.log("occurring", type); - if (typeVars.has(type)) { // type IS a type variable: return new Set([type]); } return new Set(type.params.flatMap(p => [...occurring(p, typeVars)])); -} +}; // Merge 2 substitution-mappings, uni-directional. const mergeOneWay = (m1, m2) => { const m1copy = new Map(m1); const m2copy = new Map(m2); - for (const [key1, val1] of m1copy.entries()) { - if (m2copy.has(val1)) { - m1copy.set(key1, m2.get(val1)); - m2copy.delete(val1); - return [false, m1copy, m2copy, new Set([val1])]; + for (const [var1, typ1] of m1copy.entries()) { + if (m2copy.has(typ1)) { + // typ1 is a typeVar for which we also have a substitution + // -> fold substitutions + m1copy.set(var1, m2.get(typ1)); + m2copy.delete(typ1); + return [false, m1copy, m2copy, new Set([typ1])]; } } return [true, m1copy, m2copy, new Set()]; // stable -} +}; -// Merge 2 substitution-mappings, bi-directional. -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)}`); +const checkConflict = (m1, m2) => { + for (const [var1, typ1] of m1) { + if (m2.has(var1)) { + const other = m2.get(var1); + if (!eqType(typ1, other)) { + throw new Error(`conflicting substitution: ${pretty(typ1)}vs. ${pretty(other)}`); } } } +}; + +// Merge 2 substitution-mappings, bi-directional. +export const mergeTwoWay = (m1, m2) => { + // console.log("mergeTwoWay", {m1, m2}); + checkConflict(m1, m2); + // checkConflict(m2, m1); // <- don't think this is necessary... // actually merge let stable = false; - let deletedTypeVars = new Set(); + let deleted = new Set(); while (!stable) { let d; // notice we swap m2 and m1, so the rewriting can happen both ways: [stable, m2, m1, d] = mergeOneWay(m1, m2); - deletedTypeVars = deletedTypeVars.union(d); + deleted = deleted.union(d); } - return [new Map([...m1, ...m2]), deletedTypeVars]; -} + const result = { + substitutions: new Map([...m1, ...m2]), + deleted, // deleted type variables + }; + // console.log("mergeTwoWay result =", result); + return result; +}; // 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}, -) => { - // console.log("unify", pretty({formalTypeVars, formalType, actualTypeVars, actualType})); +const unifyInternal = (typeVars, fType, aType) => { + // console.log("unify", pretty({typeVars, fType, aType})); - if (formalTypeVars.has(formalType)) { + if (typeVars.has(fType)) { // simplest case: formalType is a type paramater // => substitute with actualType // console.log("assign actual to formal"); return { - substitutions: new Map([[formalType, actualType]]), - typeVars: new Set([ - ...actualTypeVars, - ...[...formalTypeVars].filter(a => a !== formalType), - ]), - type: actualType, + substitutions: new Map([[fType, aType]]), + genericType: { + typeVars: typeVars.difference(new Set([fType])), + type: aType, + }, }; } - if (actualTypeVars.has(actualType)) { + if (typeVars.has(aType)) { // same as above, but in the other direction // console.log("assign formal to actual"); return { - substitutions: new Map([[actualType, formalType]]), - typeVars: new Set([ - ...[...actualTypeVars].filter(a => a !== actualType), - ...formalTypeVars, - ]), - type: formalType, + substitutions: new Map([[aType, fType]]), + genericType: { + typeVars: typeVars.difference(new Set([aType])), + type: fType, + }, }; } // recursively unify - if (formalType.symbol !== actualType.symbol) { - throw new Error(`cannot unify ${pretty(formalType.symbol)} and ${pretty(actualType.symbol)}`); + if (fType.symbol !== aType.symbol) { + throw new Error(`cannot unify ${prettyT(fType)} and ${prettyT(aType)}`); } else { - // console.log("symbols match - unify recursively", formalType.symbol); - 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))); + // console.log("symbols match - unify recursively", formal.symbol); + const unifiedParams = + zip(fType.params, aType.params) + .map(([fParam, aParam]) => unifyInternal(typeVars, fParam, aParam)); + const {substitutions, deleted} = + unifiedParams.reduce(({substitutions: s, deleted: d}, cur) => { + // console.log('merging', s, cur.substitutions); + const {substitutions, deleted} = mergeTwoWay(s, cur.substitutions); + return { + substitutions, + deleted: deleted.union(d), + }; + }, { substitutions: new Map(), deleted: new Set() }); + // console.log(pretty({unifiedParams})); return { - substitutions: unifiedSubstitusions, - typeVars: unifiedTypeVars, - type: { - symbol: formalType.symbol, - params: unifiedParams.map(p => p.type), + substitutions, + genericType: { + typeVars: typeVars.difference(substitutions).difference(deleted), + type: { + symbol: fType.symbol, + params: unifiedParams.map(p => p.genericType.type), + }, }, }; } }; +export const unify = (fGenericType, aGenericType) => { + const {genericType} = unifyInternal( + fGenericType.typeVars.union(aGenericType.typeVars), + fGenericType.type, + aGenericType.type, + ) + return genericType; +} + export const substitute = (type, substitutions) => { + // console.log("substitute", {type, substitutions}) if (substitutions.has(type)) { - // type IS a type var to be substituted: return substitutions.get(type); } - if (type.params.length === 0) { - // Attention: there's a reason why we have this special case. - // Types are compared by object ID, so we don't want to create a new object for a type that takes no type parameters (then the newly create type would differ). - // Should fix this some day. - return type; + if (typeof type === "symbol") { + return type; // nothing to substitute here } 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: inType, - }, paramType); - const substitutedOutType = substitute(outType, matchedInType.substitutions); - return { - typeVars: matchedInType.typeVars, - type: substitutedOutType, - }; + const allTypeVars = genFnType.typeVars.union(paramType.typeVars) + const {substitutions} = unifyInternal(allTypeVars, inType, paramType.type); + const substitutedOutType = substitute(outType, substitutions); + return onlyOccurring(substitutedOutType, allTypeVars); }; + +export const assignFn = (genFnType, paramType) => { + const [inType] = genFnType.type.params; + const allTypeVars = genFnType.typeVars.union(paramType.typeVars) + const {substitutions} = unifyInternal(allTypeVars, inType, paramType.type); + // console.log({genFnType: prettyT(genFnType), paramType: prettyT(paramType), substitutions}) + const substitutedFnType = substitute(genFnType.type, substitutions); + return onlyOccurring(substitutedFnType, allTypeVars); +} \ No newline at end of file diff --git a/generics/generics.test.js b/generics/generics.test.js index 0d967d2..3dcffdf 100644 --- a/generics/generics.test.js +++ b/generics/generics.test.js @@ -1,21 +1,20 @@ import { Bool, Int } from "../primitives/types.js"; -import { fnType, lsType } from "../structures/types.js"; +import { fnType, lsType, prettyT } from "../structures/types.js"; import { assign, makeGeneric, unify } from "./generics.js"; -import { pretty } from "../util/pretty.js"; // a -> Int const a_to_Int = makeGeneric(a => fnType(a)(Int)); // Bool -> Int const Bool_to_Int = makeGeneric(() => fnType(lsType(Bool))(Int)); console.log("should be: [Bool] -> Int") -console.log(pretty(unify(a_to_Int, Bool_to_Int))); +console.log(prettyT(unify(a_to_Int, Bool_to_Int))); // (a -> a) -> b const fnType2 = makeGeneric((a,b) => fnType(fnType(a)(a))(b)); // (Bool -> Bool) -> a const fnType3 = makeGeneric(a => fnType(fnType(Bool)(Bool))(a)); console.log("should be: (Bool -> Bool) -> a"); -console.log(pretty(unify(fnType2, fnType3))); +console.log(prettyT(unify(fnType2, fnType3))); // (a -> b) -> [a] -> [b] const mapFnType = makeGeneric((a,b) => @@ -23,10 +22,10 @@ const mapFnType = makeGeneric((a,b) => (fnType(a)(b)) (fnType(lsType(a))(lsType(b)))) // a -> a -const idFnType = makeGeneric(a => - fnType(a)(a)); -console.log("should be: [a] -> [a]"); -console.log(pretty(assign(mapFnType, idFnType))); +const idFnType = makeGeneric((_,__,c) => + fnType(c)(c)); +console.log("should be: [c] -> [c]"); +console.log(prettyT(assign(mapFnType, idFnType))); // (a -> Int) -> [a] -> a const weirdFnType = makeGeneric(a => @@ -40,4 +39,4 @@ const weirdFnType = makeGeneric(a => // a := b // b := Int console.log("should be: [Int] -> Int"); -console.log(pretty(assign(weirdFnType, idFnType))); +console.log(prettyT(assign(weirdFnType, idFnType))); diff --git a/lib/symbol.js b/lib/symbol.js index e55a17c..bbaf586 100644 --- a/lib/symbol.js +++ b/lib/symbol.js @@ -3,20 +3,17 @@ import { Bool, SymbolT, Type } from "../primitives/types.js"; import { String } from "../structures/list.js"; import { typedFnType } from "../structures/types.js"; +// The way instances of SymbolT are currently encoded, their constructor is not a valid DOPE function, because it is impure. +// The only way to construct symbols is to do it in JS code. + export const ModuleSymbol = {l:[ {i: SymbolT, t: Type}, - // ...typedFnType(constructSymbol, fnType => - // fnType - // (String) - // (SymbolT) - // ), - ...typedFnType(getName, fnType => fnType (SymbolT) (String) ), - ...typedFnType(eqSymbol, fnType => fnType(SymbolT, fnType(SymbolT, Bool))), + ...typedFnType(eqSymbol, fnType => fnType(SymbolT)(fnType(SymbolT)(Bool))), ]}; diff --git a/primitives/generic_type.js b/primitives/generic_type.js new file mode 100644 index 0000000..1f029aa --- /dev/null +++ b/primitives/generic_type.js @@ -0,0 +1,21 @@ +import { constructorLeft, constructorRight } from "../structures/sum.js"; +import { fnType, setType, sumType, typedFnType } from "../structures/types.js"; +import { GenericType, SymbolT, Type, Unit } from "./types.js"; +import { unit } from "./unit.js"; + +export const getType = genericType => genericType.type; +export const getTypeVars = genericType => genericType.typeVars; + +export const toNonGeneric = genericType => (genericType.typeVars.size === 0) + ? constructorRight(genericType.type) + : constructorLeft(unit); + +export const ModuleGenericType = {l:[ + {i: GenericType, t: Type}, + + ...typedFnType(getType, fnType => fnType(GenericType)(Type)), + + ...typedFnType(getTypeVars, fnType => fnType(GenericType)(setType(SymbolT))), + + ...typedFnType(toNonGeneric, fnType => fnType(GenericType)(sumType(Unit)(Type))), +]}; diff --git a/type.js b/primitives/type.js similarity index 76% rename from type.js rename to primitives/type.js index bbd299f..a1f994a 100644 --- a/type.js +++ b/primitives/type.js @@ -1,7 +1,7 @@ -import { Bool, SymbolT, Type } from "./primitives/types.js"; -import { isFunction, lsType, typedFnType } from "./structures/types.js"; -import { getSymbol, getParams } from "./type_constructor.js"; -import { deepEqual } from "./util/util.js"; +import { Bool, SymbolT, Type } from "./types.js"; +import { isFunction, lsType, typedFnType } from "../structures/types.js"; +import { getSymbol, getParams } from "../type_constructor.js"; +import { deepEqual } from "../util/util.js"; // we can test whether types are equal: export const eqType = t1 => t2 => deepEqual(t1, t2); @@ -30,4 +30,4 @@ export const ModuleType = {l:[ ...typedFnType(getParams, fnType => fnType(Type)(lsType(Type))), ...typedFnType(isFunction, fnType => fnType(Type)(Bool)), -]}; +]}; \ No newline at end of file diff --git a/primitives/types.js b/primitives/types.js index cfe423f..974497d 100644 --- a/primitives/types.js +++ b/primitives/types.js @@ -10,6 +10,7 @@ const SymbolChar = Symbol('Char'); const SymbolUnit = Symbol('Unit'); const SymbolSymbol = Symbol('Symbol'); const SymbolType = Symbol('Type'); +const SymbolGenericType = Symbol('GenericType'); export const Int = makeTypeConstructor(SymbolInt)(0); export const Bool = makeTypeConstructor(SymbolBool)(0); @@ -24,6 +25,8 @@ export const SymbolT = makeTypeConstructor(SymbolSymbol)(0); export const Type = makeTypeConstructor(SymbolType)(0); +export const GenericType = makeTypeConstructor(SymbolGenericType)(0); + export const ModuleSymbols = {l:[ {i: SymbolInt , t: SymbolT}, @@ -34,4 +37,5 @@ export const ModuleSymbols = {l:[ {i: SymbolUnit , t: SymbolT}, {i: SymbolSymbol, t: SymbolT}, {i: SymbolType , t: SymbolT}, -]}; \ No newline at end of file + {i: SymbolGenericType, t: SymbolT}, +]}; diff --git a/primitives/unit.js b/primitives/unit.js index 68fa78c..af5156b 100644 --- a/primitives/unit.js +++ b/primitives/unit.js @@ -1,10 +1,12 @@ import { typedFnType } from "../structures/types.js"; import { Bool, Type, Unit } from "./types.js"; -const eqUnit = x => y => x === y; +export const eqUnit = x => y => x === y; + +export const unit = {}; export const ModuleUnit = {l:[ - {i: {}, t: Unit}, + {i: unit, t: Unit}, {i: Unit, t: Type}, diff --git a/scripts/main.js b/scripts/main.js index e84803c..ef3a6ab 100644 --- a/scripts/main.js +++ b/scripts/main.js @@ -4,8 +4,37 @@ import { DefaultMap } from "../util/defaultmap.js"; import { pretty } from '../util/pretty.js'; import { isFunction, prettyT } from '../structures/types.js'; import { ModuleStd } from '../stdlib.js'; -import { Double, Int, SymbolT, Type } from "../primitives/types.js"; -import { eqType } from '../type.js'; +import { Double, GenericType, Int, SymbolT, Type } from "../primitives/types.js"; +import { eqType } from '../primitives/type.js'; +import { Any } from '../typed.js'; +import { assign, assignFn, makeGeneric, onlyOccurring } from '../generics/generics.js'; + + +// import {emitKeypressEvents} from 'node:readline'; + +// // Configure readline to read from stdin +// emitKeypressEvents(process.stdin); +// process.stdin.setRawMode(true); + +// console.log('Press any key (ESC to exit)...'); + +// process.stdin.on('keypress', (str, key) => { +// if (key.name === 'escape') { +// console.log('Escape key pressed!'); +// process.exit(); +// } +// }); + +const prettyIT = ({i, t}) => ({ + strI: isType(i) ? prettyT(i) : pretty(i), + strT: prettyT(t), + // strI: pretty(i), + // strT: pretty(t), +}); + +const isType = i => i.typeVars || i.symbol; + // ctx.types.getdefault(i).has(Type) + // || ctx.types.getdefault(i).has(GenericType); class Context { constructor(mod) { @@ -14,64 +43,44 @@ class Context { this.instances = new DefaultMap(() => new Set()); // type to instance for (const {i, t} of mod.l) { + const {strI, strT} = prettyIT({i,t}) + // console.log(strI, '::', strT); + this.types.getdefault(i, true).add(t); + this.types.getdefault(i, true).add(Any); this.instances.getdefault(t, true).add(i); + this.instances.getdefault(Any, true).add(i); } - - this.functionsFrom = new DefaultMap(() => new Set()); // type to outgoing function - this.functionsTo = new DefaultMap(() => new Set()); // type to incoming function - - for (const t of this.instances.m.keys()) { + const addIfFunctionType = (t, originalT, add) => { if (isFunction(t)) { - // 't' is a function signature - for (const fn of this.instances.getdefault(t)) { - this.functionsFrom.getdefault(t.params[0], true).add(fn); - this.functionsTo .getdefault(t.params[1], true).add(fn); + for (const fn of this.instances.getdefault(originalT)) { + add(fn); } } } - - // this.typeVarAssigns = new Map(); - - // for (const t of this.instances.m.keys()) { - // if (t.typeVars) { - // for (const t2 of this.instances.m.keys()) { - // const genericT2 = (t2.typeVars === undefined) - // ? makeGeneric(() => t2) - // : t2; - // try { - // const unification = unify(t, t2); - // console.log(unification); - // } catch (e) { - // // skip - // } - // } - // } - // } + this.functions = []; + for (const type of this.instances.getdefault(Type)) { + addIfFunctionType(type, type, fn => this.functions.push({fn, type})); + } + this.genericFunctions = []; + for (const genericType of this.instances.getdefault(GenericType)) { + addIfFunctionType(genericType.type, genericType, fn => this.genericFunctions.push({fn, genericType})); + } } addToCtx({i, t}) { return new Context({l:[ ...this.mod.l, {i, t}, - ]}) + ]}); } } - let ctx = new Context({l:[ ...ModuleStd.l, ...ModulePoint.l, ]}); - -const prettyIT = ({i, t}) => ({ - strI: isType(i) ? prettyT(i) : pretty(i), - strT: prettyT(t), - // strI: pretty(i), - // strT: pretty(t), -}); - const toChoices = ([i, types]) => { return [...types].map(t => { const {strI, strT} = prettyIT({i, t}); @@ -82,50 +91,33 @@ const toChoices = ([i, types]) => { short: `${strI} :: ${strT}`, }; }); -} - -const isType = i => ctx.types.getdefault(i).has(Type); +}; async function topPrompt() { const action = await select({ message: "What do you want to do?", choices: [ "list all types", + "list all generic types", "list all functions", "list all", ], }); if (action === "list all types") { await listInstances(Type); - // await listAllTypes(); + } + if (action === "list all generic types") { + await listInstances(GenericType); } if (action === "list all functions") { await listAllFunctions(); } if (action === "list all") { - await listAllInstances(); + await listAll(); } return topPrompt(); } -// async function listAllTypes() { -// const choice = await select({ -// message: "select type:", -// choices: [ -// "(go back)", -// ...[...ctx.instances.m.keys()].map(t => ({ -// value: t, -// name: prettyT(t), -// })), -// ] -// }); -// if (choice === "(go back)") { -// return; -// } -// await typeOptions(choice); -// return listAllTypes(); -// } - async function listAllFunctions() { const choice = await select({ message: "select function:", @@ -150,11 +142,12 @@ async function listAllFunctions() { return listAllFunctions(); } -async function typeOptions(t) { +async function typeOptions(t, tt) { const choice = await select({ - message: `actions for type ${prettyT(t)} :: Type`, + message: `actions for type ${prettyT(t)} :: ${prettyT(tt)}`, choices: [ "(go back)", + "create instance", "list instances", // "list outgoing functions", // "list incoming functions", @@ -165,6 +158,13 @@ async function typeOptions(t) { if (choice === "(go back)") { return; } + else if (choice === "create instance") { + const i = await createInstance(t); + if (i !== undefined) { + ctx = ctx.addToCtx({i, t}); + return instanceOrTypeOrFnOptions({i, t}); + } + } else if (choice === "list instances") { await listInstances(t); } @@ -172,15 +172,15 @@ async function typeOptions(t) { console.log(pretty(t)); } else if (choice === "treat as instance") { - await instanceOptions(t, Type) + await instanceOptions(t, tt) } else { console.log("unimplemented:", choice); } - return typeOptions(t); + return typeOptions(t, tt); } -async function listAllInstances() { +async function listAll() { const choice = await select({ message: `all instances:`, choices: [ @@ -195,11 +195,16 @@ async function listAllInstances() { } async function instanceOrTypeOrFnOptions({i, t}) { + if (t.typeVars) { + if (isFunction(t.type)) { + return functionOptions(i, t); + } + } if (isFunction(t)) { return functionOptions(i, t); } - if (isType(i)) { - return typeOptions(i); + if (t === Type || t === GenericType) { + return typeOptions(i, t); } return instanceOptions(i,t); } @@ -230,8 +235,8 @@ async function listTypes(i) { if (choice === "(go back)") { return; } - const {i: chosenType} = choice; - await typeOptions(chosenType); + const {i: chosenType, t: typeOfChosenType} = choice; + await typeOptions(chosenType, typeOfChosenType); return listTypes(i); } @@ -257,53 +262,98 @@ async function functionOptions(fn, fnT) { return functionOptions(fn, fnT); } +async function createInstance(t) { + if (t.typeVars && t.typeVars.size === 0) { + t = t.type; // can treat as non-generic + } + if (eqType(t)(Int)) { + const n = await number({ + message: `enter an integer (leave empty to go back):`, + step: 1, // only integers + }); + if (n === undefined) { + return; + } + return BigInt(n); + } + else if (eqType(t)(Double)) { + const n = await number({ + message: `enter a number (leave empty to go back):`, + step: 'any', + }); + return n; + } + else if (eqType(t)(SymbolT)) { + console.log("Note: you are creating a new Symbol. Even if the description matches that of another symbol (e.g., \"Int\"), a new Symbol will be created that is unique and only equal to itself."); + const symbolDescr = await input({message: "enter symbol description:"}); + return Symbol(symbolDescr); + } + else { + console.log("no prompt handler for creating new", prettyT(t)); + } +} + async function callFunction(fn, fnT) { const {strI, strT} = prettyIT({i: fn, t: fnT}); - const inType = fnT.params[0]; + let choices; + let inType; + if (fnT.typeVars) { + // generic + choices = [...ctx.types.m.entries()].flatMap(([i, types]) => { + return [...types].flatMap(t => { + const genT = t.typeVars ? t : makeGeneric(() => t); + let assignedFnType; + try { + assignedFnType = assignFn(fnT, genT); + } catch (e) { + if (e.message.startsWith("cannot unify")) { + // console.warn(e); + return []; + } + throw e; + } + const assignedInType = onlyOccurring(assignedFnType.type.params[0], assignedFnType.typeVars); + if (assignedInType.typeVars.size > 0) { + return toChoices([i, [assignedInType]]); + } + else { + return toChoices([i, [assignedInType.type]]); + } + }); + }); + inType = onlyOccurring(fnT.type.params[0], fnT.typeVars) + } + else { + inType = fnT.params[0]; + choices = [...ctx.instances.getdefault(inType)].flatMap(i => toChoices([i, ctx.types.getdefault(i)])); + } + const choice = await select({ - message: `select parameter for function ${strI} :: ${strT}`, + message: `select parameter of type ${prettyT(inType)} for function ${strI} :: ${strT}`, choices: [ "(go back)", "(new)", - ... [...ctx.instances.getdefault(inType)].flatMap(i => toChoices([i, ctx.types.getdefault(i)])), + ...choices, ], }); - let i; + let i, t; if (choice === "(go back)") { return; } - if (choice === "(new)") { - if (eqType(inType)(Int)) { - const n = await number({ - message: `enter an integer (leave empty to go back):`, - step: 1, // only integers - }); - if (n === undefined) { - return; - } - i = BigInt(n); - } - else if (eqType(inType)(Double)) { - const n = await number({ - message: `enter a number (leave empty to go back):`, - step: 'any', - }); - if (n === undefined) { - return; - } - i = n; - } - else if (eqType(inType)(SymbolT)) { - console.log("Note: you are creating a new Symbol. Even if the description matches that of another symbol (e.g., \"Int\"), a new Symbol will be created that is unique and only equal to itself."); - const symbolDescr = await input({message: "enter symbol description:"}); - i = Symbol(symbolDescr); - } - else { - console.log("no prompt handler for creating new", prettyT(inType)); - return callFunction(fn, fnT); + else if (choice === "(new)") { + i = await createInstance(inType); + t = inType; + if (i === undefined) { + return; } } - await apply(i, fn, fnT); + else { + i = choice.i; + t = choice.t; + } + const genT = t.typeVars ? t : makeGeneric(() => t); + const assignedFnType = assignFn(fnT, genT); + await apply(i, fn, assignedFnType); return callFunction(fn, fnT); } @@ -332,12 +382,35 @@ async function instanceOptions(i,t) { async function transform(i, t) { const {strI, strT} = prettyIT({i, t}); // console.log(ctx.functionsFrom.getdefault(t)); + + const genT = t.typeVars ? t : makeGeneric(() => t); const choice = await select({ message: `choose transformation to perform on ${strI} :: ${strT}`, choices: [ "(go back)", - ... [...ctx.functionsFrom.getdefault(t)].flatMap(fn => toChoices([fn, ctx.types.getdefault(fn)])), + + ...ctx.functions + .filter(({type}) => { + // console.log(type.params[0], t); + return eqType(type.params[0])(t) + }) + .flatMap(({fn, type}) => toChoices([fn, [type]])), + + ...ctx.genericFunctions + .flatMap(({fn, genericType}) => { + let fnType; + try { + fnType = assignFn(genericType, genT); + } catch (e) { + if (e.message.startsWith("cannot unify")) { + // console.warn(e); + return []; + } + throw e; + } + return toChoices([fn, [fnType]]); + }), ], }); if (choice === "(go back)") { @@ -352,9 +425,18 @@ async function transform(i, t) { async function apply(i, fn, fnT) { const result = fn(i); // console.log(fn, '(', i, ')', '=', result); - const resultType = fnT.params[1]; + let resultType; + // console.log(fnT); + if (fnT.typeVars) { + resultType = onlyOccurring(fnT.type.params[1], fnT.typeVars); + } + else { + resultType = fnT.params[1]; + } // update context with newly produced value - ctx = ctx.addToCtx({i: result, t: resultType}); + ctx = ctx + .addToCtx({i: result, t: resultType}) + .addToCtx({i: resultType, t: resultType.typeVars ? GenericType : Type}); const {strI: strResult, strT: strResultType} = prettyIT({i: result, t: resultType}); console.log(`result = ${strResult} :: ${strResultType}`); return instanceOrTypeOrFnOptions({i: result, t: resultType}); diff --git a/stdlib.js b/stdlib.js index 4f28010..9f6c1bf 100644 --- a/stdlib.js +++ b/stdlib.js @@ -11,11 +11,14 @@ import { ModuleFunction } from "./structures/function.js"; import { ModuleList } from "./structures/list.js"; import { ModuleProduct } from "./structures/product.js"; import { ModuleSum } from "./structures/sum.js"; -import { ModuleType } from "./type.js"; +import { ModuleType } from "./primitives/type.js"; import { ModuleTyped } from "./typed.js"; +import { ModuleSet } from "./structures/set.js"; +import { ModuleGenericType } from "./primitives/generic_type.js"; export const ModuleStd = {l:[ ...ModuleType.l, + ...ModuleGenericType.l, ...ModuleTyped.l, ...ModuleTypeConstructor.l, @@ -35,4 +38,5 @@ export const ModuleStd = {l:[ ...ModuleList.l, ...ModuleProduct.l, ...ModuleSum.l, -]}; \ No newline at end of file + ...ModuleSet.l, +]}; diff --git a/structures/list.js b/structures/list.js index 6582ff9..60a6cd4 100644 --- a/structures/list.js +++ b/structures/list.js @@ -1,16 +1,19 @@ import { typedFnType } from "./types.js"; -import { Char, Type } from "../primitives/types.js"; +import { Char, GenericType, Type } from "../primitives/types.js"; import { Int } from "../primitives/types.js"; import { makeGeneric } from "../generics/generics.js"; import { lsType } from "./types.js"; +import { Typed } from "../typed.js" // 'normal' implementation const emptyList = {l:[]}; +const emptyListType = makeGeneric(a => lsType(a)); 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 String = lsType(Char); // alias +export const Module = lsType(Typed); export const ModuleList = {l:[ // Type -> Type @@ -21,7 +24,8 @@ export const ModuleList = {l:[ ), // [a] - {i: emptyList, t: makeGeneric(a => lsType(a))}, + {i: emptyList, t: emptyListType}, + {i: emptyListType, t: GenericType}, // [a] -> Int -> a ...typedFnType(get, fnType => @@ -31,7 +35,7 @@ export const ModuleList = {l:[ /* out */ (fnType /* in */ (Int) /* out */ (a) - ))), + )), GenericType), // [a] -> Int -> a -> [a] ...typedFnType(put, fnType => @@ -44,7 +48,7 @@ export const ModuleList = {l:[ /* in */ (a) /* out */ (lsType(a)) ) - ))), + )), GenericType), // [a] -> a -> [a] ...typedFnType(push, fnType => @@ -55,8 +59,5 @@ export const ModuleList = {l:[ (a) (lsType(a)) ) - ) - ), - - // {i: String, t: Type}, // alias + ), GenericType), ]}; diff --git a/structures/nominal.js b/structures/nominal.js index f92f089..b0105da 100644 --- a/structures/nominal.js +++ b/structures/nominal.js @@ -1,13 +1,76 @@ -import { Any } from "../typed.js"; -import { String } from "./list.js"; -import { sumType, prodType, fnType } from "./types.js"; +import { SymbolT, Type } from "../primitives/types.js"; +import { makeTypeConstructor } from "../type_constructor.js"; +import { Module, String } from "./list.js"; +import { prodType, fnType, lsType } from "./types.js"; +function capitalizeFirstLetter(val) { + return String(val).charAt(0).toUpperCase() + String(val).slice(1); +} -export const createNominalADT = symbol => variants => { - makeTypeConstructor(symbol, 0, ) +export const createStruct = (typeVars, symbol, fields) => { + const makeConstructor = (remainingFields, obj={}) => { + if (remainingFields.length===0) { + return obj; + } + const {left: fieldName} = remainingFields[remainingFields.length-1]; + return v => makeConstructor( + remainingFields.slice(0,-1), + Object.assign({[fieldName]: v}, obj)); + }; + const constructor = makeConstructor(fields); + + const type = makeTypeConstructor(symbol)(typeVars.size); + const types = [ type ]; + const recordFnType = inType => outType => { + const fnT = fnType(inType)(outType); + types.push(fnT); + return fnT; + } + + const makeConstructorType = (remainingFields, type) => { + if (remainingFields.length===0) { + return type; + } + const {right: fieldType} = remainingFields[remainingFields.length-1]; + return recordFnType(makeConstructorType(remainingFields.slice(0,-1)))(fieldType); + }; + const constructorType = makeConstructorType(fields); + + const functions = [ + ["constructor", constructor, constructorType], + ...fields.map(({left: fieldName, right: fieldType}) => { + const getterName = 'get'+capitalizeFirstLetter(fieldName); + const getter = { + // stupid trick to give the JS-function a computed name. + // only important for debugging, so it says [Function: getAge] instead of [Function (anonymous)]: + [getterName]: obj => obj[fieldName], + }[getterName]; + if (typeVars.has(fieldType)) { + // getterFnType = recordFnType(type)(fieldType) + } + const getterFnType = recordFnType(type)(fieldType); + return [fieldName, getter, getterFnType]; + }), + ]; + + const module = {l:[ + {i: type, t: Type}, + + ...functions.flatMap(([_, getter, getterFnType]) => [ + {i: getter , t: getterFnType}, + ]), + + ...types.map(type => ({i: type, t: Type})), + ]}; + + return { + module, + constructor, + functions: Object.fromEntries(functions), + }; }; -export const createNominalADTFnType = - fnType - (Any) - (); \ No newline at end of file +export const createNominalADTModuleFnType = + fnType(SymbolT) + (fnType(lsType(prodType(String)(Type))) + (Module)); diff --git a/structures/product.js b/structures/product.js index ebf577e..166738f 100644 --- a/structures/product.js +++ b/structures/product.js @@ -1,5 +1,5 @@ import { makeGeneric } from "../generics/generics.js"; -import { Type } from "../primitives/types.js"; +import { GenericType, Type } from "../primitives/types.js"; import { typedFnType } from "./types.js"; import { prodType } from "./types.js"; @@ -29,7 +29,7 @@ export const ModuleProduct = {l: [ (b) (prodType(a)(b)) ) - )), + ), GenericType), // (a, b) -> a ...typedFnType(getLeft, fnType => @@ -37,7 +37,7 @@ export const ModuleProduct = {l: [ fnType (prodType(a)(b)) (a) - )), + ), GenericType), // (a, b) -> b ...typedFnType(getRight, fnType => @@ -45,5 +45,5 @@ export const ModuleProduct = {l: [ fnType (prodType(a)(b)) (b) - )), + ), GenericType), ]}; diff --git a/structures/set.js b/structures/set.js index 48f52ea..dc7ac94 100644 --- a/structures/set.js +++ b/structures/set.js @@ -1,13 +1,14 @@ import { setType, typedFnType } from "./types.js"; -import { Bool, Type } from "../primitives/types.js"; +import { Bool, GenericType, Type } from "../primitives/types.js"; import { makeGeneric } from "../generics/generics.js"; // 'normal' implementation const emptySet = new Set(); +const emptySetType = makeGeneric(a => setType(a)); const has = set => elem => set.has(elem); const add = set => elem => new Set([...set, elem]); -export const ModuleList = {l:[ +export const ModuleSet = {l:[ // Type -> Type ...typedFnType(setType, fnType => fnType @@ -15,7 +16,8 @@ export const ModuleList = {l:[ /* out */ (Type) ), - {i: emptySet, t: makeGeneric(a => setType(a))}, + {i: emptySet , t: emptySetType}, + {i: emptySetType, t: GenericType }, ...typedFnType(has, fnType => makeGeneric(a => @@ -24,7 +26,7 @@ export const ModuleList = {l:[ /* out */ (fnType /* in */ (a) /* out */ (Bool) - ))), + )), GenericType), ...typedFnType(add, fnType => makeGeneric(a => @@ -33,6 +35,5 @@ export const ModuleList = {l:[ /* out */ (fnType /* in */ (a) /* out */ (setType(a)) - ))), - + )), GenericType), ]}; diff --git a/structures/sum.js b/structures/sum.js index b587c3e..e8136ca 100644 --- a/structures/sum.js +++ b/structures/sum.js @@ -1,5 +1,5 @@ import { prodType } from "./types.js"; -import { Type } from "../primitives/types.js"; +import { GenericType, Type } from "../primitives/types.js"; import { typedFnType } from "./types.js"; import { makeGeneric } from "../generics/generics.js"; import { sumType } from "./types.js"; @@ -31,7 +31,7 @@ export const ModuleSum = {l:[ fnType (a) (sumType(a)(b)) - )), + ), GenericType), // b -> a | b ...typedFnType(constructorRight, fnType => @@ -39,7 +39,7 @@ export const ModuleSum = {l:[ fnType (b) (sumType(a)(b)) - )), + ), GenericType), // a | b -> (a -> c, b-> c) -> c ...typedFnType(match, fnType => @@ -53,5 +53,5 @@ export const ModuleSum = {l:[ ) (c) ) - )), + ), GenericType), ]}; diff --git a/structures/types.js b/structures/types.js index 763b3c5..d8b0427 100644 --- a/structures/types.js +++ b/structures/types.js @@ -13,17 +13,8 @@ export const fnType = makeTypeConstructor(symbolFunction)(2); export const isFunction = type => getSymbol(type) === symbolFunction; -// Convenience function. Wrapper around function below. -export const typedFnType = (instance, callback) => { - const [t, typesOfFns] = typedFnType2(callback); - const res = [ - { i: instance, t }, - ...typesOfFns, - ]; - return res; -}; // Convenience function. Creates 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 => { +export const typedFnType = (instance, callback, typeOfType = Type) => { const fnTs = []; const wrappedFnType = inType => outType => { const fnT = fnType(inType)(outType); @@ -31,10 +22,15 @@ export const typedFnType2 = callback => { return fnT; }; const t = callback(wrappedFnType); // force evaluation - return [ - t, - fnTs.map(fnT => ({ i: fnT, t: Type })), + if (t.typeVars && typeOfType === Type) { + throw new Error("you probably meant to create a GenericType"); + } + const res = [ + { i: instance, t }, + { i: t , t: typeOfType }, + // ...fnTs.map(fnT => ({ i: fnT, t: Type })), ]; + return res; }; // Sum type @@ -57,14 +53,20 @@ export const lsType = makeTypeConstructor(symbolList)(1); const symbolSet = Symbol('Set'); export const setType = makeTypeConstructor(symbolSet)(1); + // Pretty print type export function prettyT(type) { // console.log("pretty:", type); + if (typeof type === "symbol") { + return type.description; + } if (type.typeVars) { if (type.typeVars.size > 0) { - return `∀${[...type.typeVars].map(prettyT).join(", ")}: ${prettyT(type.type)}`; + return `∀(${[...type.typeVars].map(prettyT).join(", ")}): ${prettyT(type.type)}`; + } + else { + return prettyT(type.type); } - return prettyT(type.type); } if (type.symbol === symbolFunction) { return `${prettyT(type.params[0])} -> ${prettyT(type.params[1])}`; diff --git a/structures/versioned.js b/structures/versioned.js index c683465..d177fb0 100644 --- a/structures/versioned.js +++ b/structures/versioned.js @@ -9,17 +9,22 @@ const symbolVersioned = Symbol("Versioned"); export const versionedType = makeTypeConstructor(symbolVersioned)(1); -export const constructor = parents => value => { +export const constructor = parents => alternatives => { return { parents, alternatives }; } const constructorType = makeGeneric(a => fnType - (a) (setType(versionedType(a))) + (fnType + (setType(a)) + (versionedType(a)) + ) ); -// const getValue = v => +const initial = x => ({ parents: new Set(), alternatives: new Set(x) }); + +const initialFnType = makeGeneric(a => fnType(a)(versionedType(a))); const eq = eqDict => vA => vB => { return getEq(eqDict)(vA.value,vB.value) // compare values diff --git a/type_constructor.js b/type_constructor.js index 291e4d1..52ed83b 100644 --- a/type_constructor.js +++ b/type_constructor.js @@ -1,6 +1,11 @@ import { DefaultMap } from "./util/defaultmap.js"; -const nullaryTypeConstructors = new DefaultMap(symbol => ({symbol, params: []})); // symbol -> 0-ary type constructor (= a type, basically) +const nullaryTypeConstructors = new DefaultMap( + // symbol -> 0-ary type constructor (= a type, basically) + symbol => ({ + symbol, + params: [], + })); const makeTypeConstructorInternal = (symbol, n_ary, params = []) => { // console.log("n_ary:", n_ary); diff --git a/typeclasses/eq.js b/typeclasses/eq.js index 42535cd..5aac1e2 100644 --- a/typeclasses/eq.js +++ b/typeclasses/eq.js @@ -1,5 +1,5 @@ import { makeGeneric } from "../generics/generics"; -import { SymbolT, Type, Unit } from "../primitives/types"; +import { GenericType, SymbolT, Type, Unit } from "../primitives/types"; import { typedFnType } from "../structures/types"; import { Bool, Byte, Char, Double, Int } from "../primitives/types"; import { deepEqual } from "../util/util"; @@ -22,7 +22,7 @@ export const ModuleEq = {l:[ (a) (Bool) ) - ))), + )), GenericType), ]}; // all our data (and types) are encoded such that we can test equality the same way: diff --git a/typed.js b/typed.js index 516e6ce..4542870 100644 --- a/typed.js +++ b/typed.js @@ -1,18 +1,22 @@ import { typedFnType } from "./structures/types.js"; import { Type } from "./primitives/types.js"; +import { makeTypeConstructor } from "./type_constructor.js"; // Everything is (implicitly) typed by the Any type. -export const Any = { symbol: Symbol('Any'), params: [] }; +const symbolAny = Symbol('Any'); +export const Any = makeTypeConstructor(symbolAny)(0); // A type-link, connecting a value to its Type. -export const Typed = { symbol: Symbol('Typed'), params: [] }; +const symbolTyped = Symbol('Typed'); +export const Typed = makeTypeConstructor(symbolTyped)(0); const getInst = lnk => lnk.i; const getType = lnk => lnk.t; export const ModuleTyped = {l:[ {i: Typed, t: Type}, + {i: Any , t: Type}, - ...typedFnType(getInst, fnType => fnType(Typed)(Type)), - ...typedFnType(getType, fnType => fnType(Typed)(Type)), + ...typedFnType(getInst, fnType => fnType(Typed)(Any)), + ...typedFnType(getType, fnType => fnType(Typed)(Any)), ]}; diff --git a/util/pretty.js b/util/pretty.js index d343a79..645f3fa 100644 --- a/util/pretty.js +++ b/util/pretty.js @@ -4,4 +4,3 @@ import { inspect } from 'node:util'; export function pretty(obj) { return inspect(obj, { colors: true, depth: null }); } -