Kleisli Composition

nn·2023년 6월 27일
1
  L.filter = curry(function* (fn, iter) {
    for (const a of iter) {
      if (fn(a)) yield a;
    }
  })

  go([1, 2, 3, 4, 5, 6],
    L.map(a => Promise.resolve(a * a)),
    L.filter(a => {
      log(a)
      a % 2
    }),
    L.map(a => (a * a)),
    take(2),
    log
  )

위 코드를 실행하면 빈 배열이 출력된다.
이유는 아래와 같이 L.map에서 프로미스를 L.filter에 보내지만 프로미스를 L.filter에서 처리하지 못하기 때문이다.

지연평가와 프로미스를 제어하기위해 Kleisli Composition 이 필요하다.

우선 L.filter에서 프로미스를 제어해보도록하자.

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

go1을 통해서 fn와 a를 실행해서 boolean값을 가지는 프로미스 혹은 값을 리턴하게 했다.

v가 프로미스인경우 v의 값을 풀어서 참인 경우에만 a를 리턴하게 하고, 거짓인 경우 에러를 리턴하게 해두었다.

go를 통해 함수를 순차적으로 실행할때, reject이된 비동기 처리는 다음 함수로 전파되지 않게 하는 것이 Kleisli Composition 이다.

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

filter 에서 에러가 난 경우 그 값은 L.map에 들어가지 않게 해야한다.

이 상태에서 콘솔을 찍으면 다음과 같이 나온다.

fliter 에서 짝수를 만났기 때문에 에러가나고 멈춘것이다.

이 에러를 처리하기 위해 take를 수정해야한다.
reject이 된 것은 take에서 제외하도록 수정해보자.

  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);
            if (res.length === l) return res;
            return recur();
          }).catch((e) => {
            if (e === nop) return recur();
            return Promise.reject(e);
          })
        }
        res.push(a);
        if (res.length === l) return res;
      }
      return res;
    })();
  })

catch문에서 필터에서 거짓으로 평가된 nop인 경우에는 다음 요소을 평가하도록 recur()를 리턴하고, 그 외의 에러라면 reject를 하도록 했다.

즉 이렇게 함으로서

 go([1, 2, 3, 4, 5, 6],
    L.map(a => Promise.resolve(a * a)),
    L.filter(a =>  a % 2),
    L.map(a => (a * a)),
    take(3),
    log
  )

요소 1인 경우 필터에서 참으로 평가하고, 1의 제곱을 하여 take에 담긴다.
요소 2는 필터에서 거짓으로 평가되었기 때문에 reject nop이 발생했고, 그러므로 then이아닌 catch로 바로 가게 된다.
즉 4 제곱을 하지 않고, 바로 take의 catch문에서 평가되고 recur()를 통해 다음 요소인 3을 평가하게 된다.

이렇게 Kleisli Composition 을 통해 거짓인 경우엔 다음 함수까지 전파하지 않고 바로 넘겨버릴 수 있도록 할 수 있다.


reduce에서 nop

  const add = (a, b) => a + b;

  go([1, 2, 3, 4, 5],
    L.map(a => Promise.resolve(a * a)),
    L.filter(a => Promise.resolve(a % 2)),
    reduce(add),
    log);

프로미스를 연산하려는 경우 아래와 같이 에러가 발생한다.
reduce에서 reject nop인 경우도 연산을 하려고 하기 때문이다.

reduce에서 nop인 경우 다음 요소를 평가하도록 넘기도록 수정해보자.

  const reduce = curry((fn, acc, iter) => {
    if (!iter) {
      return reduce(fn, head((iter = acc[Symbol.iterator]())), iter)
    }
    iter = iter[Symbol.iterator]();
    return go1(acc, function recur(acc) {
      let cur;
      while (!(cur = iter.next()).done) {
        const a = cur.value;
        acc = fn(acc, a);
        if (acc instanceof Promise) {
          return acc.then(recur);
        }
      }
      return acc;
    })
  });

지금 reduce 함수는 go함수에서 L.map과 L.filter를 통해 얻게 된 프로미스가 a에 들어가서 동작하면서 에러가 일어나고 있다.

즉, a를 프로미스인지 확인하고, fn에 acc와 a를 적용한 값이 무조건 프로미스가 풀린 상태가 되도록 해야한다.

const reduceF = (acc, a, fn) => {
    if (a instanceof Promise) {
      return a.then(a => fn(acc, a)).catch(e => {
        if (e === nop) {
          return acc;
        } else {
          Promise.reject(e)
        }
      })
    } else {
      return fn(acc, a)
    }
  }
  
  const reduce = curry((fn, acc, iter) => {
    if (!iter) {
      return reduce(fn, head((iter = acc[Symbol.iterator]())), iter)
    }
    iter = iter[Symbol.iterator]();
    return go1(acc, function recur(acc) {
      let cur;
      while (!(cur = iter.next()).done) {
        const a = cur.value;
        acc = reduceF(acc, a, fn);
        if (acc instanceof Promise) {
          return acc.then(recur);
        }
      }
      return acc;
    })
  });

reduceF를 통해서 a가 promise인지 확인하여 acc는 항상 값이 리턴되도록 했다.

그리고 nop인 경우 acc를 그대로 유지하여 다음 요소와 함께 평가 되도록 했다.

profile
내가 될 거라고 했잖아

0개의 댓글