kind-adt 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COMMERCIAL_LICENSE.md +25 -0
- package/LICENSE +373 -0
- package/README.md +627 -0
- package/index.d.ts +1455 -0
- package/index.js +346 -0
- package/package.json +36 -0
- package/utils.d.ts +57 -0
- package/utils.js +172 -0
package/index.js
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate constructors and match functions for an ADT.
|
|
3
|
+
* @param {Array<string>} [variants] The variants of the ADT. If not provided, a proxy object will
|
|
4
|
+
* be returned.
|
|
5
|
+
* @returns {*}
|
|
6
|
+
*/
|
|
7
|
+
export function make(variants) {
|
|
8
|
+
/* Guard */
|
|
9
|
+
const createGuard = (tag) =>
|
|
10
|
+
renameFunction((adt) => adt._tag === tag, `is${tag}`);
|
|
11
|
+
|
|
12
|
+
/* Conditional deconstructor */
|
|
13
|
+
const createConditionalDeconstructor = (tag) =>
|
|
14
|
+
renameFunction((adt, onMatch, otherwise) => {
|
|
15
|
+
if (adt._tag === tag) return onMatch(...unwrap(adt));
|
|
16
|
+
if (otherwise) return otherwise(adt);
|
|
17
|
+
}, `if${tag}`);
|
|
18
|
+
|
|
19
|
+
/* Deconstructor */
|
|
20
|
+
const createDeconstructor = (tag) =>
|
|
21
|
+
renameFunction((adt) => {
|
|
22
|
+
if (adt == null || adt._tag !== tag)
|
|
23
|
+
throw new TypeError(
|
|
24
|
+
`Expected \`${tag}(...)\`, but got \`${stringify(adt)}\``,
|
|
25
|
+
);
|
|
26
|
+
return unwrap(adt);
|
|
27
|
+
}, `unwrap${tag}`);
|
|
28
|
+
|
|
29
|
+
/* Constructor */
|
|
30
|
+
const createConstructor = (tag) =>
|
|
31
|
+
Object.assign(
|
|
32
|
+
renameFunction((...args) => {
|
|
33
|
+
const result = { _tag: tag };
|
|
34
|
+
for (let i = 0; i < args.length; i++) result["_" + i] = args[i];
|
|
35
|
+
return result;
|
|
36
|
+
}, tag),
|
|
37
|
+
{
|
|
38
|
+
_tag: tag,
|
|
39
|
+
toJSON: () => ({ _tag: tag }),
|
|
40
|
+
[Symbol.for("nodejs.util.inspect.custom")]: () => ({ _tag: tag }),
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const result = {
|
|
45
|
+
unwrap:
|
|
46
|
+
variants ?
|
|
47
|
+
function unwrap(adt) {
|
|
48
|
+
if (adt == null || variants.indexOf(adt._tag) === -1)
|
|
49
|
+
throw new TypeError(
|
|
50
|
+
`Expected ${variants.map((tag) => "`" + tag + "(...)`").join("/")}, but got \`${stringify(adt)}\``,
|
|
51
|
+
);
|
|
52
|
+
return entriesOf(adt)
|
|
53
|
+
.filter(
|
|
54
|
+
([key]) =>
|
|
55
|
+
key.startsWith("_") && !Object.is(Number(key.slice(1)), NaN),
|
|
56
|
+
)
|
|
57
|
+
.sort(([a], [b]) => Number(a.slice(1)) - Number(b.slice(1)))
|
|
58
|
+
.map(([, value]) => value);
|
|
59
|
+
}
|
|
60
|
+
: unwrap,
|
|
61
|
+
|
|
62
|
+
match:
|
|
63
|
+
variants ?
|
|
64
|
+
function match(adt, cases) {
|
|
65
|
+
if (!cases) {
|
|
66
|
+
const cases = adt;
|
|
67
|
+
return function match(adt) {
|
|
68
|
+
if (adt == null || variants.indexOf(adt._tag) === -1)
|
|
69
|
+
throw new TypeError(
|
|
70
|
+
`Expected ${variants.map((tag) => "`" + tag + "(...)`").join("/")}, but got \`${stringify(adt)}\``,
|
|
71
|
+
);
|
|
72
|
+
return _match(adt, cases);
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (adt == null || variants.indexOf(adt._tag) === -1)
|
|
76
|
+
throw new TypeError(
|
|
77
|
+
`Expected ${variants.map((tag) => "`" + tag + "(...)`").join("/")}, but got \`${stringify(adt)}\``,
|
|
78
|
+
);
|
|
79
|
+
return _match(adt, cases);
|
|
80
|
+
}
|
|
81
|
+
: function match(adt, cases) {
|
|
82
|
+
if (!cases) {
|
|
83
|
+
const cases = adt;
|
|
84
|
+
return function match(adt) {
|
|
85
|
+
return _match(adt, cases);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return _match(adt, cases);
|
|
89
|
+
},
|
|
90
|
+
matchW:
|
|
91
|
+
variants ?
|
|
92
|
+
function matchW(adt, cases) {
|
|
93
|
+
if (!cases) {
|
|
94
|
+
const cases = adt;
|
|
95
|
+
return function matchW(adt) {
|
|
96
|
+
if (adt == null || variants.indexOf(adt._tag) === -1)
|
|
97
|
+
throw new TypeError(
|
|
98
|
+
`Expected ${variants.map((tag) => "`" + tag + "(...)`").join("/")}, but got \`${stringify(adt)}\``,
|
|
99
|
+
);
|
|
100
|
+
return _match(adt, cases);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (adt == null || variants.indexOf(adt._tag) === -1)
|
|
104
|
+
throw new TypeError(
|
|
105
|
+
`Expected ${variants.map((tag) => "`" + tag + "(...)`").join("/")}, but got \`${stringify(adt)}\``,
|
|
106
|
+
);
|
|
107
|
+
return _match(adt, cases);
|
|
108
|
+
}
|
|
109
|
+
: function matchW(adt, cases) {
|
|
110
|
+
if (!cases) {
|
|
111
|
+
const cases = adt;
|
|
112
|
+
return function matchW(adt) {
|
|
113
|
+
return _match(adt, cases);
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return _match(adt, cases);
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
if (!variants)
|
|
121
|
+
return new Proxy(result, {
|
|
122
|
+
get(target, prop, receiver) {
|
|
123
|
+
if (typeof prop !== "string" || prop in target)
|
|
124
|
+
return Reflect.get(target, prop, receiver);
|
|
125
|
+
|
|
126
|
+
/* Guard */
|
|
127
|
+
if (matchesPrefix(prop, "is")) {
|
|
128
|
+
const tag = prop.slice(2);
|
|
129
|
+
return createGuard(tag);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* Conditional deconstructor */
|
|
133
|
+
if (matchesPrefix(prop, "if")) {
|
|
134
|
+
const tag = prop.slice(2);
|
|
135
|
+
return createConditionalDeconstructor(tag);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* Deconstructor */
|
|
139
|
+
if (matchesPrefix(prop, "unwrap")) {
|
|
140
|
+
const tag = prop.slice(6);
|
|
141
|
+
return createDeconstructor(tag);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* Constructor */
|
|
145
|
+
return createConstructor(prop);
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
for (const tag of variants) {
|
|
150
|
+
/* Constructor */
|
|
151
|
+
result[tag] = createConstructor(tag);
|
|
152
|
+
|
|
153
|
+
/* Guard */
|
|
154
|
+
result[`is${tag}`] = createGuard(tag);
|
|
155
|
+
|
|
156
|
+
/* Conditional deconstructor */
|
|
157
|
+
result[`if${tag}`] = createConditionalDeconstructor(tag);
|
|
158
|
+
|
|
159
|
+
/* Deconstructor */
|
|
160
|
+
result[`unwrap${tag}`] = createDeconstructor(tag);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Extract the fields of an ADT.
|
|
168
|
+
* @param {*} adt The ADT to unwrap.
|
|
169
|
+
* @returns {Array}
|
|
170
|
+
*/
|
|
171
|
+
export function unwrap(adt) {
|
|
172
|
+
return entriesOf(adt)
|
|
173
|
+
.filter(
|
|
174
|
+
([key]) => key.startsWith("_") && !Object.is(Number(key.slice(1)), NaN),
|
|
175
|
+
)
|
|
176
|
+
.sort(([a], [b]) => Number(a.slice(1)) - Number(b.slice(1)))
|
|
177
|
+
.map(([, value]) => value);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Match an ADT with the provided cases.
|
|
182
|
+
* @private
|
|
183
|
+
*
|
|
184
|
+
* @param {*} adt The ADT to match.
|
|
185
|
+
* @param {Object<string, Function>} cases The cases to match.
|
|
186
|
+
* @returns {*}
|
|
187
|
+
*/
|
|
188
|
+
function _match(adt, cases) {
|
|
189
|
+
if (adt != null && cases[adt._tag]) return cases[adt._tag](...unwrap(adt));
|
|
190
|
+
if (cases._) return cases._(adt);
|
|
191
|
+
throw new Error(
|
|
192
|
+
`No case found for \`${stringify(adt)}\`. Consider adding a catch-all case (\`_\`) if needed`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/*********************
|
|
197
|
+
* Utility functions *
|
|
198
|
+
*********************/
|
|
199
|
+
/**
|
|
200
|
+
* Check if a name matches a prefix.
|
|
201
|
+
*
|
|
202
|
+
* A prefix is a string that is at the beginning of another string followed by a capital letter.
|
|
203
|
+
* @private
|
|
204
|
+
*
|
|
205
|
+
* @param {string} str The name to check.
|
|
206
|
+
* @param {string} prefix The prefix to match.
|
|
207
|
+
* @returns {boolean}
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```javascript
|
|
211
|
+
* matchesPrefix("isFoo", "is"); // => true
|
|
212
|
+
* matchesPrefix("isfoo", "is"); // => false
|
|
213
|
+
* matchesPrefix("is", "is"); // => false
|
|
214
|
+
* matchesPrefix("is你好", "is"); // => true
|
|
215
|
+
* ```
|
|
216
|
+
*/
|
|
217
|
+
function matchesPrefix(str, prefix) {
|
|
218
|
+
return (
|
|
219
|
+
str.startsWith(prefix) &&
|
|
220
|
+
str.length > prefix.length &&
|
|
221
|
+
str[prefix.length] === str[prefix.length].toUpperCase()
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/****************************
|
|
226
|
+
* Common utility functions *
|
|
227
|
+
****************************/
|
|
228
|
+
/**
|
|
229
|
+
* Get the entries of an object.
|
|
230
|
+
* @private
|
|
231
|
+
*
|
|
232
|
+
* @param {*} obj The object to get the entries from.
|
|
233
|
+
* @returns {Array}
|
|
234
|
+
*/
|
|
235
|
+
function entriesOf(obj) {
|
|
236
|
+
const entries = [];
|
|
237
|
+
for (const key in obj)
|
|
238
|
+
if (Object.prototype.hasOwnProperty.call(obj, key))
|
|
239
|
+
entries.push([key, obj[key]]);
|
|
240
|
+
return entries;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Change the name of a function for better debugging experience.
|
|
245
|
+
* @private
|
|
246
|
+
*
|
|
247
|
+
* @template {Function} F
|
|
248
|
+
* @param {F} fn The function to rename.
|
|
249
|
+
* @param {string} name The new name of the function.
|
|
250
|
+
* @returns {F}
|
|
251
|
+
*/
|
|
252
|
+
function renameFunction(fn, name) {
|
|
253
|
+
return Object.defineProperty(fn, "name", {
|
|
254
|
+
value: name,
|
|
255
|
+
configurable: true,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Stringify a value to provide better debugging experience, handling common cases that simple
|
|
261
|
+
* `JSON.stringify` does not handle, e.g., `undefined`, `bigint`, `function`, `symbol`, `Date`.
|
|
262
|
+
* Circular references are considered.
|
|
263
|
+
*
|
|
264
|
+
* This is a simple port of the [showify](https://github.com/Snowflyt/showify/blob/7759b8778d54f686c85eba4d88b2dac2afdbcdd6/packages/lite/src/index.ts)
|
|
265
|
+
* package, which is a library for stringifying objects in a human-readable way.
|
|
266
|
+
* @param {*} value The object to stringify.
|
|
267
|
+
* @returns {string}
|
|
268
|
+
*/
|
|
269
|
+
const stringify = (value) => {
|
|
270
|
+
const identifierRegex = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
271
|
+
|
|
272
|
+
const serialize = (value, /** @type {Array} */ ancestors) => {
|
|
273
|
+
if (typeof value === "bigint") return `${value}n`;
|
|
274
|
+
if (typeof value === "function")
|
|
275
|
+
return value.name ?
|
|
276
|
+
`[Function: ${value.name}]`
|
|
277
|
+
: "[Function (anonymous)]";
|
|
278
|
+
if (typeof value === "symbol") return value.toString();
|
|
279
|
+
if (value === undefined) return "undefined";
|
|
280
|
+
if (value === null) return "null";
|
|
281
|
+
|
|
282
|
+
if (typeof value === "object") {
|
|
283
|
+
if (ancestors.indexOf(value) !== -1) return "[Circular]";
|
|
284
|
+
const nextAncestors = ancestors.concat([value]);
|
|
285
|
+
|
|
286
|
+
// Handle special object types
|
|
287
|
+
if (value instanceof Date) return value.toISOString();
|
|
288
|
+
|
|
289
|
+
if (value instanceof RegExp) return value.toString();
|
|
290
|
+
|
|
291
|
+
if (value instanceof Map) {
|
|
292
|
+
const entries = Array.from(value.entries())
|
|
293
|
+
.map(
|
|
294
|
+
([k, v]) =>
|
|
295
|
+
`${serialize(k, nextAncestors)} => ${serialize(v, nextAncestors)}`,
|
|
296
|
+
)
|
|
297
|
+
.join(", ");
|
|
298
|
+
return `Map(${value.size}) ` + (entries ? `{ ${entries} }` : "{}");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (value instanceof Set) {
|
|
302
|
+
const values = Array.from(value)
|
|
303
|
+
.map((v) => serialize(v, nextAncestors))
|
|
304
|
+
.join(", ");
|
|
305
|
+
return `Set(${value.size}) ` + (values ? `{ ${values} }` : "{}");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Handle arrays and objects
|
|
309
|
+
const isClassInstance =
|
|
310
|
+
value.constructor &&
|
|
311
|
+
value.constructor.name &&
|
|
312
|
+
value.constructor.name !== "Object";
|
|
313
|
+
const className = isClassInstance ? value.constructor.name : "";
|
|
314
|
+
|
|
315
|
+
if (Array.isArray(value)) {
|
|
316
|
+
const arrayItems = value
|
|
317
|
+
.map((item) => serialize(item, nextAncestors))
|
|
318
|
+
.join(", ");
|
|
319
|
+
let result = `[${arrayItems}]`;
|
|
320
|
+
if (className !== "Array")
|
|
321
|
+
result = `${className}(${value.length}) ${result}`;
|
|
322
|
+
return result;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const objectEntries = Reflect.ownKeys(value)
|
|
326
|
+
.map((key) => {
|
|
327
|
+
const keyDisplay =
|
|
328
|
+
typeof key === "symbol" ? `[${key.toString()}]`
|
|
329
|
+
: identifierRegex.test(key) ? key
|
|
330
|
+
: JSON.stringify(key);
|
|
331
|
+
const val = value[key];
|
|
332
|
+
return `${keyDisplay}: ${serialize(val, nextAncestors)}`;
|
|
333
|
+
})
|
|
334
|
+
.join(", ");
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
(className ? `${className} ` : "") +
|
|
338
|
+
(objectEntries ? `{ ${objectEntries} }` : "{}")
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return JSON.stringify(value);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
return serialize(value, []);
|
|
346
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kind-adt",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "🪴 The kind of ADTs you can count on in TypeScript",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"typescript",
|
|
7
|
+
"type safe",
|
|
8
|
+
"ADT",
|
|
9
|
+
"algebraic data type",
|
|
10
|
+
"pattern matching",
|
|
11
|
+
"HKT",
|
|
12
|
+
"higher kinded type"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/Snowflyt/kind-adt",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/Snowflyt/kind-adt/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/Snowflyt/kind-adt"
|
|
21
|
+
},
|
|
22
|
+
"license": "MPL-2.0",
|
|
23
|
+
"author": "Ge Gao (Snowflyt) <gaoge011022@gmail.com>",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "./index.js",
|
|
26
|
+
"module": "./index.js",
|
|
27
|
+
"types": "./index.d.ts",
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"showify": ">=0.1"
|
|
30
|
+
},
|
|
31
|
+
"peerDependenciesMeta": {
|
|
32
|
+
"showify": {
|
|
33
|
+
"optional": true
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
package/utils.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ShowOptions } from "showify";
|
|
2
|
+
/**
|
|
3
|
+
* Print arguments including (possibly) ADTs to `stdout` with newline.
|
|
4
|
+
* @param args The arguments to print.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* // Suppose we have an ADT `data Tree<T> = Empty | Node(T, Tree<T>, Tree<T>)`
|
|
9
|
+
* const tree = Tree.Node(
|
|
10
|
+
* 1,
|
|
11
|
+
* Tree.Node(2, Tree.Node(3, Tree.Empty, Tree.Empty), Tree.Empty),
|
|
12
|
+
* Tree.Node(4, Tree.Empty, Tree.Node(3, Tree.Empty, Tree.Empty)),
|
|
13
|
+
* );
|
|
14
|
+
*
|
|
15
|
+
* // ANSI colors are supported
|
|
16
|
+
* println(tree);
|
|
17
|
+
* // Node(
|
|
18
|
+
* // 1,
|
|
19
|
+
* // Node(2, Node(3, Empty, Empty), Empty),
|
|
20
|
+
* // Node(4, Empty, Node(3, Empty, Empty))
|
|
21
|
+
* // )
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @see {@linkcode show} if you want more control over the output.
|
|
25
|
+
*/
|
|
26
|
+
export declare function println(...args: unknown[]): void;
|
|
27
|
+
/**
|
|
28
|
+
* Stringify a (possibly) ADT to human-readable format.
|
|
29
|
+
* @param value The value to stringify.
|
|
30
|
+
* @param options The options for stringification.
|
|
31
|
+
* @returns
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* // Suppose we have an ADT `data Tree<T> = Empty | Node(T, Tree<T>, Tree<T>)`
|
|
36
|
+
* const tree = Tree.Node(
|
|
37
|
+
* 1,
|
|
38
|
+
* Tree.Node(2, Tree.Empty, Tree.Empty),
|
|
39
|
+
* Tree.Empty,
|
|
40
|
+
* );
|
|
41
|
+
*
|
|
42
|
+
* console.log(show(tree));
|
|
43
|
+
* // Node(1, Node(2, Empty, Empty), Empty)
|
|
44
|
+
*
|
|
45
|
+
* console.log(show(tree, { indent: 4, breakLength: 0, trailingComma: "auto" }));
|
|
46
|
+
* // Node(
|
|
47
|
+
* // 1,
|
|
48
|
+
* // Node(
|
|
49
|
+
* // 2,
|
|
50
|
+
* // Empty,
|
|
51
|
+
* // Empty,
|
|
52
|
+
* // ),
|
|
53
|
+
* // Empty,
|
|
54
|
+
* // )
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export declare function show(value: unknown, options?: ShowOptions): string;
|
package/utils.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { Node as SerializerNode, serializer, show as stringify } from "showify";
|
|
2
|
+
|
|
3
|
+
import { unwrap } from ".";
|
|
4
|
+
|
|
5
|
+
const { between, pair, sequence, text, variant } = SerializerNode;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Print arguments including (possibly) ADTs to `stdout` with newline.
|
|
9
|
+
* @param args The arguments to print.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* // Suppose we have an ADT `data Tree<T> = Empty | Node(T, Tree<T>, Tree<T>)`
|
|
14
|
+
* const tree = Tree.Node(
|
|
15
|
+
* 1,
|
|
16
|
+
* Tree.Node(2, Tree.Node(3, Tree.Empty, Tree.Empty), Tree.Empty),
|
|
17
|
+
* Tree.Node(4, Tree.Empty, Tree.Node(3, Tree.Empty, Tree.Empty)),
|
|
18
|
+
* );
|
|
19
|
+
*
|
|
20
|
+
* // ANSI colors are supported
|
|
21
|
+
* println(tree);
|
|
22
|
+
* // Node(
|
|
23
|
+
* // 1,
|
|
24
|
+
* // Node(2, Node(3, Empty, Empty), Empty),
|
|
25
|
+
* // Node(4, Empty, Node(3, Empty, Empty))
|
|
26
|
+
* // )
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @see {@linkcode show} if you want more control over the output.
|
|
30
|
+
*/
|
|
31
|
+
export function println(...args) {
|
|
32
|
+
getConsole().log(
|
|
33
|
+
...args.map((arg) =>
|
|
34
|
+
typeof arg === "string" ? arg : show(arg, { colors: true, indent: 2 }),
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Stringify a (possibly) ADT to human-readable format.
|
|
41
|
+
* @param value The value to stringify.
|
|
42
|
+
* @param options The options for stringification.
|
|
43
|
+
* @returns
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* // Suppose we have an ADT `data Tree<T> = Empty | Node(T, Tree<T>, Tree<T>)`
|
|
48
|
+
* const tree = Tree.Node(
|
|
49
|
+
* 1,
|
|
50
|
+
* Tree.Node(2, Tree.Empty, Tree.Empty),
|
|
51
|
+
* Tree.Empty,
|
|
52
|
+
* );
|
|
53
|
+
*
|
|
54
|
+
* console.log(show(tree));
|
|
55
|
+
* // Node(1, Node(2, Empty, Empty), Empty)
|
|
56
|
+
*
|
|
57
|
+
* console.log(show(tree, { indent: 4, breakLength: 0, trailingComma: "auto" }));
|
|
58
|
+
* // Node(
|
|
59
|
+
* // 1,
|
|
60
|
+
* // Node(
|
|
61
|
+
* // 2,
|
|
62
|
+
* // Empty,
|
|
63
|
+
* // Empty,
|
|
64
|
+
* // ),
|
|
65
|
+
* // Empty,
|
|
66
|
+
* // )
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export function show(value, options = {}) {
|
|
70
|
+
return stringify(value, {
|
|
71
|
+
...options,
|
|
72
|
+
serializers: [
|
|
73
|
+
serializer({
|
|
74
|
+
if: (value, { omittedKeys }) =>
|
|
75
|
+
"_tag" in value &&
|
|
76
|
+
typeof value._tag === "string" &&
|
|
77
|
+
// Detect if `_tag` is already omitted to avoid infinite recursion
|
|
78
|
+
!omittedKeys.has("_tag"),
|
|
79
|
+
then: (val, { ancestors, c, level }, expand) => {
|
|
80
|
+
const fields = unwrap(val);
|
|
81
|
+
const fieldKeys = fields.map((_, i) => `_${i}`);
|
|
82
|
+
|
|
83
|
+
let body = expand(val, {
|
|
84
|
+
level,
|
|
85
|
+
omittedKeys: new Set([
|
|
86
|
+
"_tag",
|
|
87
|
+
...fieldKeys,
|
|
88
|
+
"toJSON",
|
|
89
|
+
Symbol.for("nodejs.util.inspect.custom"),
|
|
90
|
+
]),
|
|
91
|
+
ancestors,
|
|
92
|
+
});
|
|
93
|
+
if (
|
|
94
|
+
body.type === "sequence" &&
|
|
95
|
+
body.values[0].type === "text" &&
|
|
96
|
+
body.values[0].value.startsWith("[Function")
|
|
97
|
+
) {
|
|
98
|
+
body.values[2].ref = body.ref;
|
|
99
|
+
body = body.values[2];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
fields.length ?
|
|
104
|
+
variant(
|
|
105
|
+
sequence([
|
|
106
|
+
text(c.blue(val._tag) + "("),
|
|
107
|
+
...flatMap(fields, (field, i, arr) =>
|
|
108
|
+
i === arr.length - 1 ?
|
|
109
|
+
expand(field)
|
|
110
|
+
: [expand(field), text(", ")],
|
|
111
|
+
),
|
|
112
|
+
...(body.type === "text" ? [text(")")] : [text(") "), body]),
|
|
113
|
+
]),
|
|
114
|
+
body.type === "text" ?
|
|
115
|
+
between(
|
|
116
|
+
fields.map((field) => pair(expand(field), text(","))),
|
|
117
|
+
text(c.blue(val._tag) + "("),
|
|
118
|
+
text(")"),
|
|
119
|
+
)
|
|
120
|
+
: pair(
|
|
121
|
+
between(
|
|
122
|
+
fields.map((field) => pair(expand(field), text(","))),
|
|
123
|
+
text(c.blue(val._tag) + "("),
|
|
124
|
+
text(") "),
|
|
125
|
+
),
|
|
126
|
+
body,
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
: body.type === "text" ? text(c.blue(val._tag))
|
|
130
|
+
: pair(text(c.blue(val._tag) + " "), body)
|
|
131
|
+
);
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
...(options.serializers || []),
|
|
135
|
+
],
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/****************************
|
|
140
|
+
* Common utility functions *
|
|
141
|
+
****************************/
|
|
142
|
+
/**
|
|
143
|
+
* A polyfill for `Array#flatMap` to support pre ES2019 environments.
|
|
144
|
+
* @param arr The array to flatten.
|
|
145
|
+
* @param fn The function to call on each element of the array.
|
|
146
|
+
* @returns
|
|
147
|
+
*/
|
|
148
|
+
const flatMap = (arr, fn) => {
|
|
149
|
+
const result = [];
|
|
150
|
+
for (let i = 0; i < arr.length; i++) {
|
|
151
|
+
const value = fn(arr[i], i, arr);
|
|
152
|
+
if (Array.isArray(value)) Array.prototype.push.apply(result, value);
|
|
153
|
+
else result.push(value);
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// `console` is not standard in JavaScript. Though rare, it is possible that `console` is not
|
|
159
|
+
// available in some environments. We use a proxy to handle this case and ignore errors if `console`
|
|
160
|
+
// is not available.
|
|
161
|
+
const getConsole = (() => {
|
|
162
|
+
let cachedConsole = undefined;
|
|
163
|
+
return () => {
|
|
164
|
+
if (cachedConsole !== undefined) return cachedConsole;
|
|
165
|
+
try {
|
|
166
|
+
cachedConsole = new Function("return console")();
|
|
167
|
+
} catch {
|
|
168
|
+
cachedConsole = null;
|
|
169
|
+
}
|
|
170
|
+
return cachedConsole;
|
|
171
|
+
};
|
|
172
|
+
})();
|