From 145835ad5de531536035f2d75d3ce534c9db99e1 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Mon, 24 Mar 2025 17:28:07 +0100 Subject: [PATCH] progress --- generics/generics.js | 23 ++++++++--- generics/generics.test.js | 29 +++++++------ lib/point.js | 2 +- main.js | 85 +++++++++++++++++++++++++++++++-------- primitives/bool.js | 4 +- primitives/byte.js | 2 +- primitives/char.js | 2 +- primitives/double.js | 2 +- primitives/int.js | 2 +- primitives/types.js | 2 + structures/function.js | 2 +- structures/list.js | 2 +- structures/product.js | 2 +- structures/sum.js | 8 ++-- structures/types.js | 2 +- type.js | 6 +-- typeclasses/eq.js | 12 +++--- typeclasses/num.js | 19 ++++----- typeclasses/num.test.js | 21 +++++----- typeclasses/resolve.js | 1 - typed.js | 2 +- util.js | 13 +++--- 22 files changed, 153 insertions(+), 90 deletions(-) delete mode 100644 typeclasses/resolve.js diff --git a/generics/generics.js b/generics/generics.js index 7851964..5515b30 100644 --- a/generics/generics.js +++ b/generics/generics.js @@ -8,10 +8,11 @@ import { pretty, zip } from "../util.js"; // makeGeneric(a => fnType({in: a, out: fnType({in: a, out: 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( + letter => ({ + symbol: Symbol(letter), + params: [], + })); const type = callback(...typeVars); return { typeVars: occurring(type, new Set(typeVars)), @@ -28,6 +29,7 @@ export const occurring = (type, typeVars) => { 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); @@ -41,6 +43,7 @@ const mergeOneWay = (m1, m2) => { 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) { @@ -69,10 +72,12 @@ export const unify = ( {typeVars: formalTypeVars, type: formalType}, {typeVars: actualTypeVars, type: actualType}, ) => { - // console.log("unify", {formalTypeVars, formalType, actualTypeVars, actualType}); + // console.log("unify", pretty({formalTypeVars, formalType, actualTypeVars, actualType})); + if (formalTypeVars.has(formalType)) { // 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([ @@ -84,6 +89,7 @@ export const unify = ( } if (actualTypeVars.has(actualType)) { // same as above, but in the other direction + // console.log("assign formal to actual"); return { substitutions: new Map([[actualType, formalType]]), typeVars: new Set([ @@ -98,6 +104,7 @@ export const unify = ( throw new Error(`cannot unify ${pretty(formalType.symbol)} and ${pretty(actualType.symbol)}`); } 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); @@ -124,6 +131,12 @@ export const substitute = (type, substitutions) => { // 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; + } return { symbol: type.symbol, params: type.params.map(p => substitute(p, substitutions)), diff --git a/generics/generics.test.js b/generics/generics.test.js index 0917773..3054c13 100644 --- a/generics/generics.test.js +++ b/generics/generics.test.js @@ -1,41 +1,40 @@ import { Bool, Int } from "../primitives/types.js"; -import { lsType } from "../structures/types.js"; -import { fnType } from "../structures/function.js"; +import { fnType, lsType } from "../structures/types.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})); +const a_to_Int = makeGeneric(a => fnType(a)(Int)); // Bool -> Int -const Bool_to_Int = makeGeneric(() => fnType({in: lsType(Bool), out: 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))); // (a -> a) -> b -const fnType2 = makeGeneric((a,b) => fnType({in: fnType({in: a, out: a}), out: b})); +const fnType2 = makeGeneric((a,b) => fnType(fnType(a)(a))(b)); // (Bool -> Bool) -> a -const fnType3 = makeGeneric(a => fnType({in: fnType({in: Bool, out: Bool}), out: a})); +const fnType3 = makeGeneric(a => fnType(fnType(Bool)(Bool))(a)); console.log("should be: (Bool -> Bool) -> a"); console.log(pretty(unify(fnType2, fnType3))); // (a -> b) -> [a] -> [b] const mapFnType = makeGeneric((a,b) => - fnType({ - in: fnType({in: a, out: b}), - out: fnType({in: lsType(a), out: lsType(b)}), - })); + fnType + (fnType(a)(b)) + (fnType(lsType(a))(lsType(b)))) // a -> a const idFnType = makeGeneric(a => - fnType({in: a, out: a})); + fnType(a)(a)); console.log("should be: [a] -> [a]"); console.log(pretty(assign(mapFnType, idFnType))); // (a -> Int) -> [a] -> a const weirdFnType = makeGeneric(a => - fnType({ - in: fnType({in: a, out: Int}), - out: fnType({in: lsType(a), out: a}), - })); + fnType + (fnType(a)(Int)) + (fnType + (lsType(a)) + (a))) // we call this function with parameter of type (b -> b) ... // giving these substitutions: // a := b diff --git a/lib/point.js b/lib/point.js index dc8231a..300581b 100644 --- a/lib/point.js +++ b/lib/point.js @@ -1,4 +1,4 @@ -import { Type } from "../type.js"; +import { Type } from "../primitives/types.js"; import { typedFnType } from "../structures/types.js" import { Double } from "../primitives/types.js"; diff --git a/main.js b/main.js index 3f6efc6..0a3ef9c 100644 --- a/main.js +++ b/main.js @@ -3,7 +3,8 @@ import { ModulePoint } from "./lib/point.js"; import { DefaultMap, pretty, prettyT } from './util.js'; import { symbolFunction } from './structures/types.js'; import { ModuleStd } from './stdlib.js'; -import { Type } from './type.js'; +import { Type } from "./primitives/types.js"; +import { assign, makeGeneric, unify } from './generics/generics.js'; class Context { constructor(mod) { @@ -11,14 +12,14 @@ class Context { this.types = new DefaultMap(() => new Set()); // instance to type this.instances = new DefaultMap(() => new Set()); // type to instance - this.functionsFrom = new DefaultMap(() => new Set()); // type to outgoing function - this.functionsTo = new DefaultMap(() => new Set()); // type to incoming function - for (const {i, t} of mod.l) { this.types.getdefault(i, true).add(t); this.instances.getdefault(t, 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()) { if (t.symbol === symbolFunction) { // 't' is a function signature @@ -28,6 +29,24 @@ class Context { } } } + + // 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 + // } + // } + // } + // } } addToCtx({i, t}) { @@ -38,13 +57,13 @@ class Context { } } + let ctx = new Context({l:[ ...ModuleStd.l, ...ModulePoint.l, ]}); - const prettyIT = ({i, t}) => ({ strI: isType(i) ? prettyT(i) : pretty(i), strT: prettyT(t), @@ -74,7 +93,11 @@ async function topPrompt() { ], }); if (action === "list all types") { - await listAllTypes(); + await listInstances(Type); + // await listAllTypes(); + } + if (action === "list all functions") { + await listAllFunctions(); } if (action === "list all") { await listAllInstances(); @@ -82,22 +105,46 @@ async function topPrompt() { return topPrompt(); } -async function listAllTypes() { +// 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 type:", + message: "select function:", choices: [ "(go back)", - ...[...ctx.instances.m.keys()].map(t => ({ - value: t, - name: prettyT(t), - })), - ] + ...[...ctx.types.m.entries()] + .filter(([i, types]) => { + for (const type of types) { + if (type.symbol === symbolFunction) + return true; + } + return false; + }) + .flatMap(toChoices), + ], }); if (choice === "(go back)") { return; } - await typeOptions(choice); - return listAllTypes(); + const {i, t} = choice; + await functionOptions(i, t); + return listAllFunctions(); } async function typeOptions(t) { @@ -106,8 +153,9 @@ async function typeOptions(t) { choices: [ "(go back)", "list instances", - "list outgoing functions", - "list incoming functions", + // "list outgoing functions", + // "list incoming functions", + "print raw", "treat as instance", ], }); @@ -117,6 +165,9 @@ async function typeOptions(t) { else if (choice === "list instances") { await listInstances(t); } + else if (choice === "print raw") { + console.log(pretty(t)); + } else if (choice === "treat as instance") { await instanceOptions(t, Type) } diff --git a/primitives/bool.js b/primitives/bool.js index b4bd3a0..40d265a 100644 --- a/primitives/bool.js +++ b/primitives/bool.js @@ -1,5 +1,5 @@ import { fnType, typedFnType } from "../structures/types.js"; -import { Type } from "../type.js"; +import { Type } from "./types.js"; import { Bool } from "./types.js"; const eqBool = x => y => x === y; @@ -7,7 +7,7 @@ const eqBool = x => y => x === y; export const ModuleBool = {l:[ {i: true , t: Bool}, {i: false, t: Bool}, - + {i: Bool , t: Type}, // Bool -> Bool -> Bool diff --git a/primitives/byte.js b/primitives/byte.js index 8e35a1b..563e554 100644 --- a/primitives/byte.js +++ b/primitives/byte.js @@ -1,5 +1,5 @@ import { typedFnType } from "../structures/types.js"; -import { Type } from "../type.js"; +import { Type } from "./types.js"; import {Byte, Bool} from "./types.js"; const eqByte = x => y => x === y; diff --git a/primitives/char.js b/primitives/char.js index 1bbc9d2..11d355f 100644 --- a/primitives/char.js +++ b/primitives/char.js @@ -1,5 +1,5 @@ import { typedFnType } from "../structures/types.js"; -import { Type } from "../type.js"; +import { Type } from "./types.js"; import {Char, Bool} from "./types.js"; const eq = x => y => x === y; diff --git a/primitives/double.js b/primitives/double.js index fc85ce2..55ff842 100644 --- a/primitives/double.js +++ b/primitives/double.js @@ -1,5 +1,5 @@ import { typedFnType } from "../structures/types.js"; -import { Type } from "../type.js"; +import { Type } from "./types.js"; import {Bool, Double} from "./types.js"; export const addDouble = x => y => x + y; diff --git a/primitives/int.js b/primitives/int.js index 4b01c1a..c956e87 100644 --- a/primitives/int.js +++ b/primitives/int.js @@ -1,5 +1,5 @@ import { typedFnType } from "../structures/types.js"; -import { Type } from "../type.js"; +import { Type } from "./types.js"; import {Bool, Int} from "./types.js"; diff --git a/primitives/types.js b/primitives/types.js index adb5d3e..ce2ee79 100644 --- a/primitives/types.js +++ b/primitives/types.js @@ -5,3 +5,5 @@ 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: [] }; + +export const Type = { symbol: Symbol('Type'), params: [] }; diff --git a/structures/function.js b/structures/function.js index bfef251..fbde126 100644 --- a/structures/function.js +++ b/structures/function.js @@ -1,4 +1,4 @@ -import { Type } from "../type.js"; +import { Type } from "../primitives/types.js"; import { typedFnType } from "./types.js"; import { fnType } from "./types.js"; diff --git a/structures/list.js b/structures/list.js index e7c7901..c20e8ce 100644 --- a/structures/list.js +++ b/structures/list.js @@ -1,5 +1,5 @@ import { fnType, typedFnType } from "./types.js"; -import { Type } from "../type.js"; +import { Type } from "../primitives/types.js"; import { Int } from "../primitives/types.js"; import { makeGeneric } from "../generics/generics.js"; import { lsType } from "./types.js"; diff --git a/structures/product.js b/structures/product.js index 2718cc7..ebf577e 100644 --- a/structures/product.js +++ b/structures/product.js @@ -1,5 +1,5 @@ import { makeGeneric } from "../generics/generics.js"; -import { Type } from "../type.js"; +import { Type } from "../primitives/types.js"; import { typedFnType } from "./types.js"; import { prodType } from "./types.js"; diff --git a/structures/sum.js b/structures/sum.js index c41ef46..b587c3e 100644 --- a/structures/sum.js +++ b/structures/sum.js @@ -1,15 +1,15 @@ import { prodType } from "./types.js"; -import { Type } from "../type.js"; +import { Type } from "../primitives/types.js"; import { typedFnType } from "./types.js"; import { makeGeneric } from "../generics/generics.js"; import { sumType } from "./types.js"; -const constructorLeft = left => ({variant: "L", value: left }); -const constructorRight = right => ({variant: "R", value: right}); +export const constructorLeft = left => ({variant: "L", value: left }); +export const constructorRight = right => ({variant: "R", value: right}); // signature: // sum-type -> (leftType -> resultType, rightType -> resultType) -> resultType -const match = sum => handlers => sum.variant === "L" +export const match = sum => handlers => sum.variant === "L" ? handlers.left(sum.value) : handlers.right(sum.value); diff --git a/structures/types.js b/structures/types.js index 1aefe60..3e7056c 100644 --- a/structures/types.js +++ b/structures/types.js @@ -1,6 +1,6 @@ // to break up dependency cycles, type constructors are defined in their own JS module -import { Type } from "../type.js"; +import { Type } from "../primitives/types.js"; import { DefaultMap } from "../util.js"; diff --git a/type.js b/type.js index 7528535..4d13737 100644 --- a/type.js +++ b/type.js @@ -1,11 +1,7 @@ -import { Bool } from "./primitives/types.js"; +import { Bool, Type } from "./primitives/types.js"; import { typedFnType } from "./structures/types.js"; import { deepEqual } from "./util.js"; -// 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: [] }; - export const getSymbol = type => type.symbol; export const getParams = type => type.params; diff --git a/typeclasses/eq.js b/typeclasses/eq.js index 9256e6a..14752a1 100644 --- a/typeclasses/eq.js +++ b/typeclasses/eq.js @@ -1,5 +1,5 @@ import { makeGeneric } from "../generics/generics"; -import { Type } from "../type"; +import { Type } from "../primitives/types"; import { typedFnType } from "../structures/types"; import { Bool, Byte, Char, Double, Int } from "../primitives/types"; import { deepEqual } from "../util"; @@ -32,10 +32,10 @@ const eq = x => y => deepEqual(x,y); const EqDict = {eq}; export const EqInstances = new Map([ - [Int, EqDict], - [Bool, EqDict], + [Int , EqDict], + [Bool , EqDict], [Double, EqDict], - [Byte, EqDict], - [Char, EqDict], - [Type, EqDict], + [Byte , EqDict], + [Char , EqDict], + [Type , EqDict], ]); diff --git a/typeclasses/num.js b/typeclasses/num.js index 2418071..eaf288c 100644 --- a/typeclasses/num.js +++ b/typeclasses/num.js @@ -1,7 +1,7 @@ import { makeGeneric } from "../generics/generics.js"; import { addDouble, mulDouble } from "../primitives/double.js"; import { addInt, mulInt } from "../primitives/int.js"; -import { Type } from "../type.js"; +import { Type } from "../primitives/types.js"; import { typedFnType, typedFnType2 } from "../structures/types.js"; import { Double, Int } from "../primitives/types.js"; import { numDictType } from "./num_type.js"; @@ -10,14 +10,15 @@ export const getAdd = numDict => numDict.add; export const getMul = numDict => numDict.mul; // getAdd and getMul have same (generic) type: +// NumDict a -> a -> a -> a const [getAddMulFnType, typesOfFns] = typedFnType2(fnType => - makeGeneric(a => fnType({ - in: numDictType(a), - out: fnType({ - in: a, - out: fnType({in: a, out: a}), - }), - }))); + makeGeneric(a => + fnType + (numDictType(a)) + (fnType + (a) + (fnType(a)(a)) + ))); export const ModuleNum = {l:[ ...typedFnType(numDictType, fnType => fnType({in: Type, out: Type})), @@ -51,4 +52,4 @@ export const ModuleNumInstances = {l:[ export const NumInstances = new Map([ [Int , IntNumDict ], [Double, DoubleNumDict], -]); \ No newline at end of file +]); diff --git a/typeclasses/num.test.js b/typeclasses/num.test.js index a310c07..c0ffcbc 100644 --- a/typeclasses/num.test.js +++ b/typeclasses/num.test.js @@ -1,29 +1,28 @@ import { assign } from "../generics/generics.js"; import { makeGeneric } from "../generics/generics.js"; -import { fnType } from "../structures/function.js"; import { Double, Int } from "../primitives/types.js"; +import { fnType } from "../structures/types.js"; +import { pretty } from "../util.js"; import { getMul, NumInstances } from "./num.js"; import { numDictType } from "./num_type.js"; const square = numDict => x => getMul(numDict)(x)(x); +// NumDict a -> a -> a const squareFnType = makeGeneric(a => - fnType({ - in: numDictType(a), - out: fnType({ - in: a, - out: a, - }), - })); + fnType + (numDictType(a)) + (fnType(a)(a)) + ); console.log("should be: Int -> Int"); -console.log(assign(squareFnType, makeGeneric(() => numDictType(Int)))); +console.log(pretty(assign(squareFnType, makeGeneric(() => numDictType(Int))))); console.log("should be: Double -> Double"); -console.log(assign(squareFnType, makeGeneric(() => numDictType(Double)))); +console.log(pretty(assign(squareFnType, makeGeneric(() => numDictType(Double))))); // to call 'square' we need: +// - the type of our argument (=Int) // - access to a mapping from types to their typeclass instantiation -// - to know that our argument is 'Int' console.log(""); console.log(square(NumInstances.get(Int))(42n)); // 1764n diff --git a/typeclasses/resolve.js b/typeclasses/resolve.js deleted file mode 100644 index 8aaf7c0..0000000 --- a/typeclasses/resolve.js +++ /dev/null @@ -1 +0,0 @@ -// const resolveTypeClass = (mapping, ) \ No newline at end of file diff --git a/typed.js b/typed.js index 843a731..516e6ce 100644 --- a/typed.js +++ b/typed.js @@ -1,5 +1,5 @@ import { typedFnType } from "./structures/types.js"; -import { Type } from "./type.js"; +import { Type } from "./primitives/types.js"; // Everything is (implicitly) typed by the Any type. export const Any = { symbol: Symbol('Any'), params: [] }; diff --git a/util.js b/util.js index 6146ad8..c8c7214 100644 --- a/util.js +++ b/util.js @@ -1,3 +1,6 @@ +import { inspect } from 'node:util'; +import { symbolFunction, symbolList, symbolProduct, symbolSum } from './structures/types.js'; + // re-inventing the wheel: export function deepEqual(a, b) { if (a === b) return true; // <- shallow equality and primitives @@ -45,17 +48,17 @@ export class DefaultMap { } } - -import { inspect } from 'node:util'; -import { symbolFunction, symbolList, symbolProduct, symbolSum } from './structures/types.js'; - export function pretty(obj) { return inspect(obj, {colors: true, depth: null}); } +// Pretty print type export function prettyT(type) { if (type.typeVars) { - return `∀${[...type.typeVars].map(prettyT).join(", ")}: ${prettyT(type.type)}`; + if (type.typeVars.size > 0) { + return `∀${[...type.typeVars].map(prettyT).join(", ")}: ${prettyT(type.type)}`; + } + return prettyT(type.type); } if (type.symbol === symbolFunction) { return `${prettyT(type.params[0])} -> ${prettyT(type.params[1])}`;