3. 컬렉션 중심 프로그래밍
컬렉션은 배열과 같은 돌림직한 데이터들을 다루는 것을 의미한다.
함수형 프로그래밍에서 빛을 발하는 방식이다.
const users = [
{ id: 1, name: 'ID', age: 36 },
{ id: 2, name: 'BJ', age: 32 },
{ id: 3, name: 'JM', age: 32 },
{ id: 4, name: 'PJ', age: 27 },
{ id: 5, name: 'HA', age: 25 },
{ id: 6, name: 'JE', age: 26 },
{ id: 7, name: 'JI', age: 31 },
{ id: 8, name: 'MP', age: 23 },
];
수집하기 - map, values, pluck#
- _map
- values
- _identity
- _curryr 를 이용한 _values
- pluck
_map(users, function(user) {
return user.name;
});
object 의 값들을 꺼내는 역할을 한다.
배열은 Input 과 output 결과가 같기 때문에 의미가 없다.
function _values(data) {
return _map(data, function(val) { return val; });
}
console.log(users[0]); // { id: 1, name: 'ID', age: 36 }
console.log(_keys(users[0])); // [ 'id', 'name', 'age' ]
console.log(_values(users[0])); // [ 1, 'ID', 36 ]
values 의 내부를 _identity 함수로 대체해 보자.
function _identity(val) {
return val;
}
function _values(data) {
return _map(data, _idenity);
}
앞서 _map 은 _curryr 를 이용하였다.
function _curryr(fn) {
return function (a) {
return arguments.length === 2 ? fn(a, b) : function(b) { return fn(b, a)}
}
}
var _map = _curryr(_map);
결과적으로 _values 를 _map 을 이용하여 다음과 같이 작성할 수 있다.
_map(_identity)(users[0]); // [ 1, 'ID', 36 ]
expect
_pluck(users, 'age'); // [33, 22, 11, ...]
function _pluck(data, key) {
return _map(data, function(obj) {
return obj[key];
})
}
// same as
function __pluck(data, key) {
return _map(data, _get(key));
}
거르기 - filter, reject, compact#
reject 는 필터를 반대로 동작시킨 것이라고 볼 수 있다.
- _filter
- _reject
- _reject & _negate
- _compact
filter 는 true 인 것을 갖는다.
_filter(users, function(users) {
return user.age > 30;
});
filter 와 반대로 false 인 것을 갖는다.
_reject(users, function(user) {
return user.age > 30;
});
function _reject(data, predi) {
return _filter(data, function(val) {
return !predi(val);
})
}
/*
[
{ id: 4, name: 'PJ', age: 27 },
{ id: 5, name: 'HA', age: 25 },
{ id: 6, name: 'JE', age: 26 },
{ id: 8, name: 'MP', age: 23 }
]
*/
function _negate(func) {
return function(val) {
return !func(val);
}
}
function _reject(data, predi) {
return _filter(data, _negate(predi));
}
truthy 한 값만 갖는다.
var _filter = _curryr(_filter);
var _compact = _filter(_identity);
_compact([1, 2, 0, false, null, {}]); // [ 1, 2, {} ]
찾아내기 - find, find_index, some, every#
- _find
- _find_index
- 다른방법들
- _some
- _every
배열 값 중 조건이 처음으로 true 로 평가 되는 값 하나를 리턴
function _find(list, predi) {
var keys = _keys(list);
for (var i = 0, len = keys.length; i < len; i++) {
var val = list[keys[i]];
if (predi(val)) return val;
}
return list;
}
console.log(
_find(users, function(user) {
return user;
})
); // { id: 1, name: 'ID', age: 36 }
find 와 동일하게 동작하는데, 해당하는 값의 인덱스를 리턴.
function _find_index(list, predi) {
var keys = _keys(list);
for (var i = 0, len = keys.length; i < len; i++) {
if (predi(list[keys[i]])) return i;
}
return -1;
}
console.log(
_get(_find(users, function(user) {
return user.id == 50;
}), 'name');
);
_go(users,
_find(function(user) { return user.id == 50; }),
_get('name'),
console.log
)
하나의 조건이라도 만족하는 값이 있으면 true 를 리턴
_some([1, 2, 5, 10, 20], function(val) {
return val > 10;
});
console.log(
_some([1, 2, 0, 10]),
_some([null, false, 0])
)
function _some(data, predi) {
// predi 가 인자로 없다면,
// 안의 값들 중 true 로 평가되는 것이 하나라도 있는지 검사할 수 있다.
predi = predi || _identity
return _find_index(data, predi) != -1;
}
모든 값이 조건에 true 를 만족해야 true 를 리턴
console.log(_every([1, 2, 5, 10, 20], function(val) {
return val > 10;
})); // false
console.log(_every([12, 24, 5, 10, 20], function(val) {
return val > 3
})); // true
function _every(data, predi) {
return _find_index(data, _negate(pred || _identity)) == -1;
}
접기 - reduce, min_by, max_by#
reduce 를 함수형 관점에서 사용하고, min_by 와 max_by 를 만들어 본다.
- _min _max
- _min_by _max_by
배열의 값 중에 젤 작은 값을 리턴
_min([1, 2, 4, 10, 5, -4]); // -4
function _min(data) {
return _reduce(data, function(a, b) {
return a < b ? a : b;
});
}
배열의 값 중에 젤 큰 값을 리턴
_max([1, 2, 4, 10, 5, -4]); // 10
function _max(data) {
return _reduce(data, function(a, b) {
return a > b ? a : b;
});
}
_min _max 는 값을 직접 비교하기 때문에 다형성이 상대적으로 낮다.
_min_by _max_by 는 _filter 와 같은 다른 함수처럼 보조함수를 갖추었기 때문에 무엇으로 비교할지 정해줄 수 있다.
_min_by
console.log(_min_by([1, 2, 4, 10, 5, -11], Math.abs)); // 1
function _min_by(data, iter) {
return _reduce(data, function(a, b) {
return iter(a) < iter(b) ? a : b;
});
}
_max_by
console.log(_max_by([1, 2, 4, 10, 5, -11], Math.abs)); // -11
function _max_by(data, iter) {
return _reduce(data, function(a, b) {
return iter(a) > iter(b) ? a : b;
});
}
_max_by(users, function(user) {
return user.age;
}); // { id: 1, name: 'ID', age: 36 }
_map 을 사용한 비교
_max(_map([1, 2, 4, 10, 5, -11], Math.abs)); // 11
_go 를 이용해 함수조합을 이용한 함수형 프로그래밍
_go(users,
_filter(user => user.age >= 30),
_min_by( function(user) {
return user.age;
}),
console.log); // { id: 7, name: 'JI', age: 31 }
_go(users,
_filter(user => user.age >= 30),
_min_by(_get('age')),
console.log); // { id: 7, name: 'JI', age: 31 }
_go(users,
_reject(user => user.age >= 30),
_max_by(_get('age')),
console.log); // { id: 4, name: 'PJ', age: 27 }
_go(users,
_reject(user => user.age >= 30),
_max_by(_get('age')),
_get('name'),
console.log); // PJ
접기 - group_by, count_by, 조합#
- _group_by
- _count_by
- _pairs, _each의 변형 키-값 쌍 출력하기
- 조합
expected
_group_by(users, function(user) {
return user.age;
});
var users2 = {
36: [{ id: ...}],
32: [{ id: ...}, { id: ...}, { id: ...}],
// ...
}
_group_by
var _group_by = function(data, iter) {
return _reduce(data, function(grouped, val) {
var key = iter(val);
(grouped[key] = grouped[key] || []).push(val);
return grouped;
}, {})
};
execute
_go(
users,
_group_by(function(user) {
return user.age;
}),
console.log
)
/*
{
'23': [ { id: 8, name: 'MP', age: 23 } ],
'25': [ { id: 5, name: 'HA', age: 25 } ],
'26': [ { id: 6, name: 'JE', age: 26 } ],
'27': [ { id: 4, name: 'PJ', age: 27 } ],
'31': [ { id: 7, name: 'JI', age: 31 } ],
'32': [ { id: 2, name: 'BJ', age: 32 }, { id: 3, name: 'JM', age: 32 } ],
'36': [ { id: 1, name: 'ID', age: 36 } ]
}
*/
_push 분리
function _push(obj, key, val) {
(obj[key] = obj[key] || []).push(val);
return obj;
}
var _group_by = _curryr(function(data, iter) {
return _reduce(data, function(grouped, val) {
return _push(grouped, iter(val), val);
}, {})
});
나이대 별로 그룹화
_go(
users,
_group_by(function(user) {
return user.age - user.age % 10;
}),
console.log
);
/*
{
'20': [
{ id: 4, name: 'PJ', age: 27 },
{ id: 5, name: 'HA', age: 25 },
{ id: 6, name: 'JE', age: 26 },
{ id: 8, name: 'MP', age: 23 }
],
'30': [
{ id: 1, name: 'ID', age: 36 },
{ id: 2, name: 'BJ', age: 32 },
{ id: 3, name: 'JM', age: 32 },
{ id: 7, name: 'JI', age: 31 }
]
}
*/
성으로 그룹화
_go(
users,
_group_by(function(user) {
return user.name[0];
}),
console.log
);
var _head = function(list) {
return list[0];
}
_go(
users,
_group_by(_pipe(_get('name'), _head),
console.log
);
/*
{
I: [ { id: 1, name: 'ID', age: 36 } ],
B: [ { id: 2, name: 'BJ', age: 32 } ],
J: [
{ id: 3, name: 'JM', age: 32 },
{ id: 6, name: 'JE', age: 26 },
{ id: 7, name: 'JI', age: 31 }
],
P: [ { id: 4, name: 'PJ', age: 27 } ],
H: [ { id: 5, name: 'HA', age: 25 } ],
M: [ { id: 8, name: 'MP', age: 23 } ]
}
*/
_group_by 로 인해 만들어진 key 의 배열 값이 몇 개인지 보여줌.
_count_by
var _count_by = function(data, iter) {
return _reduce(data, function(count, val) {
var key = iter(val);
count[key] ? count[key]++ : count[key] = 1;
return count;
}, {});
};
var _inc = function(count, key) {
count[key] ? count[key]++ : count[key] = 1;
return count;
}
var _count_by = function(data, iter) {
return _reduce(data, function(count, val) {
return _inc(count, iter(val));
}, {});
};
_go(
users,
_count_by((user) => user.age),
console.log
); // { '23': 1, '25': 1, '26': 1, '27': 1, '31': 1, '32': 2, '36': 1 }
expected
_go(
users,
_count_by(user => user.age - user.age % 10),
_map((count, key) => `${key}대는 ${count} 명 입니다.`),
console.log
); // [ '20대는 4 명 입니다.', '30대는 4 명 입니다.' ]
_each 의 변형
function _each(list, iter) {
var keys = _keys(list);
for (var i = 0, len = keys.length; i < len; i++) {
iter(list[keys[i]], keys[i]);
}
}
function _map(list, mapper) {
const new_list = [];
_each(list, function(val, key) {
new_list.push(mapper(val, key));
});
return new_list;
}
var f1 = _pipe(
_count_by(user => user.age - user.age % 10),
_map((count, key) => `<li>${key}대는 ${count} 명 입니다.</li>`),
list => `<ul>${list.join('')}</ul>`,
document.write.bind(document)
)
_go(users, _reject(user => user.age < 20), f1);
/*
* 20대는 4 명 입니다.
* 30대는 4 명 입니다.
*/