‘지연적인 함수’에 Proimse 사용할 수 있도록 코드 추가

목표

  • 지연적인 함수에서 Promise 상황에도 사용할 수 있도록 코드 수정
  • 상황에 따라 지연적이지 않을 때를 대비하는 함수 작성

L.map, map, take 함수를 Promise에서도 사용하도록

  • 밑에 코드에서는 Promise 값을 받게되면 정상적으로 동작하지 않음
go(
  [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)],
  L.map((a) => Promise.resolve(a + 10)),
  take(2),
  log,
);

L.map 함수에서 Promise 값 사용할 수 있도록 변경

const go1 = (a, f) => (a instanceof Promise ? a.then(f) : f(a));

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

Take 함수에서 Promise 값을 꺼낼 수 있도록 변경

const take = curry((l, iter) => {
  const 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()))
          .catch((e) => (e == nop ? recur() : Promise.reject(e)));
      }

      res.push(a);

      if (res.length == l) return res;
    }
    return res;
  })();
});

다양한 경우에도 동작 함

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

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

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

go(
  [1, 2, 3],
  map((a) => Promise.resolve(a + 10)),
  log,
);

go(
  [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)],
  map((a) => a + 10),
  log,
);

go(
  [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)],
  map((a) => Promise.resolve(a + 10)),
  log,
);

L.filter, Take에 Kleisli Composition 적용

  • Filter함수에서 지연성과 Promise를 함께 지원할려면, Kleisli Composition을 활용해야 함
  • 밑에 코드 처럼 Filter함수에 들어오는 값이 Promise이기 때문에 아직은 동작하지 않음
go(
  [1, 2, 3, 4, 5, 6],
  L.map((a) => Promise.resolve(a * a)),
  L.filter((a) => a % 2),
  take(2),
  log,
);

L.filter 지연성과 Promise를 만족하기 위한 수정

const go1 = (a, f) => (a instanceof Promise ? a.then(f) : f(a));

const nop = Symbol('nop');

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

    if (b instanceof Promise) {
      // Promise.reject을 통해 다음 함수들이 작동되는 것을 막음(Kleisli Composition)
      yield b.then((b) => (b ? a : Promise.reject(nop)));
    } else if (b) {
      yield a;
    }
  }
});

Take 함수에서 Promise를 만족하기 위한 수정

const take = curry((l, iter) => {
  const 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()))
          .catch((e) => (e == nop ? recur() : Promise.reject(e)));
      }

      res.push(a);

      if (res.length == l) return res;
    }
    return res;
  })();
});

사용

  • Promise에서 값이 reject을 반환했을 때 처리 됨
  • 또한 Promise를 만나도 지연적으로 코드 진행
go(
  [1, 2, 3, 4, 5, 6],
  L.map((a) => Promise.resolve(a * a)),
  L.map((a) => a * a),
  filter((a) => Promise.resolve(a % 2)),
  L.map((a) => a * a),
  L.map((a) => {
    log(a);
    return a * a;
  }),
  L.map((a) => {
    log(a);
    return a * a;
  }),
  take(4),
  log,
);

reduce에서 Promise 부분 추가

  • 밑에 코드 동작되지 않음
  • reduce에 Promise 부분을 처리하는 코드가 아직 없음
go(
  [1, 2, 3, 4],
  L.map((a) => Promise.resolve(a * a)),
  L.filter((a) => Promise.resolve(a % 2)),
  reduce(add),
  log,
);
const nop = Symbol('nop');
const head = (iter) => go1(take(1, iter), ([h]) => h);

// 만약에 밑에 코드가 재사용 가능성이 높은 코드일 경우에는
// 다양한 예외처리를 생각해서 작성해야 함
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) {
    return reduce(f, head((iter = acc[Symbol.iterator]())), iter);
  }

  iter = iter[Symbol.iterator]();

  return go1(acc, function recur(acc) {
    let cur;

    while (!(cur = iter.next()).done) {
      acc = reduceF(acc, cur.value, f);

      if (acc instanceof Promise) {
        return acc.then(recur);
      }
    }
    return acc;
  });
});

지연 평가와 Promise사용 예제

  • 함께 사용함으로써 효율성 증가
go([1, 2, 3, 4, 5],
  L.map(a => Promise.resolve(a * a)),
  L.filter(a => Promise.resolve(a % 2),
  log);

go([1, 2, 3, 4, 5, 6, 7, 8],
  L.map(a => {
    log(a);
    return new Promise(resolve => setTimeout(() => resolve(a * a), 1000))
  }),
  L.filter(a => {
    log(a);
    return new Promise(resolve => setTimeout(() => resolve(a % 2), 1000))
  }),
  take(2),
  log);


느낀점

이번 학습을 통해 기존에 작성한 지연적인 함수에 Promise 상황에서도 사용 할 수 있도록 코드를 수정해보았다. 이 과정속에서 Promise와 지연적인 함수에 대해 이해를 더 하는 시간을 갖게 되었다.


참고

유인동님의 함수형 프로그래밍과 JS ES6+ 강의