Provide an easy way to create a new array, filled via a mapping function

Creating multidimensional arrays from ranges/arrays is an intriguing idea.

But the api you proposed is not clear to me ;(
the argument i has to always begin with zero? what if we want a step? Can you explain the mapping you're introducing here?

1 Like

Can’t you just do i * step? i and j are both indices of the array in the dimensions.
More formally, For a non-negative integer n in the pseudo-expression [x_0, x_1, …, x_(n-1)].buildShape((y_0, y_1, …, y_(n-1)) => ……), y_n is a non-negative integer smaller than (and not equal to) x_n representing its index in the (n+1)th dimension.
So [5].buildShape(i => i * 2) should totally work like Array.from({ length: 5 }, (_, i) => i * 2).

I'm not sure buildShape is as easy to understand as the Number.range stuff.

const result = [5, 7].buildShape((i, j) => i * 7 + j + 1);

vs

const result = Number.range(0, 5)
  .map(row => Number.range(1, 8).map(i => row * 7 + i).toArray())
  .toArray();

The Number.range stuff is more chars, sure, but it's more versatile.

What if we have a higher-dimensional array? Of course we can use recursion, but we want a built-in API for it. There does exist some prior art like the Python numpy.zeros. Somehow off-topic though.

@theScottyJam @ljharb Could you give some comments on it?

I'm certainly not an authoritative voice, just a dude with opinions, which I'll certainly share.

I don't think the original proposal, of a method generating an array of a fixed size will get very far. It would certainly be a helpful addition, and I would lover to have it, but I think it just won't add enough utility to the language over the range + iter helpers solution.

I would assume that a multi-dimensional array creation function has a better chance of getting through, because it would be able to solve other problems that are a tiny bit more tricky to solve without such a helper. Plus, if it's generalized to construct arrays of n dimensions, then it could also be used as a convenient one-dimensional array construction method.

1 Like

@theScottyJam @ljharb @jschoi

I just put all my thoughts into a package:

I am hoping to make a proposal with these methods, but neither I am familiar with writing a specification, nor I will have much time to do so. If anyone could help with the proposal, it would be appreciated.

Here is the source code in TypeScript:

1 Like

@graphemecluster - These nested utility functions seem nice. Might I suggest opening a new thread for that discussion, and linking to it from here? I have some feedback I would like to share, but I feel like I'd rear this thread pretty off-course from its original topic if I dumped it all here.

2 Likes

Great, here is: Proposal for Array.prototype methods for multi-dimensional arrays

1 Like

I'd have loved 0:9 to be a shortcut for Number.range(0, 9), that way [...0:9].map(i => i**2) could be done for example, in a short way

Some discussions was in the proposal-slice-notation Slice Extensibility · Issue #19 · tc39/proposal-slice-notation · GitHub

May be difficult to disambiguate that from a ternary?

cond ? 1:2:3

Is this: 1 : (2:3) or (1:2) : 3

yes, in those context (ternary), wrapping in () would be required: cond ? (0:2) : (1:2)

If this map syntax becomes a thing, perhaps it would need to be exclusive inside square brackets [].

[cond ? 1 : 9]   // Unambiguously ternary
[1 : 9]          // Unambiguously range
[cond ? 5 : 1 : 9] // Invalid
[cond ? 5 : (1:9)] // Also invalid
[cond ? 5 : [1:9]] // Valid

This is hypothetical of course.

1 Like

Iterable numbers (Iterable numbers: why not? · Issue #48 · tc39/proposal-Number.range · GitHub) could significantly simplify it, but it was rejected:

const result = Array.from(5, i => i + 1);

Just need to throw on a custom suffix and you're good to go

const result = Array.from(5x, i => i + 1);
1 Like

Intriguing, this can be done today with only slightly extra syntax using symbols (using [x] instead of x).

Safely augments the built-in Number.prototype using a symbol property key (rather than a string property key to avoid issues like #SmooshGate):

const x = Symbol();

Object.defineProperty(Number.prototype, x, {
  *get() {
    for (let n = 0; n < this; n++) yield n;
  },
});

Array.from(5[x], (i) => i + 1);
// => [ 1, 2, 3, 4, 5 ]

Or to avoid the overhead of a generator while still inheriting from the %IteratorPrototype% object:

const x = Symbol();

const Iterator = {
  prototype: Object.getPrototypeOf(
    Object.getPrototypeOf([][Symbol.iterator]()),
  ),
};

Object.defineProperty(Number.prototype, x, {
  get() {
    let n = 0;
    const next = () => {
      if (n >= this) return { value: undefined, done: true };
      return { value: n++, done: false };
    };
    return Object.create(Iterator.prototype, { next: { value: next } });
  },
});

Array.from(5[x], (i) => i + 1);
// => [ 1, 2, 3, 4, 5 ]

What would it require for the committee to be convinced that something like Array.build needs to be added to the language? I'm asking because beliefs should be falsifiable. Otherwise, no argument we make will ever be enough to convince the committee.

I can think of a lot of really persuasive arguments for adding a function like Array.create.

  1. Array.create is the introduction rule for arrays. Every data type requires rules to produce values of that data type, i.e. introduction rules, and rules to consume values of that data type, i.e. elimination rules. Array#reduce is the elimination rule for arrays. However, we don't have a canonical introduction rule for arrays. No, Array.from doesn't count.

    What does it mean for Array.create to be an introduction rule for arrays? It means that whenever somebody wants to create an array, the Array.create function should be their go-to function. It also means that beginners in JavaScript should be taught to use Array.create whenever they want to create an array.

    Note: Array literals are another introduction rule for arrays. Array literals should be preferred over using Array.create whenever possible. However, if you find yourself creating an empty array literal and then pushing additional elements to it inside a for loop, then you should consider using Array.create instead.

  2. It's semantically different from Array.from and Number.range. The Array.from function is meant for converting iterables into arrays. The Number.range function is meant for iterating over a range of numbers. However, the Array.create function is meant for creating arrays. It serves a very different purpose than Array.from and Number.range.

    Q. I want to convert an iterable into an array. What function do I use?
    A. Use the Array.from function.

    Q. I want to iterate over a range of numbers. What function do I use?
    A. Use the Number.range function.

    Q. I want to create a new array. What function do I use?
    A. Use the Array.create function.

  3. It's the tersest way to create a new array. Compare the following.

    const a = Array.from({ length: 5 }, (_, i) => i + 1);
    const b = Number.range(0, 5).map((i) => i + 1).toArray();
    const c = Array.create(5, (i) => i + 1);
    

    In addition, it's the most semantically accurate of the three ways as mentioned in point 2. That's +1 for brevity and another +1 for semantic accuracy.

  4. It's better for performance as compared to Number.range. We can pre-allocate the entire array and then populate it.

  5. It promotes a functional style of programming. No more creating an array and populating it within a for loop.

  6. It hints at a pattern for creating introduction rules for other data types. For example, consider the following Matrix class. As you can see, we use nested Array.create function calls to implement the Matrix.create function. We then use the Matrix.create function to implement the Matrix#transpose method.

    class Matrix {
        // new Matrix :: (Int, Int, Array (Array a)) -> Matrix a
        constructor(rows, cols, data) {
            this.rows = rows;
            this.cols = cols;
            this.data = data;
        }
    
        // Matrix.create :: (Int, Int, (Int, Int) -> a) -> Matrix a
        static create(rows, cols, mapping) {
            const data = Array.create(rows, row =>
                Array.create(cols, col => mapping(row, col)));
            return new Matrix(rows, cols, data);
        }
    
        // Matrix#transpose :: Matrix a ~> () -> Matrix a
        transpose() {
            const { rows, cols, data } = this;
            return Matrix.create(cols, rows, (row, col) => data[col][row]);
        }
    }
    
1 Like

I personally prefer Array.create over Array.fromLength for the following reasons.

  1. Array.fromLength seems like a specialization of Array.from. I don't like this similarity because Array.from has a very specific semantic meaning — converting iterables into arrays. However, the Array.fromLength function doesn't do any conversion.
  2. I like the name Array.create because it describes precisely what the function does. It creates a new array. What information do we need to create a new array? First, we require the length of the new array. Second, we require the elements of the new array. Hence, Array.create requires two arguments, the length of the new array as well as a mapping function to get the elements of the new array.

A lot of programming constructs are superfluous.

  1. Why bother with Array#map and Array#filter when we can always use Array#reduce instead?
  2. Why bother with regular expressions when we can always use parsers for context-free grammars?
  3. Why bother with defining functor or applicative instances when we can always define monad instances?

There is inherent value in using less powerful constructs. Here are some of the advantages of using Array.create over Number.range.

  1. It's more semantically accurate to use Array.create in order to create an array. It makes the intention clear to the reader. On the other hand, using Number.range as you showed in your examples above doesn't reveal the intention as clearly as Array.create does.
  2. It's better for performance as we can pre-allocate the entire array before populating it.

By the way, the Array.from function was meant for converting iterables into arrays. However, I have never used it for that purpose because using the spread operator is a nicer way to convert iterables into arrays.

// when not mapping
const a = Array.from(someIterable);
const b = [...someIterable];

// when mapping
const c = Array.from(someIterable, mappingFn);
const d = [...someIterable].map(mappingFn);

In fact, the only time I ever use Array.from is specifically when I want to create an array of a specific length, i.e. I always write something like Array.from({ length: n }, (_, i) => …). So, I personally never use Array.from for its intended purpose. I only ever use it to create arrays of a specific length.

This is all the more reason to add Array.create to the language.

Anyway, I think I've done enough bikeshedding for today. I firmly believe that Array.create is a great addition to the language. From what I read, I also believe that everybody else is of the same opinion. If the committee is so against adding a simple quality of life function to the language, then that's their choice. I've said everything that I want to say.

I "gathered some data" (actually, it was just personal experience) related to this. Look at this commit history.

As you can see, we started with Array(n)+for loop (probably because in "the old days" pre-allocating an array was faster than repeatedly pushing values, otherwise the garbage-collector and the allocator would work too much).

Then we transitioned to []+while (n--)+push. Then Array(n).fill().map(). And finally Array.from({ length }, ...).

I was confused as to why this is a "hacky solution", because I thought { length } was a named-arg bag. Then I read the docs, and realized the arg is supposed to be iterable or "array-like", and { length } is neither but it still works, because it has the minimal requirement to be considered "array-like" (the length prop). The mapping fn is also discarding both args (value and index), which makes it a "filler" rather than a "mapper"