1

This is normally a detail that is hidden from the programmer, but as a test I made a new class Vector extending Array that changed the constructor to unpack an input array and now Vector.map() stopped working:

class Vector extends Array {
    constructor(array) {
        console.assert(Array.isArray(array),
            `Vector constructor expected Array, got ${array}`);
        // ignore special case of length 1 array
        super(...array);
    }

    add(other) {
        return this.map((e, i) => e + other[i]);
    }
}

let a = new Vector([1,2,3]);
console.log(a.add(a));

I assume what happened based on my Python experience is that Array.map() internally constructs a new Array with an iterator, like that of Array.prototype[@@iterator](), but my Vector constructor instead takes an Array object. Is this correct? Maybe map is a primitive of the JS implementation.

qwr
  • 9,525
  • 5
  • 58
  • 102
  • Does this answer your question? [How to make an iterator out of an ES6 class](https://stackoverflow.com/questions/28739745/how-to-make-an-iterator-out-of-an-es6-class) – KyleRifqi Feb 13 '22 at 03:01
  • No that's not what i'm asking – qwr Feb 13 '22 at 03:01

1 Answers1

3

This has nothing to do with iterators, it's because of the way that Array.prototype.map() creates the result.

The result will be the same subclass as the object it's being called on, so it calls your constructor to create the result. It expects any subclasses of Array to support the same parameters as Array itself: either the argument is a single integer which is the size of the array to create, or it's multiple arguments to provide the initial contents of the array.

Since the array you're mapping over is 3 elements long, it wants to create a Vector with 3 elements, so it calls Vector(3) to create this.

You need to redefine your class to accept arguments like Array:

class Vector extends Array {
  constructor(...args) {
    // Allow it to be called with an Array argument and convert it to Vector.
    if (args.length == 1 && Array.isArray(args[0])) {
      if (args[0].length == 1) {
        // need to handle 1 element specially so it's not used as the length
        super(1);
        self[0] = args[0];
      } else {
        super(...args[0]);
      }
    } else {
      super(...args);
    }
  }

  add(other) {
    return this.map((e, i) => e + other[i]);
  }
}

let a = new Vector([1, 2, 3]);
console.log(a.add(a));

This implementation accepts all the Array arguments, and additionally accepts an array as the sole argument in order to convert it to a vector.

Barmar
  • 741,623
  • 53
  • 500
  • 612
  • It should also be noted that `super(... array)` is problematic when the Vector constructor is passed `[2]` for example, because then the Array constructor will think it's supposed to create a new array of length 2. – Pointy Feb 13 '22 at 03:25
  • @Kaiido How stupid of me, of course! – Barmar Feb 13 '22 at 03:29
  • @Pointy That's what all the `if` conditions were supposed to handle, but I left out the original spread syntax. – Barmar Feb 13 '22 at 03:29
  • @Kaiido And that fixed the "non-callable @@iterator" error. – Barmar Feb 13 '22 at 03:30
  • Yes. Part of the problem is that I originally wrote `constructor(size=0, ...args)`. When I changed the function signature I didn't fix up all the uses properly. – Barmar Feb 13 '22 at 03:33
  • Thanks for the response. This is toy code but the way you have written is the way I wrote it originally, with unpacking an array as an extra convenience option for constructors. But I may move that to its own convenience function. – qwr Feb 13 '22 at 03:36