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]
에서 각각 x
와 y
를 뽑아 x
와 y
가 같지 않으면 튜플 (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);
Pingback: JavaScript Array Comprehension | 서광열의 코딩 스쿨