This commit is contained in:
Joeri Exelmans 2025-03-24 17:28:07 +01:00
parent 6af72e525c
commit 145835ad5d
22 changed files with 153 additions and 90 deletions

View file

@ -8,7 +8,8 @@ import { pretty, zip } from "../util.js";
// makeGeneric(a => fnType({in: a, out: fnType({in: a, out: Bool})})) // makeGeneric(a => fnType({in: a, out: fnType({in: a, out: Bool})}))
export const makeGeneric = callback => { export const makeGeneric = callback => {
// type variables to make available: // type variables to make available:
const typeVars = ['a', 'b', 'c', 'd', 'e'].map(letter => ({ const typeVars = ['a', 'b', 'c', 'd', 'e'].map(
letter => ({
symbol: Symbol(letter), symbol: Symbol(letter),
params: [], params: [],
})); }));
@ -28,6 +29,7 @@ export const occurring = (type, typeVars) => {
return new Set(type.params.flatMap(p => [...occurring(p, typeVars)])); return new Set(type.params.flatMap(p => [...occurring(p, typeVars)]));
} }
// Merge 2 substitution-mappings, uni-directional.
const mergeOneWay = (m1, m2) => { const mergeOneWay = (m1, m2) => {
const m1copy = new Map(m1); const m1copy = new Map(m1);
const m2copy = new Map(m2); const m2copy = new Map(m2);
@ -41,6 +43,7 @@ const mergeOneWay = (m1, m2) => {
return [true, m1copy, m2copy, new Set()]; // stable return [true, m1copy, m2copy, new Set()]; // stable
} }
// Merge 2 substitution-mappings, bi-directional.
export const mergeTwoWay = (m1, m2) => { export const mergeTwoWay = (m1, m2) => {
// check for conflicts: // check for conflicts:
for (const [typeVar, actual] of m1) { for (const [typeVar, actual] of m1) {
@ -69,10 +72,12 @@ export const unify = (
{typeVars: formalTypeVars, type: formalType}, {typeVars: formalTypeVars, type: formalType},
{typeVars: actualTypeVars, type: actualType}, {typeVars: actualTypeVars, type: actualType},
) => { ) => {
// console.log("unify", {formalTypeVars, formalType, actualTypeVars, actualType}); // console.log("unify", pretty({formalTypeVars, formalType, actualTypeVars, actualType}));
if (formalTypeVars.has(formalType)) { if (formalTypeVars.has(formalType)) {
// simplest case: formalType is a type paramater // simplest case: formalType is a type paramater
// => substitute with actualType // => substitute with actualType
// console.log("assign actual to formal");
return { return {
substitutions: new Map([[formalType, actualType]]), substitutions: new Map([[formalType, actualType]]),
typeVars: new Set([ typeVars: new Set([
@ -84,6 +89,7 @@ export const unify = (
} }
if (actualTypeVars.has(actualType)) { if (actualTypeVars.has(actualType)) {
// same as above, but in the other direction // same as above, but in the other direction
// console.log("assign formal to actual");
return { return {
substitutions: new Map([[actualType, formalType]]), substitutions: new Map([[actualType, formalType]]),
typeVars: new Set([ typeVars: new Set([
@ -98,6 +104,7 @@ export const unify = (
throw new Error(`cannot unify ${pretty(formalType.symbol)} and ${pretty(actualType.symbol)}`); throw new Error(`cannot unify ${pretty(formalType.symbol)} and ${pretty(actualType.symbol)}`);
} }
else { 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 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) => { const [unifiedSubstitusions, deleted] = unifiedParams.reduce(([substitutionsSoFar, deletedSoFar], cur) => {
// console.log('merging', substitutionsSoFar, cur.substitutions); // console.log('merging', substitutionsSoFar, cur.substitutions);
@ -124,6 +131,12 @@ export const substitute = (type, substitutions) => {
// type IS a type var to be substituted: // type IS a type var to be substituted:
return substitutions.get(type); 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 { return {
symbol: type.symbol, symbol: type.symbol,
params: type.params.map(p => substitute(p, substitutions)), params: type.params.map(p => substitute(p, substitutions)),

View file

@ -1,41 +1,40 @@
import { Bool, Int } from "../primitives/types.js"; import { Bool, Int } from "../primitives/types.js";
import { lsType } from "../structures/types.js"; import { fnType, lsType } from "../structures/types.js";
import { fnType } from "../structures/function.js";
import { assign, makeGeneric, unify } from "./generics.js"; import { assign, makeGeneric, unify } from "./generics.js";
import { pretty } from "../util.js"; import { pretty } from "../util.js";
// a -> Int // a -> Int
const a_to_Int = makeGeneric(a => fnType({in: a, out: Int})); const a_to_Int = makeGeneric(a => fnType(a)(Int));
// Bool -> 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("should be: [Bool] -> Int")
console.log(pretty(unify(a_to_Int, Bool_to_Int))); console.log(pretty(unify(a_to_Int, Bool_to_Int)));
// (a -> a) -> b // (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 // (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("should be: (Bool -> Bool) -> a");
console.log(pretty(unify(fnType2, fnType3))); console.log(pretty(unify(fnType2, fnType3)));
// (a -> b) -> [a] -> [b] // (a -> b) -> [a] -> [b]
const mapFnType = makeGeneric((a,b) => const mapFnType = makeGeneric((a,b) =>
fnType({ fnType
in: fnType({in: a, out: b}), (fnType(a)(b))
out: fnType({in: lsType(a), out: lsType(b)}), (fnType(lsType(a))(lsType(b))))
}));
// a -> a // a -> a
const idFnType = makeGeneric(a => const idFnType = makeGeneric(a =>
fnType({in: a, out: a})); fnType(a)(a));
console.log("should be: [a] -> [a]"); console.log("should be: [a] -> [a]");
console.log(pretty(assign(mapFnType, idFnType))); console.log(pretty(assign(mapFnType, idFnType)));
// (a -> Int) -> [a] -> a // (a -> Int) -> [a] -> a
const weirdFnType = makeGeneric(a => const weirdFnType = makeGeneric(a =>
fnType({ fnType
in: fnType({in: a, out: Int}), (fnType(a)(Int))
out: fnType({in: lsType(a), out: a}), (fnType
})); (lsType(a))
(a)))
// we call this function with parameter of type (b -> b) ... // we call this function with parameter of type (b -> b) ...
// giving these substitutions: // giving these substitutions:
// a := b // a := b

View file

@ -1,4 +1,4 @@
import { Type } from "../type.js"; import { Type } from "../primitives/types.js";
import { typedFnType } from "../structures/types.js" import { typedFnType } from "../structures/types.js"
import { Double } from "../primitives/types.js"; import { Double } from "../primitives/types.js";

85
main.js
View file

@ -3,7 +3,8 @@ import { ModulePoint } from "./lib/point.js";
import { DefaultMap, pretty, prettyT } from './util.js'; import { DefaultMap, pretty, prettyT } from './util.js';
import { symbolFunction } from './structures/types.js'; import { symbolFunction } from './structures/types.js';
import { ModuleStd } from './stdlib.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 { class Context {
constructor(mod) { constructor(mod) {
@ -11,14 +12,14 @@ class Context {
this.types = new DefaultMap(() => new Set()); // instance to type this.types = new DefaultMap(() => new Set()); // instance to type
this.instances = new DefaultMap(() => new Set()); // type to instance 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) { for (const {i, t} of mod.l) {
this.types.getdefault(i, true).add(t); this.types.getdefault(i, true).add(t);
this.instances.getdefault(t, true).add(i); 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()) { for (const t of this.instances.m.keys()) {
if (t.symbol === symbolFunction) { if (t.symbol === symbolFunction) {
// 't' is a function signature // '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}) { addToCtx({i, t}) {
@ -38,13 +57,13 @@ class Context {
} }
} }
let ctx = new Context({l:[ let ctx = new Context({l:[
...ModuleStd.l, ...ModuleStd.l,
...ModulePoint.l, ...ModulePoint.l,
]}); ]});
const prettyIT = ({i, t}) => ({ const prettyIT = ({i, t}) => ({
strI: isType(i) ? prettyT(i) : pretty(i), strI: isType(i) ? prettyT(i) : pretty(i),
strT: prettyT(t), strT: prettyT(t),
@ -74,7 +93,11 @@ async function topPrompt() {
], ],
}); });
if (action === "list all types") { if (action === "list all types") {
await listAllTypes(); await listInstances(Type);
// await listAllTypes();
}
if (action === "list all functions") {
await listAllFunctions();
} }
if (action === "list all") { if (action === "list all") {
await listAllInstances(); await listAllInstances();
@ -82,22 +105,46 @@ async function topPrompt() {
return 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({ const choice = await select({
message: "select type:", message: "select function:",
choices: [ choices: [
"(go back)", "(go back)",
...[...ctx.instances.m.keys()].map(t => ({ ...[...ctx.types.m.entries()]
value: t, .filter(([i, types]) => {
name: prettyT(t), for (const type of types) {
})), if (type.symbol === symbolFunction)
] return true;
}
return false;
})
.flatMap(toChoices),
],
}); });
if (choice === "(go back)") { if (choice === "(go back)") {
return; return;
} }
await typeOptions(choice); const {i, t} = choice;
return listAllTypes(); await functionOptions(i, t);
return listAllFunctions();
} }
async function typeOptions(t) { async function typeOptions(t) {
@ -106,8 +153,9 @@ async function typeOptions(t) {
choices: [ choices: [
"(go back)", "(go back)",
"list instances", "list instances",
"list outgoing functions", // "list outgoing functions",
"list incoming functions", // "list incoming functions",
"print raw",
"treat as instance", "treat as instance",
], ],
}); });
@ -117,6 +165,9 @@ async function typeOptions(t) {
else if (choice === "list instances") { else if (choice === "list instances") {
await listInstances(t); await listInstances(t);
} }
else if (choice === "print raw") {
console.log(pretty(t));
}
else if (choice === "treat as instance") { else if (choice === "treat as instance") {
await instanceOptions(t, Type) await instanceOptions(t, Type)
} }

View file

@ -1,5 +1,5 @@
import { fnType, typedFnType } from "../structures/types.js"; import { fnType, typedFnType } from "../structures/types.js";
import { Type } from "../type.js"; import { Type } from "./types.js";
import { Bool } from "./types.js"; import { Bool } from "./types.js";
const eqBool = x => y => x === y; const eqBool = x => y => x === y;

View file

@ -1,5 +1,5 @@
import { typedFnType } from "../structures/types.js"; import { typedFnType } from "../structures/types.js";
import { Type } from "../type.js"; import { Type } from "./types.js";
import {Byte, Bool} from "./types.js"; import {Byte, Bool} from "./types.js";
const eqByte = x => y => x === y; const eqByte = x => y => x === y;

View file

@ -1,5 +1,5 @@
import { typedFnType } from "../structures/types.js"; import { typedFnType } from "../structures/types.js";
import { Type } from "../type.js"; import { Type } from "./types.js";
import {Char, Bool} from "./types.js"; import {Char, Bool} from "./types.js";
const eq = x => y => x === y; const eq = x => y => x === y;

View file

@ -1,5 +1,5 @@
import { typedFnType } from "../structures/types.js"; import { typedFnType } from "../structures/types.js";
import { Type } from "../type.js"; import { Type } from "./types.js";
import {Bool, Double} from "./types.js"; import {Bool, Double} from "./types.js";
export const addDouble = x => y => x + y; export const addDouble = x => y => x + y;

View file

@ -1,5 +1,5 @@
import { typedFnType } from "../structures/types.js"; import { typedFnType } from "../structures/types.js";
import { Type } from "../type.js"; import { Type } from "./types.js";
import {Bool, Int} from "./types.js"; import {Bool, Int} from "./types.js";

View file

@ -5,3 +5,5 @@ export const Bool = { symbol: Symbol('Bool') , params: [] };
export const Double = { symbol: Symbol('Double'), params: [] }; export const Double = { symbol: Symbol('Double'), params: [] };
export const Byte = { symbol: Symbol('Byte') , params: [] }; export const Byte = { symbol: Symbol('Byte') , params: [] };
export const Char = { symbol: Symbol('Char') , params: [] }; export const Char = { symbol: Symbol('Char') , params: [] };
export const Type = { symbol: Symbol('Type'), params: [] };

View file

@ -1,4 +1,4 @@
import { Type } from "../type.js"; import { Type } from "../primitives/types.js";
import { typedFnType } from "./types.js"; import { typedFnType } from "./types.js";
import { fnType } from "./types.js"; import { fnType } from "./types.js";

View file

@ -1,5 +1,5 @@
import { fnType, typedFnType } from "./types.js"; import { fnType, typedFnType } from "./types.js";
import { Type } from "../type.js"; import { Type } from "../primitives/types.js";
import { Int } from "../primitives/types.js"; import { Int } from "../primitives/types.js";
import { makeGeneric } from "../generics/generics.js"; import { makeGeneric } from "../generics/generics.js";
import { lsType } from "./types.js"; import { lsType } from "./types.js";

View file

@ -1,5 +1,5 @@
import { makeGeneric } from "../generics/generics.js"; import { makeGeneric } from "../generics/generics.js";
import { Type } from "../type.js"; import { Type } from "../primitives/types.js";
import { typedFnType } from "./types.js"; import { typedFnType } from "./types.js";
import { prodType } from "./types.js"; import { prodType } from "./types.js";

View file

@ -1,15 +1,15 @@
import { prodType } from "./types.js"; import { prodType } from "./types.js";
import { Type } from "../type.js"; import { Type } from "../primitives/types.js";
import { typedFnType } from "./types.js"; import { typedFnType } from "./types.js";
import { makeGeneric } from "../generics/generics.js"; import { makeGeneric } from "../generics/generics.js";
import { sumType } from "./types.js"; import { sumType } from "./types.js";
const constructorLeft = left => ({variant: "L", value: left }); export const constructorLeft = left => ({variant: "L", value: left });
const constructorRight = right => ({variant: "R", value: right}); export const constructorRight = right => ({variant: "R", value: right});
// signature: // signature:
// sum-type -> (leftType -> resultType, rightType -> resultType) -> resultType // 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.left(sum.value)
: handlers.right(sum.value); : handlers.right(sum.value);

View file

@ -1,6 +1,6 @@
// to break up dependency cycles, type constructors are defined in their own JS module // 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"; import { DefaultMap } from "../util.js";

View file

@ -1,11 +1,7 @@
import { Bool } from "./primitives/types.js"; import { Bool, Type } from "./primitives/types.js";
import { typedFnType } from "./structures/types.js"; import { typedFnType } from "./structures/types.js";
import { deepEqual } from "./util.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 getSymbol = type => type.symbol;
export const getParams = type => type.params; export const getParams = type => type.params;

View file

@ -1,5 +1,5 @@
import { makeGeneric } from "../generics/generics"; import { makeGeneric } from "../generics/generics";
import { Type } from "../type"; import { Type } from "../primitives/types";
import { typedFnType } from "../structures/types"; import { typedFnType } from "../structures/types";
import { Bool, Byte, Char, Double, Int } from "../primitives/types"; import { Bool, Byte, Char, Double, Int } from "../primitives/types";
import { deepEqual } from "../util"; import { deepEqual } from "../util";
@ -32,10 +32,10 @@ const eq = x => y => deepEqual(x,y);
const EqDict = {eq}; const EqDict = {eq};
export const EqInstances = new Map([ export const EqInstances = new Map([
[Int, EqDict], [Int , EqDict],
[Bool, EqDict], [Bool , EqDict],
[Double, EqDict], [Double, EqDict],
[Byte, EqDict], [Byte , EqDict],
[Char, EqDict], [Char , EqDict],
[Type, EqDict], [Type , EqDict],
]); ]);

View file

@ -1,7 +1,7 @@
import { makeGeneric } from "../generics/generics.js"; import { makeGeneric } from "../generics/generics.js";
import { addDouble, mulDouble } from "../primitives/double.js"; import { addDouble, mulDouble } from "../primitives/double.js";
import { addInt, mulInt } from "../primitives/int.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 { typedFnType, typedFnType2 } from "../structures/types.js";
import { Double, Int } from "../primitives/types.js"; import { Double, Int } from "../primitives/types.js";
import { numDictType } from "./num_type.js"; import { numDictType } from "./num_type.js";
@ -10,14 +10,15 @@ export const getAdd = numDict => numDict.add;
export const getMul = numDict => numDict.mul; export const getMul = numDict => numDict.mul;
// getAdd and getMul have same (generic) type: // getAdd and getMul have same (generic) type:
// NumDict a -> a -> a -> a
const [getAddMulFnType, typesOfFns] = typedFnType2(fnType => const [getAddMulFnType, typesOfFns] = typedFnType2(fnType =>
makeGeneric(a => fnType({ makeGeneric(a =>
in: numDictType(a), fnType
out: fnType({ (numDictType(a))
in: a, (fnType
out: fnType({in: a, out: a}), (a)
}), (fnType(a)(a))
}))); )));
export const ModuleNum = {l:[ export const ModuleNum = {l:[
...typedFnType(numDictType, fnType => fnType({in: Type, out: Type})), ...typedFnType(numDictType, fnType => fnType({in: Type, out: Type})),

View file

@ -1,29 +1,28 @@
import { assign } from "../generics/generics.js"; import { assign } from "../generics/generics.js";
import { makeGeneric } from "../generics/generics.js"; import { makeGeneric } from "../generics/generics.js";
import { fnType } from "../structures/function.js";
import { Double, Int } from "../primitives/types.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 { getMul, NumInstances } from "./num.js";
import { numDictType } from "./num_type.js"; import { numDictType } from "./num_type.js";
const square = numDict => x => getMul(numDict)(x)(x); const square = numDict => x => getMul(numDict)(x)(x);
// NumDict a -> a -> a
const squareFnType = makeGeneric(a => const squareFnType = makeGeneric(a =>
fnType({ fnType
in: numDictType(a), (numDictType(a))
out: fnType({ (fnType(a)(a))
in: a, );
out: a,
}),
}));
console.log("should be: Int -> Int"); 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("should be: Double -> Double");
console.log(assign(squareFnType, makeGeneric(() => numDictType(Double)))); console.log(pretty(assign(squareFnType, makeGeneric(() => numDictType(Double)))));
// to call 'square' we need: // to call 'square' we need:
// - the type of our argument (=Int)
// - access to a mapping from types to their typeclass instantiation // - access to a mapping from types to their typeclass instantiation
// - to know that our argument is 'Int'
console.log(""); console.log("");
console.log(square(NumInstances.get(Int))(42n)); // 1764n console.log(square(NumInstances.get(Int))(42n)); // 1764n

View file

@ -1 +0,0 @@
// const resolveTypeClass = (mapping, )

View file

@ -1,5 +1,5 @@
import { typedFnType } from "./structures/types.js"; 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. // Everything is (implicitly) typed by the Any type.
export const Any = { symbol: Symbol('Any'), params: [] }; export const Any = { symbol: Symbol('Any'), params: [] };

11
util.js
View file

@ -1,3 +1,6 @@
import { inspect } from 'node:util';
import { symbolFunction, symbolList, symbolProduct, symbolSum } from './structures/types.js';
// re-inventing the wheel: // re-inventing the wheel:
export function deepEqual(a, b) { export function deepEqual(a, b) {
if (a === b) return true; // <- shallow equality and primitives if (a === b) return true; // <- shallow equality and primitives
@ -45,18 +48,18 @@ export class DefaultMap {
} }
} }
import { inspect } from 'node:util';
import { symbolFunction, symbolList, symbolProduct, symbolSum } from './structures/types.js';
export function pretty(obj) { export function pretty(obj) {
return inspect(obj, {colors: true, depth: null}); return inspect(obj, {colors: true, depth: null});
} }
// Pretty print type
export function prettyT(type) { export function prettyT(type) {
if (type.typeVars) { 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)}`;
} }
return prettyT(type.type);
}
if (type.symbol === symbolFunction) { if (type.symbol === symbolFunction) {
return `${prettyT(type.params[0])} -> ${prettyT(type.params[1])}`; return `${prettyT(type.params[0])} -> ${prettyT(type.params[1])}`;
} }