함수형 프로그래밍 - 지연 평가(2)

jiseong·2022년 7월 6일
0

T I Learned

목록 보기
285/291

이전 포스팅에서의 reduce, take함수는 iterator에서 값을 하나씩 꺼내 결과를 만들어내는 함수로 생각하고 코드를 작성하면 유용하다. 그리고 그런 함수들을 이용한다면 조금 더 다형성이 높은 코드를 작성할 수 있다.

다형성이 높은 join

reduce함수를 활용한다면 array일때만 가능했던 기존의 join보다 다형성이 높은 join함수를 만들 수 있다. 그리고 iterable 프로토콜을 따르기 때문에 앞에서 배웠던 지연평가도 적용이 가능하다.

const join = curry((seq = ',', iter) => reduce((a, b) => `${a}${seq}${b}`, iter));

function *genfunc() {
  yield '010';
  yield '1234';
  yield '5678';
}

console.log(genfunc().join('-'));
// 제네릭 객체는 join함수를 사용할 수 없었다.
// TypeError: genfunc(...).join is not a function
console.log(join('-', genfunc()));
// 010-1234-5678

조금 더 효율적인 find

take함수를 활용하여 find함수를 만든다면 다음과 같이 만들 수 있다.

before

const users =[
  {age: 32,},
  {age: 31,},
  {age: 37,},
  {age: 29,},
  {age: 15,},
  {age: 40,},
  {age: 10,},
  {age: 50,},
  {age: 60,},
]

const find = (f, iter) => go(
  iter,
  L.filter(f),
  take(1) // 하나의 요소만을 꺼내는 함수
);

// 30세 미만인 유저를 찾는 find함수
console.log(find(u => u.age < 30, users));
// [ { age: 29 } ]

하지만 위에서 작성한 find함수는 한가지 아쉬운점이 있다.

해당 코드의 실행 순서를 따라가보면 결과값으로 하나의 요소만을 필요로 하지만 take하기 전에 모든 요소들을 다 조회하는 비효율적인 방식으로 동작하고 있는 것을 볼 수 있다. 그래서 이전에 작성했던 지연평가가 적용된 L.filter함수를 활용하여 원하는 시점에 평가함으로서 불필요한 연산을 줄일 수 있는 find함수로 바꾸었다.

after

const find = (f, iter) => go(
  iter,
  L.filter(f), // 지연평가
  take(1)
);

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

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

앞서 작성했던 map, filter를 L.map, L.filter 와 take 조합으로 조금 더 간단하게 작성할 수 있다.

take함수의 역할은 iterator의 값을 하나씩 평가하면서 배열에 push하다가 제한된 갯수 도달하거나 더이상 평가할 값이 없을 때 해당 배열을 반환하는 함수이다. 그렇기 때문에 Infinity로 무한정 돌려도 더이상 평가할 값이 없을 때 반환된다.

map before

const map = curry((f, iter) => {
  let res = [];

  for(const a of iter) {
    res.push(f(a));
  }
  return res;
});

map after

// 1단계
const map = curry((f, iter) => go(
  iter,
  L.map(f),
  take(Infinity)
));
// 2단계
// L.map의 인자로 iterable을 바로 넘겨도 된다.
const map = curry((f, iter) => go(
  L.map(f, iter),
  take(Infinity)
));
// 3단계
// 인자가 곧 그 다음 시작하는 함수의 인자이기 때문에 pipe함수로 변경
const map = curry(pipe(L.map, take(Infinity)));

filter before

const filter = curry((f, iter) => {
  let res = [];
  for(const a of iter) {
    if(f(a)) res.push(a);
  }
  return res;
});

filter after

// 1단계
const filter = curry((f, iter) => go(
  iter,
  L.filter(f),
  take(Infinity)
));
// 2단계
const filter = curry((f, iter) => go(
  L.filter(f, iter),
  take(Infinity)
));
// 3단계
const filter = curry(pipe(L.filter, take(Infinity)));

L.flatten, flatten

flatten 함수는 모든 하위 배열 요소를 하나의 배열에 이어붙인 역할을 하는 함수이다.

[[1, 2], 3, 4, [5, 6], [7, 8, 9]] -> [1, 2, 3, 4, 5, 6, 7, 8, 9]

기존의 flat()와의 결과의 차이는 없지만 array뿐만 아니라 iterable한 것들도 활용할 수 있도록 유연한 flat()함수를 작성한다면 아래와 같이 작성할 수 있다.

const isIterable = (a) => a && a[Symbol.iterator];

L.flatten = function *f(iter) {
  for(const a of iter) {
    if(isIterable(a)) yield *a; // 해당 코드는 아래와 동일하게 동작한다.
    // if(isIterable(a)) for (const b of a) yield b;
    else yield a;
  }
}

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

또한 즉시평가도 가능한 flatten함수도 구현할 수 있다.

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

만약 depth가 깊은 이터러블을 펼치고 싶다면 L.deepFlat함수도 쉽게 구현할 수 있다.

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

L.flatMap, flatMap

flatMap는 map과 flatten을 동시에 하는 함수로 기존에는 지연적으로 동작하지 않아 비효율적이기 때문에 사용하는 함수라고 한다.

const arr = [[1, 2], [3, 4], [5, 6, 7]];
console.log(arr.flatMap(a => a.map(a => a + 1)));
// [2, 3, 4, 5,6, 7, 8]

// 조금 더 효율성이 있는 flatMap을 구현
L.flatMap = curry(pipe(L.map, L.flatten));

go(
  arr,
  L.flatMap(map(a => a + 1)),
  take(Infinity),
  console.log
);
// [2, 3, 4, 5, 6, 7, 8]

L.flatMap이 있으면 마찬가지로 이를 활용하여 즉시평가하는 flatMap함수도 만들 수 있다.

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

실무적인 코드 예시

다음과 같이 회사 동료들의 가족구성원을 보여주는 데이터가 존재한다고 가정했을 때 지금까지 작성해왔던 함수의 조합으로 동료들의 가족구성원 중 성인이 아닌 사람들의 이름을 4번째까지 뽑는다면 다음과 같이 적용이 가능하다.

const colleagues = [
  {
    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}
    ]
  }
];
go(colleagues,
    L.flatMap(c => c.family),
    L.filter(c => c.age < 20), // 성인이 아닌 동료 가족구성원들
    L.map(c => c.name),
    take(4),
    console.log
  ); 
// [ 'a3', 'a4', 'b3', 'd3' ]

지금까지는 데이터 중심적인 사고방식으로 코드를 작성해왔다면 데이터를 어떻게 구성할지를 먼저 만들어내고 코딩하는 것이 아닌 조합되어있는 함수에 맞게 데이터를 구성하는 방식으로 코딩을 한다고 생각하면 될 것 같다. 그리고 이것이 함수가 우선순위에 있는 프로그래밍 방식인 함수형 프로그래밍이라고 한다.


Reference

0개의 댓글