Skip to content

Instantly share code, notes, and snippets.

@joakim
Last active October 7, 2021 14:06
Show Gist options
  • Save joakim/8321a381e3fbdc2cdb7680121de23555 to your computer and use it in GitHub Desktop.
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.
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
@joakim
Copy link
Author

joakim commented Aug 19, 2021

This example of Tuple uses nothing, a better undefined (not required).

@joakim
Copy link
Author

joakim commented Oct 5, 2021

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

Open in playground

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment