본문 바로가기

Web[웹]/ES6+ 함수형 언어

[JS: ES6+] 제너레이터/이터레이터 프로토콜로 구현하는 지연 평가 (2)

 

#1 결과를 만드는 함수 reduce, take

 

reduce를 통해 구현하는 함수

 

먼저, 객체로부터 url의 queryString을 만들어내는 함수를 만들어 볼 것이다.

 

const queryStr = obj => obj;
console.log(queryStr({ limit : 10, offset: 10, type: 'notice' }));
// {limit: 10, offset: 10, type: "notice"}

 

이제 key와 value를 추출하는 코드를 작성해 볼 것이다.

 

const queryStr = obj => go(
   obj,
   Object.entries, // key, value를 entries로 변환
   map(([k, v]) => `${k}=${v}`), // 구조분해를 통해 key와 value를 받음
   reduce((a, b) => `${a}&${b}`) // reduce를 통해 세퍼레이터를 &로 줌
);
console.log(queryStr({ limit : 10, offset: 10, type: 'notice' }));
// limit=10&offset=10&type=notice

 

queryStr은 obj를 받아서 그대로 obj로 전달하기때문에 아래처럼 pipe로 대체가 가능하다.

 

const queryStr = pipe(
   Object.entries,
   map(([k, v]) => `${k}=${v}`),
   reduce((a, b) => `${a}&${b}`)
);
console.log(queryStr({ limit : 10, offset: 10, type: 'notice' }));
// limit=10&offset=10&type=notice

 

이어서 reduce를 통해 join 함수를 만들어 볼 것이다. 이 조인 함수는 이터러블 값을 다 순회하면서 축약이 가능하기 때문에 array에 있는 join함수 보다 훨씬 다형성이 높다.

 

const join = curry((sep = ',', iter) =>        // join함수에서 기본값 ','
   reduce((a, b) => `${a}${sep}${b}`, iter));

const queryStr = pipe(
   Object.entries,
   map(([k, v]) => `${k}=${v}`),
   join('&')
);
console.log(queryStr({ limit : 10, offset: 10, type: 'notice' }));
// limit=10&offset=10&type=notice

 

다시말해서 위에서 선언한 join 함수는 배열이 아닌 경우에도 join 함수를 사용 할 수 있기 때문에 다형성이 높다.

 

아래 코드를 보자.

 

function* a(){
   yield 10;
   yield 11;
   yield 12;   
   yield 13;
}

console.log(a().join(' - ')); // 결과를 만들 수 없다.
console.log(join(' - ', a())); // 10 - 11 - 12 - 13

 

위와 같은 제너레이터함수가 정의되어 있을 때, 일반적인 join 함수로는 결과를 만들 수 없지만, 위에서 선언한 join으로는 결과를 만들 수 있다.

 

그리고 만든 조인 함수는 reduce를 통해 축약을 했기 때문에, 이터러블 트로토콜을 따르고 있다는 말이여서, join에게 가기전에 만들어지는 값들을 지연할 수 있다. 그래서 map을 L.map이여도 동일한 결과를 나타낸다. 그리고 entries역시도 다음과 같이 선언하면 지연이 가능하다.

 

L.entries = function* (obj) {
   for (const k in obj) yield [k, obj[k]];
};

const join = curry((sep = ',', iter) =>        // join함수에서 기본값 ','
   reduce((a, b) => `${a}${sep}${b}`, iter));

const queryStr = pipe(
   L.entries,
   L.map(([k, v]) => `${k}=${v}`),
   join('&')
);
console.log(queryStr({ limit : 10, offset: 10, type: 'notice' }));
// limit=10&offset=10&type=notice

 


take를 통해 구현하는 함수

 

이어서, take를 통해 find함수를 만들어 볼 것이다.

 

const users = [
   { age: 32 },
   { age: 31 },
   { age: 37 },
   { age: 28 },
   { age: 25 },
   { age: 32 },
   { age: 31 },
   { age: 37 }
];

const find = (f, iter) => go(
   iter,
   filter(f),
   take(1), //하나의 값만 꺼내주는 역할
   ([a]) => a //배열을 꺼내서 주는 역할
);

console.log(find(u => u.age < 30, users));
// {age: 28}

 

위에서 작성한 find 함수를 보면, 한가지 아쉬운 점이 있다.

 

/* 비효율적인 코드 */
const find = (f, iter) => go(
   iter,
   filter(a=> (console.log(a), f(a))),
   take(1),
   ([a]) => a
);

console.log(find(u => u.age < 30, users));
/*
{age: 32}
{age: 31}
{age: 37}
{age: 28}
{age: 25}
{age: 32}
{age: 31}
{age: 37}
{age: 28}
*/

 

filter에 콘솔을 찍어보면, take 하기 전에 모든 값들을 다 조회 함을 알 수 있다. 이는 매우 비효율적이므로 filter앞에 L을 붙여서 해당하는 값을 찾는 때가 딱 되면 조회를 멈춰서, 효율적으로 값을 찾을 수 있다.

 

/* 효율적인 코드 */
const find = (f, iter) => go(
   iter,
   L.filter(a=> (console.log(a), f(a))),
   take(1),
   ([a]) => a
);

console.log(find(u => u.age < 30, users));
/*
{age: 32}
{age: 31}
{age: 37}
{age: 28}
{age: 28}
*/

 

아래 코드는 위 코드를 curry로 감싸서 깔끔하게 정리한 코드다.

 

const find = curry((f, iter) => go(
   iter,
   filter(a=> (console.log(a), f(a))),
   take(1),
   ([a]) => a));

console.log(find(u => u.age < 30)(users));

 

다음과 같이도 나타낼 수 있다.

 

go(users,
   L.map(u => u.age),
   find(n => n < 30),
   console.log);
// 28

 


 

#2  지연성 / 이터러블 중심의 코드 다루기

 

L.map, L.filter로 map과 filter 만들기

 

앞에서 배웠던 map, filter를 L.map, L.filter 와 take 조합으로  만들어보자.

먼저,  take를 통해 map을 만드는 과정을 보자

 

const map = curry((f, iter) => go(
   iter,
   L.map(f),
   take(Infinity)
));
//↓↓↓
const map = curry((f, iter) => go(
   L.map(f, iter),
   take(Infinity)
));
//↓↓↓
const map = curry(pipe(L.map, take(Infinity)));

 

filter도 같은방법으로 작성해서 아래와 같이 전부 정리했다.

 

const takeAll = take(Infinity);

/* L.map + take 로 map 만들기 */
const map = curry(pipe(L.map, takeAll));
console.log(map(a => a + 10, L.range(4)));
// [10, 11, 12, 13]

/* L.filter + take 로 filter 만들기 */
const filter = curry(pipe(L.filter, takeAll));
console.log(filter(a => a % 2, L.range(4)));
// [1, 3]

/* 앞에서 선언했던 L.map과 L.filter */
L.map = curry(function* (f, iter) {
    for (const a of iter) {
      yield f(a);
    }
  });
  
L.filter = curry(function* (f, iter) {
  for (const a of iter) {
    if (f(a)) yield a;
  }
});

 


 

L.flatten, flatten

 

flatten는 하나의 배열 안에 있는 묶인 배열을 펼쳐서 하나의 배열로 만드는 역할을 해준다.

 

아래와 같이 동작을 한다.

console.log([...[1, 2], 3, 4, ...[5, 6], ...[7, 8, 9]]);
//[1, 2, 3, 4, 5, 6, 7, 8, 9]

 

이 함수를 구현해보자.

 

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;
   }
};

var it = L.flatten([[1, 2], 3, 4, [5, 6], [7, 8, 9]]);
console.log([...it]);
//[1, 2, 3, 4, 5, 6, 7, 8, 9]

console.log(take(3, L.flatten([[1, 2], 3, 4, [5, 6], [7, 8, 9]])));
// [1, 2, 3]

 

L.flatten 뿐만 아니라 take의 조합으로 즉시평가할수 있는 flatten도 만들 수 있다.

 

const flatten = pipe(L.flatten, takeAll);
console.log(flatten([[1, 2], 3, 4, [5, 6], [7, 8, 9]]));
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

 

yield *를 활용하면 다음과 같이 코드를 변경 할 수 있습니다.

 

/* yield 사용 X */
L.flatten = function* (iter) {
   for(const a of iter) {
      if(isIterable(a)) for(const b of a) yield b
      else yield a;
   }
};

/* yield* 사용 O */
L.flatten = function* (iter) {
   for(const a of iter) {
      if(isIterable(a)) yield* a;
      else yield a;
   }
};

//"yield *iterable"은 "for (const val of iterable) yield val;" 과 같습니다.

 

