This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-03-17
Channels
- # announcements (6)
- # babashka-sci-dev (6)
- # beginners (99)
- # biff (3)
- # cider (4)
- # clerk (44)
- # clj-kondo (2)
- # clojure (65)
- # clojure-europe (57)
- # clojure-germany (5)
- # clojure-nl (1)
- # clojure-norway (13)
- # clojure-spec (19)
- # clojure-uk (3)
- # clojurescript (8)
- # conjure (3)
- # cursive (21)
- # datahike (19)
- # datomic (1)
- # events (7)
- # fulcro (14)
- # graalvm (3)
- # gratitude (1)
- # guix (5)
- # honeysql (1)
- # humbleui (19)
- # hyperfiddle (39)
- # lsp (4)
- # malli (7)
- # music (1)
- # off-topic (33)
- # pathom (65)
- # re-frame (9)
- # reagent (3)
- # reitit (6)
- # releases (1)
- # sql (15)
- # tools-build (7)
- # vim (5)
- # xtdb (16)
I don't understand what's written here. But I have a deep appreciation for being able to rely on such a mature and optimized runtime.
Nice! Some of my PhD work back in the '80s was on GC implementation so I've continued to be somewhat fascinated by this whole topic forty years on...
Somebody versed in JS can explain to me what Array(100)
actually do?
But I can't actually iterate on this array with map
or forEach
, so what exactly is in this array?
It’s empty until you fill it. If you open the browser console and write dir(new Array(100)), you can investigate what’s there
In nearly 15 years of writing JS on an almost daily basis, I have never once used the array constructor like this.
Interestingly enough, you can make the empty array usable by filling it with "nothing" (which is more specifically actually undefined
), because the fill
function creates any missing indexes based on the length
.
Array(5).fill().map(() => "foo")
// =>
['foo', 'foo', 'foo', 'foo', 'foo']
> so what exactly is in this array?
It is an Object with Array.prototype
as its prototype and a length
property set to the value of your argument. Or from the mental model of treating an Array as somehow different from an Object, you could say it is a "sparse Array", which is so sparse as to be equivalent to empty.
You can get a similar effect from Array-literal syntax with a bunch of commas, but no values, or by setting the length
of an empty Array.
[,,,,,]
// =>
[empty × 5]
const xs = []
xs.length = 5
[empty × 5]
Indexed values on Arrays are no different from any other property. And in fact, the indexes are stored as strings, not numbers. The "magic" of an Array is in the Array prototype, where there is logic to handle treating integral numeric keys as a sequenced index. You can assign any arbitrary property to an Array. And if the key of said property happens to be integral, it will be accessible to Array methods such as Array.prototype.map
.
const inc = x => x + 1
// Fine to just create an Object with the prototype
// instead of calling the constructor. Though,
// initially it will be missing a length.
const xs = Object.create(Array.prototype)
// Ready to act like an Array, but browser confused
// by missing length prop, sees it only as Object.
xs
// =>
Array {}
// Once we add a length, the browser will recognize
// it properly and update its print method.
xs.length = 0
xs
// =>
[]
// Correction?: I'm thinking that ^ is probably
// _not_ about the browser, but rather
// Array.prototype.toString() abandoning its
// custom logic if no length is found.
// Add arbitrary props.
xs.foo = "foo"
xs["bar"] = "bar"
// Add indexed props with number or string.
xs[0] = 0
xs["1"] = 1
// Numeric keys are stored as strings.
Object.keys(xs)
// =>
['0', '1', 'foo', 'bar']
// Indexed props displayed first without keys,
// then any other props with keys.
xs
// =>
[0, 1, foo: 'foo', bar: 'bar']
// Array.map ignores the non-integral keys, and creates
// a brand new Array from the indexed values.
xs.map(inc)
// =>
[1, 2]
// Whoops! Push misbehaves because we forgot to
// update the length. Overwrites value at 0 index
// before updating length to 1.
xs.push(20)
xs
// =>
[20, 1: 1, foo: 'foo', bar: 'bar']
xs.length
// =>
1
// Mismatch between count of numeric keys and a
// non-zero length causes interesting behavior:
// Any number outside the bounds of length is
// treated as not an index.
xs
// =>
[20, 1: 1, foo: 'foo', bar: 'bar']
xs.map(inc)
// =>
[11]
// We can fix that by fixing the length.
xs.length = 2
// =>
[11, 2]
> But I can't actually iterate on this array
Technically you can, just isn't helpful. As demonstrated above, you iterate over existing integral keys.
[,,"foo"].map(x => x.toUpperCase())
// =>
[empty × 2, 'FOO']
So if there are no integral keys it's effectively a noop, because you are iterating over nothing. In the case of .map
, the length
property is copied to the new Array, so it also reports the same number of empty slots as the input.
[,,,].map(x => x.toUpperCase())
// =>
[empty x 3]
> Interestingly enough, you can make the empty array usable by filling it with "nothing"
I, in fact, happened to use one of those fancy Array constructors in real life! Ref: the function frameInsideMask
in this code: https://hanukkah.bluebird.sh/js/text_artist.js
This is a contraction of the Array(<length>).fill().map(() => ...)
idiom.
function frameInsideMask(ddwCharArray, xMaskWidth, yMaskHeight) {
let maskingGrid = Array.from(
{length: yMaskHeight}, (_, y) => Array.from(
{length: xMaskWidth}, (_, x) => {
return {"x": x, "y": y, "text": EMPTY_CHARACTER, "color": "", href: "" };
}));
for (const ddwChar of ddwCharArray) {
let {x, y} = ddwChar;
if (y < yMaskHeight && x < xMaskWidth) {
maskingGrid[y][x] = ddwChar;
}
}
return maskingGrid.flat();
}
Hmm. My ad hoc range snippets in sandboxes/tests have been more verbose than they could have been: new Array(100).fill().map((_, i) => i)
Maybe only the work done is different?
Array(10).fill().map((_, i) => i) // three passes: initilise -> populate -> map
Because, this is longer, in fact:
Array.from({length: 10}, (_, i) => i) // single-pass?
>
Array.from({length: 10}, (_, i) => i) // single-pass?
>
> ... Array.from(obj, mapFn, thisArg)
has the same result as Array.from(obj).map(mapFn, thisArg)
, except that it does not create an intermediate array ... - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from()
>
So yes. That should be single pass. :the_horns:>
Array(10).fill().map((_, i) => i) // three passes: initilise -> populate -> map
>
Only double pass. As I discussed in length above, initializing with the the arity-1 constructor does not create any indexes. It merely sets the value of the length property.@U3Y18N0UC I expected to see some kind of response from you by now (or at least an emoji response on my post to acknowledge you got the info you were looking for). Were you able to follow my explanation? https://clojurians.slack.com/archives/C03RZGPG3/p1679107352829319?thread_ts=1679065526.533799&cid=C03RZGPG3ng sparse Arrays in JS
Did not mean to post full text of the whole previous post. Can't figure out how to remove it from the mobile app. Try to clean it up later
I don't have any blog at the moment. So if there's a post cooking, it must be in your oven. 😊 Feel free to steal anything you need from what I wrote without guilt.
Yes, sorry. I want to test something before I answer, but the life happened and I could not test this...
Ok, yeah, on Javascript's own weirdness, it makes sense. I was just baffled that it's an array with length but without elements. It sounds like a Buddhist thing this: an array with 100 elements, but also with nothing = but now the "nothing" that Javascript uses, a "nothing" that's non-representable
Anyway, thanks for all the answers. I was under the impression that .map
over this sparse array was returning me an empty array, but indeed, map
does return another sparse array - it just doesn't iterate over the "empty" elements
The key point that makes it make sense is to remember that everything in JavaScript inherits from Object
.
Array is no different. It's an Object with an Array prototype. Setting the length
property does not reserve any memory, or any such thing. It's just a property, that gets used by methods from the Array prototype to decide how to behave.
The "key point" actually doesn't make too much sense for me. In Ruby, everything also inherits from Object, and arrays work how we expect (that meaning, length
is a getter but not a setter, constructing new arrays make them traversable, etc).