Last active
October 7, 2021 14:06
-
-
Save joakim/8321a381e3fbdc2cdb7680121de23555 to your computer and use it in GitHub Desktop.
A versatile immutable data structure for JavaScript. It's like the strict yet lenient parent that Array and Object never had. Related to aunt Lua's tables.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
nothing = new Proxy(Object.freeze(Object.create(null)), Object.create(null, { | |
get: { | |
value(target, property) { | |
return property === Symbol.toPrimitive ? () => undefined : nothing | |
} | |
} | |
})) | |
Tuple = (...elements) => { | |
if (elements.length === 0) return nothing | |
if (elements.length === 1) return elements[0] | |
const props = { [Symbol.for('size')]: { value: elements.length } } | |
for (let i = 0; i < elements.length; i++) { | |
const [value, name] = Array.isArray(elements[i]) ? elements[i] : [elements[i]] | |
props[i] = { value, enumerable: true } | |
if (name !== undefined) { | |
// ideally, only valid unquoted property names would be allowed | |
// instead, allow any string of non-zero length that's not a whole number | |
if (Object.prototype.toString.call(name) !== "[object String]" || name.length === 0) { | |
throw Error(`name must be a string of non-zero length, got ${typeof name} ${name}`) | |
} | |
if (/^\d+$/.test(name)) { | |
throw Error(`name cannot be a whole number, got ${name}`) | |
} | |
props[name] = { get() { return this[i] } } | |
} | |
} | |
return Object.freeze(Object.create(Tuple.prototype, props)) | |
} | |
Tuple.prototype = Object.create(nothing, { | |
[Symbol.iterator]: { *value() { yield* Tuple.values(this) } }, | |
[Symbol.toStringTag]: { value: 'Tuple' }, | |
[Symbol.toPrimitive]: { value: (hint) => hint === 'number' ? NaN : '[object Tuple]' } | |
}) | |
Tuple.from = (source) => { | |
if (Array.isArray(source)) { | |
return source.length ? Tuple(...source.map(value => [value])) : nothing | |
} | |
else if (typeof source === 'object') { | |
const entries = Object.entries(source).map(([name, value]) => [value, name]) | |
return entries.length ? Tuple(...entries) : nothing | |
} | |
else { | |
throw Error(`source must be a collection, got ${typeof source}`) | |
} | |
} | |
Tuple.indices = (tuple) => Object.keys(tuple).map(Number) | |
Tuple.values = Object.values | |
Tuple.names = (tuple) => { | |
const indices = Object.keys(tuple) | |
return Object.getOwnPropertyNames(tuple).filter(name => !indices.includes(name)) | |
} | |
Tuple.size = (tuple) => tuple[Symbol.for('size')] | |
// New tuple | |
let tup = Tuple([42, 'foo'], true) // [value, name], [value] or just value | |
tup[0] // 42 | |
tup[1] // true | |
tup.foo // 42 | |
Tuple.indices(tup) // [ 0, 1 ] | |
Tuple.names(tup) // [ 'foo' ] | |
Tuple.values(tup) // [ 42, true ] | |
Tuple.size(tup) // 2 | |
// New tuple from object | |
let obj = Tuple.from({ foo: 42, bar: true }) | |
obj[0] // 42 | |
obj[1] // true | |
obj.foo // 42 | |
obj.bar // true | |
// New tuple from array | |
let arr = Tuple.from([42, true]) | |
arr[0] // 42 | |
arr[1] // true | |
// Possible syntax: | |
bar = true | |
tup = #(foo: 42, bar, 'hello') | |
tup.0 // 42 | |
tup.1 // true | |
tup.2 // 'hello' | |
tup.foo // 42 | |
tup.bar // true |
And here's how it might be done with Record & Tuple:
let bar = true
let tuple = #{
0: 42,
1: bar,
2: 'hello',
foo: 0,
bar: 1,
}
tuple[0] // tuple.0
tuple[1] // tuple.1
tuple[2] // tuple.2
tuple[tuple.foo] // tuple.foo
tuple[tuple.bar] // tuple.bar
tuple[tuple.baz] // tuple.baz
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This example of
Tuple
usesnothing
, a betterundefined
(not required).