지연성을 가지는 함수들
지연성은 es6부터는 공식적으로 약속된 규약이 되어, 지연성을 다루는건 js의 고유한 규칙이 되었다. (결합과 합성이 가능)
1. range 함수
const range = n => {
let res = [];
for(let i = 0 ; i < n ; i ++) {
res.push(i+1);
}
return res;
}
const L = {}
L.range = function *(n) {
let i = -1;
while (++i < n) yield i;
return;
}
console.log(range(5)); // [1,2,3,4,5]
console.log(L.range(5)); //Object [Generator] {}
const Literable = L.range(5)
console.log(Literable.next()); //{ value: 0, done: false }
위의 두 range는 중요한 차이점이 있다. 출력을 해보면 알 수 있다.
range는 바로 배열로 평가가되지만 L.range는 이터러블을 반환하며 해당 이터러블을 순회하기 전까지, 다시말해 이 이터러블에 접근하기 전까지 평가가 지연된다. 접근한다고 해도 한번에 모든 배열을 생성해서 반환해주는 것이 아닌 next() 가 호출될때마다 yield를 통해 값을 하나하나씩 전달한다. ⇒ 필요할때까지 평가를 미루는 것.
구체적인 예를 들어보면 range와 L.range의 차이는 range(10000) 해놓고 5까지만 더하기 하면 range는 10000까지 다 만들어놓고 5까지 더하지만 L.range(10000)은 최대 10000까지 뽑을수있는 상태에서 5까지만 빼서 더하므로 실행시간도 훨씬 빠르다
2. L.map
L.map = function *(f, iter) {
for(const a of iter ) {
yield f(a);
}
}
L.map = curry(function *(f, iter) {
iter = iter[Symbol.iterator]();
let cur;
while(!(cur = iter.next()).done) {
let a = cur.value;
yield f(a);
}
})
// 위와 같이 해놓으면 iter = iter[Symbol.iterator]();
// 를 통해서 이터러블을 이터레이터로 만들 수 있다.
// Well formed 이터레이터라면 자기 자신을 반환할 것.
이 map은 이터레이터를 받아서 원하는 시점에 전달 받은 함수를 적용하여 yield를 통해 값을 내놓는다. L.map의 반환 값은 이터러블이므로 원하는 시점에 원하는 형태로 결과값을 받아오는 것이 가능하다.
3. L.filter
L.map 과 거의 유사하다
L.filter = function *(f, iter) {
for(const a of iter ) {
if(f(a)) yield a;
}
}
next를 할때마다 원하는 조건에 충족되는 값만 나온다. 이제 이 지연성을 가지는 L.range, L.map, L.filter들을 조합해서 기존의 함수들과 어떤 차이가 있는지 알아보자.
기존 함수들과의 차이점
//즉시 평가
go(range(10),
map(a => a + 2),
filter(a => a > 6),
take(4),
console.log
)
//지연
go(L.range(10),
L.map(a => a + 2),
L.filter(a => a > 6),
take(4),
console.log
)
우선 위처럼 간결한 형태로 go에 넣기 위해 L.filter와 L.map, take 함수를 curry로 감쌌다. 먼저 즉시평가는 어떻게 코드가 진행되는지 파악해보자
range(10)의 결과는 바로 [0,~~, 9] 의 배열로 평가될 것이다. 그리고 바로 그 다음의 map 함수의 인자로 전달된다. map은 이 배열을 전달받아서 2씩 더한뒤에 다시 배열을 리턴한다. filter 함수는 map에서 전달한 배열을 받아서 로직을 수행하고 결과를 리턴한다. 리턴된 결과 역시 즉시 배열로 평가되는 값이며 take에게 전달한다.
이처럼 즉시평가 로직에서는 결과가 모두 '평가가 완료된 배열 값'이 인자로 오고가게 된다.
그렇다면 지연평가는 어떻게 코드가 동작할까??
L.range에서는 우선 제네레이터 함수를 통해 최대 9까지 평가될 수 있는 이터레이터를 반환한다. L.map은 L.range로부터 받은 이터레이터를 통해 이터레이터를 만들어 반환한다. L.filter 또한 L.map으로 부터 받은 이터레이터를 가지고 이터레이터를 만들어서 반환한다. 이들이 반환한 이터레이터가 최초로 평가를 요구하는 시점은 take 내부에서 next 함수를 호출했을 때이다. 그러면 L.filter은 값을 yield 해야하니 while 내부 로직이 실행되고 while 내부 로직에는 next 함수가 있어서 L.map으로부터 전달받은 이터레이터의 next를 호출한다. 마찬가지로 L.map도 yield를 해야하니 while 내부에서 next를 호출하며 L.range로부터 받은 이터레이터에서 값을 하나 꺼내온다. (마치 이벤트 버블링처럼,,)
이렇게 위로 거슬러 올라가며 값을 받아서 각자의 로직을 수행하며 다시 아래로 전달한다. 꺼내온 값에 자신의 로직을 수행한 후 결과를 filter에게 전달하고 filter은 본인이 전달받은 조건에 해당 값이 참인지 아닌지를 판단하여 참이라면 take에게 반환한다. 이러한 과정으로 take는 자신이 받은 값의 갯수가 4개가 되면 console.log에게 결과를 전달한다.
계산의 방향이 즉시평가는 가로로 진행되었다면 지연평가는 세로로 진행되는 것이다.
이런 지연성을 가지는 map, filter 계열의 함수들도 결합법칙을 만족한다.
지연성 활용예시1
L.entries = function *(obj) {
for(const k in obj) yield [k, obj[k]];
};
const join = curry(function(join, iter) {
return reduce((acc, a) => acc + join + a, iter);
})
const queryStr = pipe(
L.entries,
L.map(([k,v]) => `${k} : ${v}`),
join('/'),
console.log
);
entries와 join 함수를 이터러블 프로토콜을 따르도록 해서 일반적인 join 함수(Array Prototype의 join)와는 다르게 꼭 배열이 아니더라도 이터레이터라면 동작이 가능해서 다형성이 조금 더 높다고 할 수 있다.
지연성 활용예시2
const users = [
{age:1},
{age:41},
{age:25},
{age:55},
{age:11},
{age:111},
{age:51},
{age:151},
{age:25},
{age:74},
{age:19},
]
const find = (f, iter) => go(
iter,
filter(a => { console.log(a); return f(a)}),
take(1),
([a]) => a
)
console.log(find(a => a.age == 11 ,users));
find는 이터러블을 받아서 동작하고 find 내부는 L.filter로 지연성을 가질 수 있기 때문에 find의 인자로 평가가 완료된 값이 들어오든 평가가 미뤄진 값이 들어오든 동작이 가능하게 된다.
map, filter 함수를 지연성을 갖도록 수정해보자.
const map = curry((f, iter) => {
let res= [];
iter = iter[Symbol.iterator]();
let cur;
while(!(cur = iter.next()).done) {
const a = cur.value;
res.push(f(a));
}
return res;
})
//L.map을 사용하는 map으로 변경
const map = curry((f, iter) => go(
L.map(f, iter),
take(Infinity)
));
//첫번째 함수의 인자가 전달받은 인자와 같다면 미리 만들어뒀던 pipe 로 변경이 가능함.
const map = curry(pipe(L.map, take(Infinity)))
//같은 방법으로 filter 구현
const filter = curry(pipe(L.filter, take(Infinity)))
지연성을 가지는 함수들2
1. L.flatten
const isIterable = a => a && a[Symbol.iterator];
L.flatten = function*(iter) {
for(const a of iter ){
if (isIterable(a)) for(const b of a) yield b;
else yield a;
}
}
const flatten = pipe(L.flatten, take(Infinity));
console.log(flatten([1,2,3,[4,5,6]]))
flatten은 L.flatten을 사용하고 L.flatten은 지연적으로 동작하기 때문에 아래와 같이 take를 사용하면 값을 3개 뽑아낼 때까지만 동작하는 효율적인 코드가 될 것이다.
console.log(take(3, flatten([1,2,3,[4,5,6]])))
위의 L.flatten은 depth level 1까지만 펼칠 수 있는데 모두 펼치고 싶으면 아래와 같이 하면 된다.
L.flatten = function *f(iter) {
for(const a of iter ){
if (isIterable(a)) yield *f(a)
else yield a;
}
}
2. L.flatMap
L.flatMap = curry(pipe(L.map, L.flatten))
let it = L.flatMap(map(a => a * 2), [[1,2,3],[4,3,4]] );
console.log(...it)
해당 flatMap 함수는 2차원 배열을 flat 하게 만들어주는 함수이다.
L.map에 처음 들어가게 되면 각 요소가 배열인 이터레이터 일 것이다. L.map이 넘겨받은 함수는 map이므로 각 배열이 map(a⇒a*2)( [ 배열 ] ) 의 형태로 실행될 것이고, 위의 코드를 보면 map은 다시 L.map을 호출하는 것을 볼 수있다. 그래서 한번 더 이 배열이 펼쳐지게 되는 것이다.
첫 L.map에서 이중배열 → 배열
map 함수 내부의 L.map에서 배열 → 개별요소
위의 지연성을 가진 함수들을 가지고 이제 2차원 배열을 다루는 것이 간편해졌다.
const arr = [
[1,2],
[3,4,5],
[5,7,7]
]
go(arr,
L.flatten,
L.filter(a => a > 3),
take(4),
console.log)
go(arr,
L.flatten,
L.filter(a => a > 3),
take(4),
L.map(a => a * 2),
reduce(add),
console.log)
이렇게 마음대로 중간 과정의 함수들을 마치 부품을 조립하듯 넣었다 뺐다 하면서 자유자재로 프로그래밍 하는 것이 가능하다. 함수를 가지고 레고를 조립하듯이,,,
아래와 같이 좀 복잡한 데이터가 있다고 가정했을 때에도 적용해볼 수 있다.
const person = [
{
name : 'lll',
school : 'ajou Univ',
phone : 'galaxy',
books : [
{
name : 'JPA',
price : 32523,
quantity : 2,
},
{
name : 'JPA2',
price : 15000,
quantity : 1,
}
]
},
{
name : 'lwh',
company : 'samsung',
phone : 'iphone',
books : [
{
name : 'watch',
price : 3253,
quantity : 21,
},
]
},
{
name : 'jwi',
school : 'AA univ',
phone : 'galaxy s10',
books : [
{
name : 'JPA',
price : 32523,
quantity : 2,
},
{
name : 'JPA2',
price : 15000,
quantity : 1,
}
]
},
{
name : 'wov',
school : 'dong univ',
phone : 'galaxy s20',
books : [
{
name : 'english',
price : 32990,
quantity : 10,
},
{
name : 'JPA3',
price : 15000,
quantity : 11,
}
]
},
]
go(
person,
L.filter(p => p.school), //학교에 재학중인 사람만
L.map(p => p.name), //이름을 뽑아서
takeAll, //평가를 통해 값을 만든 후에
join(' & '), //join 함수를 사용해서 묶기
console.log //출력 결과 : lll & jwi & wov
)
관련글
https://www.inflearn.com/course/functional-es6 수업을 들으며 정리한 글입니다
'함수형 프로그래밍' 카테고리의 다른 글
자바의 함수형 프로그래밍 (0) | 2021.09.24 |
---|---|
JS 함수형 프로그래밍 - 병렬처리 (0) | 2021.08.01 |
JS 함수형 프로그래밍 - 비동기, Promise [2] (0) | 2021.08.01 |
JS 함수형 프로그래밍 - 비동기, Promise [1] (0) | 2021.08.01 |
JS 함수형 프로그래밍 - 이터러블, 이터레이터, 제너레이터 (0) | 2021.08.01 |