JavaScript Array Comprehension

Python은 쉽게 리스트를 만들 수 있는 방법으로 list comprehension을 제공합니다.

0에서 10까지 제곱의 리스트는 list comprehension으로 다음과 같이 표현할 수 있습니다. 여기서 range(10)[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 리스트를 리턴하고, 전체 식은 각 원소 x를 제곱(x**2)하여 새로운 리스트를 리턴합니다.

>>> [x**2 for x in range(10)]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

리스트 [1,2,3][3,1,4]에서 각각 xy를 뽑아 xy가 같지 않으면 튜플 (x, y)의 리스트를 리턴하는 것도 다음와 같이 간결히 표현할 수 있습니다.

>>> [(x, y) for x in [1,2,3] for y in [3,1,4] if x != y]
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]

Python은 언어 자체가 for, in, if 등의 키워드를 인지하고, 위와 같이 list comprehension을 이용해 리스트를 쉽게 생성, 조작할 수 있게 해줍니다.

JavaScript의 배열(array)를 이용해서 비슷한 일을 할 수 없을까요? 물론 JavaScript는 comprehension을 위한 특별한 문법을 제공하지 않습니다. 하지만 고차함수를 이용하여 Python의 list comprehension과 비슷한 array comprehension을 만들 수 있습니다.

일단 편의를 위해 배열에 대해 여러 고차함수를 제공하는 lodash 라이브러리를 사용하겠습니다.

lodash는 여러 고차함수를 제공하지만 array comprehension을 구현하기 위해서는 다음 두 함수를 추가할 필요가 있습니다. lodash의 mixin() 함수를 이용해 다음과 같이 추가합니다. (참고로 이 함수들은 lodash-contrib에서 가져왔습니다.)

var concat = Array.prototype.concat;
_.mixin({
  // Concatenates one or more arrays given as arguments.  If given objects and
  // scalars as arguments `cat` will plop them down in place in the result
  // array.  If given an `arguments` object, `cat` will treat it like an array
  // and concatenate it likewise.
  cat: function() {
    return _.reduce(arguments, function(acc, elem) {
      if (_.isArguments(elem)) {
        return concat.call(acc, slice.call(elem));
      }
      else {
        return concat.call(acc, elem);
      }
    }, []);
  },

  // Maps a function over an array and concatenates all of the results.
  mapcat: function(array, fun) {
    return _.cat.apply(null, _.map(array, fun));
  },
});

여기서 cat() 함수는 여러 개의 배열을 인자로 받아 하나의 배열로 합쳐줍니다.

var s1 = _.cat([1,2,3], [4], [5,[6]]);
console.log(s2);
// [ 1, 2, 3, 4, 5, [ 6 ] ]

다음으로 mapcat() 함수는 배열의 각 원소 x에 대해 함수 f를 호출하여 결과로 리턴된 배열을 합쳐서 리턴합니다. 여기서 함수 f는 원소를 인자로 받아 새로운 배열을 리턴하는 함수입니다.

var s2 = _.mapcat([1,2,3], function (x) { return [x, x]; });
console.log(s2);
// [ 1, 1, 2, 2, 3, 3 ]

map(), filter(), mapcat() 함수만 있으면 Python의 list comprehension을 다음과 같이 기계적으로 만들어낼 수 있습니다.

// >>> [(x, y) for x in [1,2,3] for y in [3,1,4] if x != y]
// [(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]

var r1 = _.mapcat([1, 2, 3], function (x) {
  return _.map(_.filter([3, 1, 4], function (y) {
    return x !== y;
  }), function (y) {
      return [x,y]; // Use an array to create a tuple.
  });
});
console.log(r1);

규칙을 눈치 채셨나요? for가 한 번만 나오면 map() 함수로, for가 2번 이상 나오면 mapcat()을 사용하다가 가장 마지막에만 map()을 씁니다. 그리고 if 조건이 나오면 적절히 filter를 삽입해 주면 됩니다. (참고로 여기서 filter는 eager evaluation하기 때문에 불필요하게 두 번 루프를 돌아서 비효율적인 면이 있습니다. lazy evaluation하는 방법으로 성능 개선이 가능합니다만, 설명이 너무 복잡해져서 이 부분은 생략합니다.)

번역 규칙을 간단히 정리하면 다음과 같습니다.

* [x+1 for x in xs] ≡

_.map(xs, function (x) { return x+1; });

* [x+1 for x in xs if x != 0] ≡

_.map(_.filter(xs, function (x) { return x !== 0; }),
      function (x) { return x+1; });

* [(x, y) for x in xs for y in ys] =

_.mapcat(xs, function (x) { 
  return _.map(ys, function (y) { 
    return [x, y]; 
  }); 
});

다음은 array comprehension을 이용해 행렬을 transpose하는 코드입니다.

/*
>>> matrix = [
...     [1, 2, 3, 4],
...     [5, 6, 7, 8],
...     [9, 10, 11, 12],
... ]

>>> [[row[i] for row in matrix] for i in range(4)]
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]
*/

var matrix = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]];
var r2 = _.mapcat(_.range(4), function (i) {
  return [_.map(matrix, function (row) {
    return row[i];
  })];
});
console.log(r2);
Advertisements