만약 깊은 이터러블을 펼치고 싶다면 L.deepFlat도 구현 가능합니다.

 

L.deepFlat = function* f(iter) {
   for (const a of iter) {
      if (isIterable(a)) yield* f(a);
      else yield a;
   }
};
console.log([...L.deepFlat([1, [2, [3, 4], [[5]]]])]);
// [1, 2, 3, 4, 5];

 


 

L.flatMap, flatMap

 

flatMap는 map과 flatten을 동시에 하는 함수이다. flatMap 있는 이유는 자바스크립트가 기본적으로 지연적으로 동작하지 않기 때문이다.

 

 

console.log([[1, 2], [3, 4], [5, 6, 7]].flatMap(a => a.map(a => a * a)));
// [1, 4, 9, 16, 25, 36, 49]

console.log(flatten([[1, 2], [3, 4], [5, 6, 7]].map(a => a.map(a => a * a))));
// [1, 4, 9, 16, 25, 36, 49]

 

flatMap은 map한 값에 flatten한 것과 동일한 값을 가짐을 볼 수 있다. 그럼 flatMap은 왜 있을까?? map과 flatten을 하는것은 매우 비효율적이기 때문이다. 아래 코드를 보면 왜 비효율인지 알 수 있다.

 

/* flatten 동작 전 map만 했을 경우 */
console.log([[1, 2], [3, 4], [5, 6, 7]].map(a => a.map(a => a * a)));
// [Array(2), Array(2), Array(3)]

 

위에서 바로 flatMap하면 바로 원하는 해당 값이 나오지만, map을 한 후 flatten을 하면 배열을 만든 후에 flatten을 하기 때문에 약간의 비효율이 생긴다.

 

좀 더 다형성이 높은 L.flatMap을 만들어 보자.

 

L.flatMap = curry(pipe(L.map, L.flatten));

var it1 = L.flatMap(map(a => a * a), [[1, 2], [3, 4], [5, 6, 7]]);
console.log([...it1]); // [1, 4, 9, 16, 25, 36, 49]

var it2 = L.flatMap(a => a, [[1, 2], [3, 4], [5, 6, 7]]);
console.log([...it2]); // [1, 2, 3, 4, 5, 6, 7]

 

L.flatMap이 있으면 역시 이를 활용해 즉시평가하는 flatMap을 또 만들 수 있다.

 

const flatMap = curry(pipe(L.map, flatten));
console.log(flatMap(a => a, [[1, 2], [3, 4], [5, 6, 7]]));
// [1, 2, 3, 4, 5, 6, 7]

 

 


 

2차원 배열 다루기

 

위에서 배운 코드를 기반으로 이제 다음과 같이 2차원 배열을 편히 다룰 수 있다.

 

const arr = [
   [1, 2],
   [3, 4, 5],
   [6, 7, 8],
   [9, 10]
];

go(arr,
   L.flatten,
   L.filter(a => a % 2),
   L.map(a => a * a),
   take(4),
   reduce(add),
   console.log);
//84

 


 

지연성 / 이터러블 중심 프로그래밍 실무적인 코드

 

한번 더 나아가 이번엔 실무적인 코드를 활용한 예제로  다시알아보자.

 

다음과 같은 사용자코드가 있다고 보자.

 

var users = [
   {
      name: 'a', age: 21, family: [
         {name: 'a1', age: 53}, {name: 'a2', age: 47},
         {name: 'a3', age: 16}, {name: 'a4', age: 15}
      ]
   },
   {
      name: 'b', age: 24, family: [
         {name: 'b1', age: 58}, {name: 'b2', age: 51},
         {name: 'b3', age: 19}, {name: 'b4', age: 22}
      ]
   },
   {
      name: 'c', age: 31, family: [
         {name: 'c1', age: 64}, {name: 'c2', age: 62}
      ]
   },
   {
      name: 'd', age: 20, family: [
         {name: 'd1', age: 42}, {name: 'd2', age: 42},
         {name: 'd3', age: 11}, {name: 'd4', age: 7}
      ]
   }
];

 

아래와 같이 2차원배열에서 다뤘던 방법처럼 실무에서도 똑같이 적용이 가능하다.

 

go(users,
   L.map(u => u.family),
   L.flatten,
   L.filter(u => u.age < 20), //아직 성인이 아닌 가족을 뽑음
   L.map(u => u.age),
   take(4), // 16, 15, 19, 11
   reduce(add), 
   console.log); // 61