함수형 프로그래밍 - 비동기 제어(2)

jiseong·2022년 7월 8일
0

T I Learned

목록 보기
287/291

오늘 보았던 강의는 이전 포스팅에 이어서 기존의 유틸함수를 Promise가 적용될 수 있도록 변경하는 과정들을 보여주는 내용이였다. 기존의 유틸함수에 프로미스를 적용하는 부분부터 조금씩 어려워지기 시작했는데 아무래도 여러번 봐야지 실제로 적용해볼 수 있을 것 같다...

L.map, map

기존의 L.map 함수의 로직은 다음과 같다.

L.map = curry(function* (f, iter) {
  for (const a of iter) {
    yield f(a);
  }
});

그리고 이 L.map을 활용하여 Promise를 적용한다면 생각했던 결과와 다르게 동작한다. 이를 정상적으로 연산이 되게끔 수정 해볼 것 이다.

go([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)],
  L.map(a => a + 10),
  take(2),
  console.log
);
// [ '[object Promise]10', '[object Promise]10' ]

해당 함수는 비교적 간단하게 이전 포스팅에서 구현해두었던 go1 함수를 활용하여 값이 프로미스인지를 체크하여 풀어주는 과정을 수행하면 된다.

L.map = curry(function *(f, iter) {
  for(const a of iter){
    yield go1(a, f);
  }
});
 const go1 = (a, f) => a instanceof Promise ? a.then(f) : f(a);

그런데 아직까지는 Promise 객체가 반환되기 때문에 take 함수도 이전 코드가 이행될 때까지 기다릴 수 있도록 변경해줘야 한다.

take

변경 전 take 함수의 로직은 다음과 같다.

const take = curry((l, iter) => {
  let res = [];
  iter = iter[Symbol.iterator]();
  let cur;
  while (!(cur = iter.next()).done) {
    const a = cur.value;
    res.push(a);
    if (res.length == l) return res;
  }
  return res;
});

이 부분의 수정도 이전 포스팅의 reduce함수와도 유사한데 while문 내부에서 프로미스 객체인 경우 then을 리턴하기 때문에 반복문이 제대로 돌 수 없는 문제를 해결하기 위해 유명함수로 작성후 즉시실행한다음 내부에서는 이 유명함수를 호출하여 재귀적으로 동작하게끔 구현했다.

const take = curry((l, iter) => {
  let res = [];
  iter = iter[Symbol.iterator]();
  return function recur(){ // 유명함수
     let cur;
     while (!(cur = iter.next()).done) {
        const a = cur.value;
        if(a instanceof Promise) return a.then(
           a => (res.push(a), res).length == l ? res : recur());
        res.push(a);
        if (res.length == l) return res;
     }
     return res;
  } (); // 즉시 호출
});

처음 본 문법

res.push(a);
res.length;
// 위의 로직을 한줄로도 작성이 가능하다.
// === (res.push(a), res).length

take 함수까지 수정하고 나면 처음에 기대했던 값과 동일한 결과를 얻을 수 있는 것을 볼 수 있다.

L.filter, filter

다음으로는 filter 함수이며 기존의 L.filter 함수의 로직은 다음과 같다.

L.filter = curry(function *(f, iter) {
  for(const a of iter){
    if(f(a)) a;
  }
});

그리고 이 L.filter 코드가 추가된다면 이전까지는 잘 작동되었던 코드가 또 다시 동작되지 않는 것을 볼 수 있는데 이 또한 filter 함수의 내부 로직이 비동기 코드를 처리할 수 없기 때문이다. 그래서 해당 부분도 수정 해보았다.

go([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)],
  L.map(a => a + 10),
  L.filter(a => a % 2), // 해당 코드가 추가됨
  take(2),
  console.log
);
// []

프로미스를 값으로써 다룰 수 있기 때문에 b라는 변수에 값을 담아두고 프로미스를 판별하여 동기코드와 분리하는 로직을 추가해주었다.

Promise.reject(nop) 부분은 비동기 코드가 이행된 이후에 b값이 존재하지 않으면 즉 필터링 조건에 부합하지 않으면 Promise.reject 상태로 귀결하여 그 다음 코드가 동작할 수 있게하기 위해서이며 다른 에러와는 다르게 구분할 수 있도록 nop이라는 심볼을 전달해주었다.

const nop = Symbol('nop');

L.filter = curry(function *(f, iter) {
  for(const a of iter){
    const b = go1(a, f);
    if(b instanceof Promise) yield b.then(b => b ? a : Promise.reject(nop));
    else if(b) yield a;
  }
});

