- 컬렉션
- 3개의 대표 함수 map, reduce, findVal
- functional.es의 함수 계층
- map 함수
- map :: Functor f => (a -> b) - f a -> f b
- 대표 타입 5가지
- 서브 타입
- map을 적용할 수 없지만 안전하게 흘려주는 타입
- Functor로 보기 적합하지 않는 형
- Currying
- reduce 함수
- reduce :: Collection c => ((a, b) -> a) -> a -> c b -> a
- Currying
- findVal 함수
- findVal :: Collection c => (a -> Any | undefined) -> c a -> Any | undefined
- find :: Collection c => (a -> Boolean) -> c a -> a | undefined
- some, none, every
앞서 다음의 열거 가능한 값들을 '컬렉션'이라고 정의했습니다.
- JSON 데이터 타입 내의 object
- Array, Map, Set ...
- 그 외
[Symbol.iterator]()
가 구현된 모든 iterable과 iterator - Generator를 실행한 결과 값
FunctionalES의 map, filter, reduce 등은 위에 정의된 컬렉션을 지원합니다. functional.es의 주요 함수들을 확인하면서 '함수형 ES6+'의 기반이 될 '함수형 관점에서의 ES6+의 타입'에 대해 구체화해보도록 하겠습니다.
map, reduce, findVal
를 이용하면 컬렉션을 다루는 거의 대부분의 로직을 구현할 수 있습니다.
reduce (c)findVal map ___________________/ | \ / \ / \ / | \ (c)find or, and mapC series pipe groupBy filter / \ | go partition reject cond (c)some, (c)none, (c)every concurrency ... ... ... unless
map
은 함수 하나와 Functor를 받는 함수입니다. 여기서의 Functor는 ADT의 Functor는 아닙니다. ES6+에서 Functor 혹은 Collection으로 바라볼 수 있는 값들을 말합니다.
그 중 Functor로 바라보는 값은 다음과 같습니다.
- []
- {}
- Map
- Promise
- Function
위 5개의 타입은 인자로 들어온 형과 동일한 형으로 리턴합니다.
const { map } = Functional;
console.log( map(a => a + 1, [1, 2]) );
// [2, 3]
console.log( map(a => a + 1, {a: 1, b: 2}) );
// {a: 2, b: 3}
console.log( map(a => a + 1, new Map([['a', 1], ['b', 2]])) );
// Map(2) {"a" => 2, "b" => 3}
console.log( map(a => a + 1, Promise.resolve(1)) );
// Promise {<resolved>: 2}
const f = map(a => a + 1, _=> 1);
console.log(f);
// (..._) => f(coll(..._))
console.log(f());
// 2
map
의 구현은 다음과 같고 함수 계층에 나와있는 것처럼 reduce
로 구현되어있습니다.
const map = curry2((f, coll) =>
coll instanceof Function ?
(..._) => f(coll(..._))
:
coll instanceof Promise ?
coll.then(f)
:
coll instanceof Map ?
reduce((m, [k, v]) => then(val => m.set(k, val), f(v)), new Map, coll.entries())
:
hasIter(coll) ?
reduce((arr, v) => then(v=> (arr.push(v), arr), f(v)), [], coll)
:
isObject(coll) ?
reduce((o, [k, v]) => then(v => (o[k] = v, o), f(v)), {}, ObjIter.entries(coll))
:
[] // else
);
map에 coll
로 서브 타입이 들어오면 항상 배열을 리턴하도록 되어있습니다.
// String
console.log(
map(a => a + 1, "12")
);
// ["11", "21"]
// NodeList
console.log(
map(el => el.nodeName, document.querySelectorAll('head *'))
);
// ['META', 'TITLE', 'SCRIPT']
// Iterator 1
console.log(
map(a => a + 1, function *() {
yield 1;
yield 2;
} ())
);
// [2, 3]
map
이 Iterator를 받을 수 있다면, 할 수 있는 일이 많아집니다.
// Iterator 2
console.log(
map(
([key, val]) => [key.toUpperCase(), val + 1],
new Map([['a', 1], ['b', 2]]).entries())
);
// [["A", 2], ["B", 3]]
console.log(
new Map(map(
([key, val]) => [key.toUpperCase(), val + 1],
new Map([['c', 3], ['d', 4]]).entries()))
);
// Map(2) {"C" => 4, "D" => 5}
ES6+에서 지원하는 Iterator를 리턴하는 함수들이나 여러가지 Helper들을 활용하여 map
과 조합하면 위와 같은 일이 가능합니다. 위 코드는 구조 분해와 Iterator를 리턴하는 Map.prototype.entries
를 활용하여 key를 함께 받는 map
으로 응용했습니다.
functional.es의 map
은 ES6+의 Iterable/Iterator 프로토콜을 잘 따르고 있어, ES6+과 브라우저의 많은 Native Helpers나 그 외 자바스크립트 진영의 많은 라이브러리들과 조합성이 좋습니다.
console.log(map(v => v + 1, 1));
// []
console.log(map(v => v + 1, null));
// []
console.log(map(v => v + 1, undefined));
// []
Set
은 결과의 크기가 변경될 수 있기 때문에 map
에서 사용하기는 적합하지 않습니다.
console.log( map(a => a % 2, new Set([1, 2, 3, 4])) );
// [1, 0, 1, 0]
console.log( new Set([1, 0, 1, 0]) );
// Set(2) {1, 0}
map
을 실행하면서 인자로 함수 하나만을 전달하면 map
은 부분 적용됩니다.
const users = [
{id: 1, name: 'AA'},
{id: 10, name: 'BB'},
{id: 5, name: 'CC'}
];
const getIds = map(a => a.id);
console.log( getIds(users) );
// [1, 10, 5]
reduce
가 받는 Collection은 다음과 같습니다.
const { reduce, log } = Functional;
const add = (a, b) => a + b;
log( reduce(add, [1, 2]) ); // 3
log( reduce(add, 0, [1, 2]) ); // 3
log( reduce(add, 10, [1, 2]) ); // 13
log( reduce(add, 100, { a: 1, b: 2 }) ); // 103
log( reduce(add, 200, new Map([['a', 1], ['b', 2]])) ); // 203
log( reduce(add, 300, new Set([1, 2])) ); // 303
log( reduce(add, 400, (function *() {
yield 1;
yield 2;
}())) ); // 403
reduce
역시 자바스크립트의 값들과 다양한 응용이 가능합니다.
const info = {
title: "Function",
list: new Map([
['map', 'Functor f => (a -> b) - f a -> f b'],
['reduce', 'Collection c => ((a, b) -> b) -> b -> c a -> a']
])
};
log(
reduce(
(a, [k, v]) => `${a}\n - ${k} :: ${v}`,
info.title,
info.list.entries())
);
// 결과
// Function
// - map :: Functor f => (a -> b) - f a -> f b
// - reduce :: Collection c => ((a, b) -> b) -> b -> c a -> a
reduce
의 커링은 함수 하나를 받았을 때만 동작합니다.
const addAll = reduce(add);
console.log( addAll([1, 2, 3, 4]) );
// 10
자바스크립트에서 커링을 엄격히 하기에는 어려움이 있고, 자바스크립트와 커링은 잘 어울리지 않는 편입니다. 커링은 첫 번째 인자만을 부분 적용하는 curry2
정도로만 지원하고, 필요할 때는 화살표 함수를 이용하여 부분 적용을 하는식으로 활용하는 것이 실용적입니다. curry2
의 2는 최소 인자 개수를 말하며, 인자 개수가 2보다 작으면 커링을 하고, 인자 개수가 2개 이상 채워졌을 때 바로 실행한다는 의미입니다.
const addAllWith10 = _ => reduce(add, 10, _);
console.log( addAllWith10([1, 2, 3, 4]) );
// 20
화살표 함수를 이용한 다양한 부분 적용은 이후 글에서 더 소개하도록 하겠습니다.
findVal
함수는 f
가 undefined
가 아닌 값을 리턴하면 그 값을 findVal
의 결과로 리턴하는 함수입니다.
const { findVal } = Functional;
log(
findVal(a => a.dream, [
{ name: 'AA' },
{ name: 'BB', dream: 'rap' },
{ name: 'CC' }
])
);
// rap
함수의 인자와 리턴 값의 타입을 정하면, 그에 따른 함수 계층을 만들 수 있고, 그것은 안전합니다. 다음은 findVal
을 통해 만들 수 있는 함수들입니다.
const find = (f, coll) => findVal(a => f(a) ? a : undefined, coll);
log(
find(a => a.dream, [
{ name: 'AA' },
{ name: 'BB', dream: 'rap' },
{ name: 'CC' }
])
);
// { name: 'BB', dream: 'rap' }
find
함수와 작은 함수들을 조합하면, some, none, every
등을 만들 수 있습니다.
const isAny = a => a !== undefined;
const some = map(isAny, find);
log( some(a => a > 2, [1, 2, 4, 5]) );
// true
log( some(a => a < 2, [1, 2, 4, 5]) );
// true
log( some(a => a > 10, [1, 2, 4, 5]) );
// false
const isUndefined = a => a === undefined;
const none = map(isUndefined, find);
log( none(a => a > 2, [1, 2, 4, 5]) );
// false
log( none(a => a < 2, [1, 2, 4, 5]) );
// false
log( none(a => a > 10, [1, 2, 4, 5]) );
// true
const not = a => !a;
const every = (f, coll) => isUndefined(find(map(not, f), coll));
log( every(a => a < 2, [1, 2, 4, 5]) );
// false
log( every(a => a > 10, [1, 2, 4, 5]) );
// false
log( every(a => a > 0, [1, 2, 4, 5]) );
// true
some, none, every
는 문(statments) 없이 함수의 조합으로만 구성되어있습니다. 함수형 프로그래밍에서는 위 코드처럼 합수의 조합으로 문제를 해결합니다. 작은 문제를 해결하고, 해결책의 조합을 통해 더 복잡한 문제들을 해결해나갑니다.
아래 글은 서광열님의 글에서 발췌했습니다.
프로그래밍의 본질은 한 번에 풀 수 없는 크고 복잡한 문제를 작은 문제들로 나누어서 해결하고 그렇게 나온 결과물들을 조합하여 다른 문제를 해결하는 것을 말합니다.
여기서 좋은 프로그램의 가장 중요한 특성으로 조합성(composability)이 등장합니다. 우리는 계속해서 크고 복잡한 문제를 풀어야 하고, 또한 비슷하지만 조금은 다른 문제들을 풀어야 합니다. 앞서 만들어 놓은 산출물을 쉽게 조합하여 새로운 문제를 해결할 수 있다면 프로그래머의 생산성은 비약적으로 늘 수 있기 때문입니다.