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/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
+ })();