여기까지 작성해주면 그래도 이전에는 [] 값을 볼 수 있는 것과 다르게 에러가 발생하는 것을 볼 수 있는데 값을 평가하는 take 함수에서 해당 에러를 핸들링 하여 다음 동작을 수행할 수 있도록 추가해주는 코드가 필요하기 때문이다.

그래서 take 함수 내부에 거부로 귀결된 값을 잡아낼 수 있는 catch문만 추가하여 해당 에러가 일반적인 에러가 아닌 특별한 에러인 nop인지를 판단하여 다음 로직을 수행할 수 있도록 작성해주면 된다.

const take = curry((l, iter) => {
 // 생략 ...
  while (!(cur = iter.next()).done) {
    const a = cur.value;
    if(a instanceof Promise) 
      return a
        .then(a => (res.push(a), res).length == l ? res : recur())
        // 아래 코드 한줄만 추가
        .catch(e => e == nop ? recur() : Promise.reject(e));
    res.push(a);
    if (res.length == l) return res;
  }
// 생략...
});

그러면 filter까지도 비동기 처리를 할 수 있는 함수가 될 수 있다.

reduce

마지막으로 reduce함수이다. 위에서 take함수는 nop인 에러를 핸들링하여 다음 동작을 수행할 수 있지만 결과를 만들 수 있는 또 다른 함수인 reduce는 불가능하다. 그래서 이 부분도 수정하려고 한다.

기존의 reduce 함수의 로직은 다음과 같다.

const reduce = curry((f, acc, iter) => {
   if (!iter) {
     iter = acc[Symbol.iterator]();
     acc = iter.next().value;
   } else {
     iter = iter[Symbol.iterator]();
   }
   return go1(acc, function recur(acc) {
     let cur;
     while (!(cur = iter.next()).done) {
       const a = cur.value;
       acc = f(acc, a); /* 문제가 되는 코드 */
       if (acc instanceof Promise) return acc.then(recur);
     }
     return acc;
   });
 });

그리고 이 reduce함수를 활용하여 다음의 코드를 실행시키면 아직까지는 오류가 발생하게 된다. 그래서 이 부분도 수정해보겠다.

go([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)],
  L.map(a => a + 10),
  L.filter(a => a % 2),
  reduce((a, b) => a + b),
  console.log
);

해당 부분도 이전 로직들과 유사하게 먼저 프로미스와 동기코드 로직을 먼저 분리시키고 프로미스 객체라면 then 이후에 실행되는 값을 리턴해주면 된다. 그리고 마찬가지로 에러가 일반적인 에러가 아닌 특별한 에러인 nop인지를 판단하여 다음 로직을 수행할 수 있도록 작성해주면 된다. reduce에서는 acc 값을 던짐으로써 다음 동작을 수행할 수 있다.

const reduceF = (acc, a, f) =>
  a instanceof Promise ?
    a.then(a => f(acc, a), e => e == nop ? acc : Promise.reject(e)) :
    f(acc, a);

const reduce = curry((f, acc, iter) => {
  if (!iter) {
    iter = acc[Symbol.iterator]();
    acc = iter.next().value;
  } else {
    iter = iter[Symbol.iterator]();
  }
  return go1(acc, function recur(acc) {
    let cur;
    while (!(cur = iter.next()).done) {
	   // const a = cur.value; // - 제거
       // acc = f(acc, a); // - 제거
      acc = reduceF(acc, cur.value, f); // + 추가
      if (acc instanceof Promise) return acc.then(recur);
    }
    return acc;
  });
});

이처럼 바꾸면 아래와 같이 정상적으로 동작함을 볼 수 있다.

reduce 내부 로직을 간결하게 정리한다면 아래의 로직을

if (!iter) {
  iter = acc[Symbol.iterator]();
  acc = iter.next().value;
} else {
  iter = iter[Symbol.iterator]();
}

재귀 함수를 활용하여 조금 더 간결하게 정리해보면 아래와 같이 나타낼 수 있다.

const head = iter => go1(take(1, iter), ([h]) => h);

if (!iter) 
  return reduce(f, head(iter = acc[Symbol.iterator]()), iter);
iter = iter[Symbol.iterator]();

지금까지 기존의 유틸함수들이 비동기성을 지원하도록 내부 코드를 수정해보았는데 아직까지는 비동기의 로직이 순차적으로 진행되어 효율적이지 못한 느낌이 든다.

그래서 다음 강의에서는 이 부분을 순차적으로가 아닌 병렬적으로 평가할 수 있도록 만들어보는 예시를 보여준다.


Reference

0개의 댓글