이전 포스팅 참고
- 컬렉션 중심 프로그래밍
컬렉션 중심 프로그래밍의 목표는 컬렉션을 다루는 함수 세트들을 잘 만들어 재사용성을 극대화시키는 데 있으며, 컬렉션을 다루는 것은 함수형 프로그래밍과 잘 어울립니다.
컬렉션을 다루는 것이 지연 평가와 무슨 관련이 있을지 알기 위해서 Partial.js 의 고차 함수들을 유형별로 묶어서 살펴보도록 하겠습니다.
- 수집 : map, pluck, values, keeys, pairs...
- 필터링 : filter, reject, difference, compact...
- 축소 : reduce, group_by, index_by, count_by, max, min...
- 찾기 : find, some, every, take, indexOf, findIndex...
각 유형의 제일 앞에 쓴 함수들 (map, filter, reduce, find) 은 대표 함수 라고 할 수 있습니다.
이 함수들이 각 유형 중 추상화 레벨이 가장 높으며, 이 대표 함수를 이용해 다른 함수들을 만들어낼 수 있기 때문입니다.
- 수집
const _ = require('partial-js');
_.pluck = function(data, key) {
return _.map(data, function(val) { return val[key]; });
};
const users = [{ id: 1, name: 'a' }, { id: 2, name: 'b' }, {id: 3, name: 'c' }];
console.info(_.pluck(users, 'id')); // [1, 2, 3]
원래 Partial.js 에 있는 함수인 _.pluck 은 두번째 인자로 받은 프로퍼티의 값만 추출하는 함수입니다.
위에서 언급했던 것 처럼 _.pluck 은 수집 유형 중 대표 함수인 _.map 으로 만들어낼 수 있습니다.
- 필터링
_.difference = function(data, target) {
return _.filter(data, function(val) {
return !_.contains(target, val);
});
};
const list = [0, 1, 2, 3, 2, 1, 3, 2, 1];
console.info(_.difference(list, [0, 1])); // [2, 3, 2, 3, 2]
_.difference 는 첫번째 배열과 배교해 차집합을 만들어내는 함수입니다.
마찬가지로 필터링의 대표 함수인 _.filter 를 통해 만들 수 있습니다.
- 축소
_.group_by = function(data, iter) {
return _.reduce(data, function(grouped, val, i, list) {
const key = iter(val, i, list);
_.has(grouped, key) ? grouped[key].push(val) : grouped[key] = [val];
return grouped;
}, {});
};
const users = [{ id: 1, age: 30 }, { id: 2, age: 29 }, { id: 3, age: 30 }, { id: 4, age: 29 }, { id: 5, age: 28 }];
console.info(_.group_by(users, item => item.age));
// {
// '28': [ { id: 5, age: 28 } ],
// '29': [ { id: 2, age: 29 }, { id: 4, age: 29 } ],
// '30': [ { id: 1, age: 30 }, { id: 3, age: 30 } ]
// }
길게 설명하지 않아도 어떤 역할을 하는 함수를 어떤 대표 함수로 만들었는지 보이실 겁니다.
축소의 경우 데이터 형이 완전히 달라지기도 하며 값을 만들어 갈 때 이전 값을 필요로 하는 경우도 빈번합니다.
미리 적자면, 이러한 특징 때문에 축소 유형은 지연 평가를 불가능하게 만듭니다.
- 찾기
_.some = function(data, iter) {
iter = iter || function(v) { return v; };
let response = false;
_.find(data, function(v) {
return response = !!iter(v);
});
return response;
}
console.info(_.some([0, 0, 1, 0, 2], function(v) { return v > 1; })); // true
찾기 유형의 함수들 또한 대표 함수인 _.find 를 이용해 구현이 가능합니다.
위에서 언급한 것 처럼 축소 유형은 지연 평가가 불가능하지만, 수집 / 필터링 / 찾기 유형은 지연 평가와 밀접한 연관이 있으며 특히 찾기 의 경우엔 지연 평가를 통해 상당한 성능 향상을 기대할 수 있습니다.
- 수집 / 필터링 / 찾기 유형과 지연 평가
앞서 말한 것처럼 수집 -> 필터링 순서로 준비된 연속적인 함수 실행 환경일 때, 배열의 모든 요소에 대해 map 을 먼저 수행해 루프를 한바퀴 돌고 이어서 filter 수행 때문에 루프를 한번 더 도는 형태가 아니라 각 요소별로 map -> filter 를 수행해도 동일한 값을 만들 수 있다는 것입니다.
제가 함수형 프로그래밍으로 작업하지 않는 프로젝트에 대해서도 이런 부분 때문에 [].map().filter() 와 같이 작성하게 되는 코드는 항상 [].reduce() 를 사용하곤 했습니다. 이와 같은 맥락이라고 볼 수 있습니다.
// 엄격한 평가
[map_1({ value: 10 }), map_1({ value: 20 }), map_1({ value: 30 })] - 1
[filter_1(10), filter_1(20), filter_1(30)] - 2
[10, 30] - 3
// 지연 평가
[[map_1({ value: 10 }), filter_1(10)],
[map_1({ value: 20 }), filter_1(20)],
[map_1({ value: 30 }), filter_1(30)]] - 1
[10, 30] - 2
엄격한 평가 코드에서의 상황을, 지연 평가에서 재배치를 통해 동일한 값을 만들어 낼 수 있습니다.
만약 map, filter 즉 수집, 필터링이 몇 번을 더 반복하더라도 재배치를 통해 같은 결과를 만들어 내는건 동일합니다.
const list = [1, 2, 3, 4, 5, 6];
_.go(list,
_.map(v => v * v),
_.filter(v => v < 20),
console.info); // [1, 4, 9, 16]
위와 같은 코드는 우선 [(v * v), (v * v), ... ] 이런식으로 모든 배열의 모든 요소에 대해 수집을 하고 [(v < 20), (v < 20), ...] 처럼
또다시 배열의 모든 요소에 대해 필터링을 한 후 결과를 얻어냅니다. 이 코드를 지연 평가로 바꿔봅시다.
const list = [1, 2, 3, 4, 5, 6];
_.go(list,
L.map(v => v * v),
L.filter(v => v < 20),
console.info);
이렇게 변경하면, 실제로 L 에 의한 재배치를 통해 [[(v * v), (v < 20)], [(v * v), (v < 20)], ...] 와 같이 수행됩니다.
여기서 한가지 짚고 갈 점은 Partial.js 의 L 에서 map, filter, reject 는 지연 평가를 시작하거나 유지시키고 take, some, every, find, loop 함수는 지연 평가를 끝냅니다.
따라서 위 코드처럼 L.filter 이후 console.info 를 찍으면 아직 끝나지 않은 지연 평가 중인 함수가 나옵니다. 이건 조금 불편하네요.
하지만 제 생각엔 일부러 이렇게 해놓은 것 같습니다.
위처럼 수집과 필터링이 연속적으로 있을 때 L 은 이들을 하나로 모아 map + filter 함수로 만드는 일을 하는데 여기서 얻어지는 이득은 map 이나 filter 함수가 실행될 때마다 생겨야 했던 새로운 배열 객체 생성을 1개로 줄이고 그만큼의 배열 변경을 줄이는게 전부입니다.
이것만으론 이득이 그리 크지 않고, 결국 이 재배치들은 그 끝에 찾기 가 있을 때 이득이 극대화됩니다.
대략 코드는 L.map -> L.filter -> L.take 의 순서가 될거고, 이래야만 지연 평가를 하는데 의미가 있기 때문에 지연 평가를 시작 혹은 유지하는 함수와 끝내는 함수를 나눠놓은 것 같습니다.
const list = [1, 2, 3, 4, 5, 6];
let count = 0;
_.go(list,
L.map(v => {
count += 1;
return v * v;
}),
L.filter(v => v < 20),
L.take(2),
console.info); // [1, 4]
console.info(count); // 2
조건을 만족하는 값 2개만 찾고 빠져나오도록 L.take(2) 를 썼을 때, 실제로 위 코드는 count 변수에 찍힌 것처럼 루프를 2번만 반복합니다. 만약 지연 평가를 하지 않았다면 _.map 과 _.filter 는 6번씩 돌게 되겠죠.
const list = [1, 2, 3, 4, 5, 6];
let count = 0;
_.go(list,
_.map(v => {
count += 1;
return v * v;
}),
_.filter(v => v < 20),
_.take(2),
console.info); // [1, 4]
console.info(count); // 6
컬렉션 중심의 지연 평가는 수집, 필터링, 찾기 이 3가지 유형의 함수들이 연속적으로 준비되었을 때 재배치하여 평가하는 것입니다.
수집과 필터링을 통해 지연 평가하고 있다가 찾기를 통해 끝내는 형태이며, 이 경우 map + filter 에서 하는 일이 복잡할수록 + 찾기 가 값을 완성하는 데 필요한 값이 적을수록 (L.take 에 들어갈 인자가 작을수록 이라고 봐도 됨) 효과가 높습니다.
지연 평가의 로직에서 제일 중요한 것은 일급 함수와 순수 함수 개념입니다.
일급 함수 개념이 있어야지만 함수를 값으로 받아 두었다가 최적화할 수 있는 시점으로 미뤄 실행할 수 있고, 외부 상태를 변경하지 않는 순수 함수여야만 실행 시점이 자유로워집니다.
map -> filter -> take 만이 지연 평가의 시나리오는 아니며 이것은 온전히 코드를 작성하는 개발자가 결정할 수 있습니다.
순수 함수를 정의한 후, 함수 객체를 다루면서 엄격하거나 느긋한 평가를 원하는 시점에 하도록 하는 것이 함수형 프로그래밍 입니다.
지금까지는 엄격한 평가 코드엔 _.map, _.filter 등을 사용하고 지연 평가 코드엔 L.map, L.filter 등을 사용했습니다.
이렇게 작성할 수도 있겠지만, 지연 평가 동작을 동적으로 가져가는 부분에 대해 다음 포스팅에서 짧게 보도록 하겠습니다.
'Backend > 함수형 자바스크립트' 카테고리의 다른 글
값 다루기 (0) | 2021.11.06 |
---|---|
지연 평가 (3) (0) | 2021.10.30 |
지연 평가 (1) (0) | 2021.10.17 |
Partial.js (3) (0) | 2021.10.09 |
Partial.js (2) (0) | 2021.10.02 |