16

Suppose I have this simple JavaScript function:

function returnArray(){
    return [1, 2, 3];
}

Further suppose that I then say

var test = [0, ...returnArray()];

You'd expect test to be equal to [0,1,2,3], and you'd be right. I tried that and of course it works.

Now I have this exercise where I'm suppose to build a function called double that takes an array as a parameter and returns another array that contains all of the original array's values doubled. So if I call double([1,2,3]) I should get [2,4,6]. The constraints of the exercise are that I must use only array destructuring, recursion, and the rest/spread operators to build my function. No array helpers allowed. So I came up with this:

function double(array){
  if (array.length===1) return 2*array[0];
  var [num, ...rest] = array;  
  if (rest.length!=0) return [2*num, ...double(rest)];  
}

If I run this function with any array whose size is at least two, I receive an error message saying that double is not a function. If I remove the ... operator before double, magically double is a function again, except that of course the result of double([1,2,3]) is [2,[4,6]], which is not quite the same as [2,4,6].

My first thought was that maybe, for some weird reason, you can't use ... in front of a function, even if the function returns an array, so I tested this hypothesis with my the returnArray() function above, and found out that it works just fine. I have no idea why it breaks down in the exercise. I can only guess recursion might have something to do with it, but I have no clue why it would be so. Can anyone point out what's wrong my my code?

EDIT: Thanks everyone, it was quite a silly mistake! I should have seen it. I added comments to some of your answers.

Felix Kling
  • 795,719
  • 175
  • 1,089
  • 1,143
Nicola
  • 379
  • 3
  • 14

4 Answers4

23

Your logical error has been pointed out in comments and answers, but let me point out a cleaner, simpler, less bug-prone way to write this which is more in line with basic principles of recursion.

function double([head, ...tail]) {
  if (head === undefined) return [];
  return [2*head, ...double(tail)];
}

In other words, there is only one "base case", namely the empty array, which returns an empty array. Everything else is simple recursion.

You could "functionalize" this further with

function map(fn) {
  return function iter([head, ...tail]) {
    return head === undefined ? [] : [fn(head), ...iter(tail)];
  };
}

const double = map(x => 2*x);
console.log(double([1, 2, 3]));
  • +1 for the points-free style. Good to see more and more people using functional programming correctly in the Javascript world. – Henrique Barcelos May 19 '17 at 14:03
  • @HenriqueBarcelos I can't say I'm fully grasping these Haskell concepts like point-free, monads and stuff, but reading a bit about it, could you explain why it's point-free if the parameters of the function are present ? – Nelson Teixeira May 19 '17 at 14:10
  • 1
    Very clean and elegant solution, thanks. The only reason I didn't tick your answer is because it wasn't so easy to grasp without the rest of the explanations, but as said thanks! :) – Nicola May 19 '17 at 14:18
  • 1
    Using a regular undefined check (`head === undefined`) might make this even neater. – Ry- May 19 '17 at 23:03
  • What I like about this answer is that it only has one base case, not two (`length = 0` and `length = 1`). – Barmar May 20 '17 at 00:21
9

It's a very strange error message, no question, but the main problem is a logical error in double: In two branches of the code, calling double results in a non-iterable value (in one case a number, in another undefined). But you're always applying spread notation to it. So that fails in those two cases. The cases are:

  1. You're only returning the number, not an array, in the array.length === 1 case.
  2. You're not returning anything in the case where array.length is not 1 and rest.length is 0, so the result of calling double in that case is undefined.

You're hitting the case where you try to spread a number first, like this:

function a() {
  return 42;
}
const b = [...a()];

For #1, you should be returning an array with one entry. For #2, You should be returning []. So the minimal changes version is:

function double(array) {
  if (array.length === 1) {
    return [2*array[0]];
    //     ^−−−−−−−−−−^−−−−−−−−−−− note
  }
  var [num, ...rest] = array;  
  if (rest.length > 0) {
    return [2*num, ...double(rest)];
  }
  return []; // <−−−−−−−−−−−−−−−−− note
}
console.log(double([1,2,3]));
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Much better explained than mine – chazsolo May 19 '17 at 13:57
  • Thanks, this helped a lot in understanding what was wrong. I tried your implementation and it works like a charm. However, I tried your solution without returning [ ] at the end as well, and it seems to work just fine anyway. Is there any case when the lack of return [ ] might break the function down? – Nicola May 19 '17 at 14:16
  • 3
    @Nicola when passing an empty array. – nils May 19 '17 at 14:20
3

Just a minor change:

function double(array) {
  // note the return here is an array, not a number
  if (array.length === 1) return [2 * array[0]];

  var [num, ...rest] = array;
  if (rest.length) return [2 * num, ...double(rest)];
}

console.log(double([1, 2, 3, 4]));

You were returning a number, and destructuring a number will leave you with an error.

...5 // throws SyntaxError
chazsolo
  • 7,873
  • 1
  • 20
  • 44
  • 1
    It's intellectually unsatisfying that the `2 *` appears in two places. –  May 19 '17 at 14:02
  • @torazaburo I agree. Your answer is definitely the right one, both in fixing the error and simplifying. – chazsolo May 19 '17 at 14:04
1

Here is the error I get using node@6.10.3

if (rest.length!=0) return [2*num, ...double(rest)];  
                                        ^

TypeError: double(...)[Symbol.iterator] is not a function
    at double (/home/henrique/labs/test.js:4:41)
    at double (/home/henrique/labs/test.js:4:41)
    at Object.<anonymous> (/home/henrique/labs/test.js:7:1)
    at Module._compile (module.js:570:32)
    at Object.Module._extensions..js (module.js:579:10)
    at Module.load (module.js:487:32)
    at tryModuleLoad (module.js:446:12)
    at Function.Module._load (module.js:438:3)
    at Module.runMain (module.js:604:10)
    at run (bootstrap_node.js:390:7)

Which probably means that the result of the evaluation of some of your function call is not an iterable.

The error is here:

if (array.length===1) return 2*array[0];

Change to:

if (array.length===1) return [2*array[0]];

and it will work.

Henrique Barcelos
  • 7,670
  • 1
  • 41
  • 66