본문 바로가기
개발/typescript, web

ts) N차 함수 N번째 항에서 함수합성하기

by 꾸루루 2022. 5. 25.

두 개의 함수가 있다고 해보자.

f: A -> B -> C

g: C -> D

이 두 함수를 합성한다면

f∘g: A -> B -> D 

처럼 된다.

 

문제는 이것을 typescript에서 어떻게 합성하냐인데.. 흔히 함수 합성에 자주 사용되는 pipe, flow를 사용하면 다음과 같이 해야한다.

(pipe나 flow는 특정 함수의 이름이 아닌, 프로그래밍 언어 공통적으로 통용되는 기능의 용어이다. 그러므로 lodash, ramda, fp-ts 등의 많은 유틸리티 라이브러리에서 제공된다.)

 

const f = (a: number) => (b: string) => new Array<string>(a).fill(b)
const g = (arr: string[]) => arr.map(str => ({data: str}))

// const fg = flow(f, g) // Error!
const fg = (...args: Parameters<typeof f>) => flow(f(...args), g) // OK
fg(3)('a') // [{data: 'a'}, {data: 'a'}, {data: 'a'}]

 

(...args: Parameters<typeof f>) => 처럼 보기만 해도 지저분한 내용이 들어가는 이유는

flow는 기본적으로 1차 함수의 결과를 합성한다고 보면 될 것이다.

그런데 우리는 2차 함수의 결과를 가지고 합성할 것이기 때문에, 인자를 받아 f 함수의 한 차수를 더 내리는 처리를 한 것이다.

 

차수를 내리지 않는다면...

flow(A -> B -> C, (B -> C) -> ...) 이런 식으로 flow에 2번째 인자로 들어가는 함수의 인자로 B -> C라는 함수가 들어오는 것이다.

이는 우리가 원한 C가 들어오는게 아니기 때문에 한 차수를 더 내릴 필요가 있는 것이다.

 

g는 f의 2차 함수의 결과를 받는다고 가정(약속)하고 함수를 합성하고, 받는다고 가정한 2차에 대한 인자를 넣고 나면 합성이 되는 것이다.

 

그래서 이를 간단히 하기 위해 flow2라는 함수를 하나 만들었다.

 

flow2.d.ts

export function flow2<A extends ReadonlyArray<unknown>, B extends ReadonlyArray<unknown>, C>(
  abc: (...a: A) => (...b: B) => C
): (...a: A) => (...b: B) => C

export function flow2<A extends ReadonlyArray<unknown>, B extends ReadonlyArray<unknown>, C, D>(
  abc: (...a: A) => (...b: B) => C,
  cd: (c: C) => D
): (...a: A) => (...b: B) => D

export function flow2<A extends ReadonlyArray<unknown>, B extends ReadonlyArray<unknown>, C, D, E>(
  abc: (...a: A) => (...b: B) => C,
  cd: (c: C) => D,
  de: (d: D) => E
): (...a: A) => (...b: B) => E

export function flow2<A extends ReadonlyArray<unknown>, B extends ReadonlyArray<unknown>, C, D, E, F>(
  abc: (...a: A) => (...b: B) => C,
  cd: (c: C) => D,
  de: (d: D) => E,
  ef: (e: E) => F
): (...a: A) => (...b: B) => F

export function flow2<A extends ReadonlyArray<unknown>, B extends ReadonlyArray<unknown>, C, D, E, F, G>(
  abc: (...a: A) => (...b: B) => C,
  cd: (c: C) => D,
  de: (d: D) => E,
  ef: (e: E) => F,
  fg: (f: F) => G
): (...a: A) => (...b: B) => G

export function flow2<A extends ReadonlyArray<unknown>, B extends ReadonlyArray<unknown>, C, D, E, F, G, H>(
  abc: (...a: A) => (...b: B) => C,
  cd: (c: C) => D,
  de: (d: D) => E,
  ef: (e: E) => F,
  fg: (f: F) => G,
  gh: (g: G) => H,
): (...a: A) => (...b: B) => H

export function flow2<A extends ReadonlyArray<unknown>, B extends ReadonlyArray<unknown>, C, D, E, F, G, H, I>(
  abc: (...a: A) => (...b: B) => C,
  cd: (c: C) => D,
  de: (d: D) => E,
  ef: (e: E) => F,
  fg: (f: F) => G,
  gh: (g: G) => H,
  hi: (h: H) => I,
): (...a: A) => (...b: B) => I

export function flow2<A extends ReadonlyArray<unknown>, B extends ReadonlyArray<unknown>, C, D, E, F, G, H, I, J>(
  abc: (...a: A) => (...b: B) => C,
  cd: (c: C) => D,
  de: (d: D) => E,
  ef: (e: E) => F,
  fg: (f: F) => G,
  gh: (g: G) => H,
  hi: (h: H) => I,
  ij: (i: I) => J
): (...a: A) => (...b: B) => J

export function flow2<A extends ReadonlyArray<unknown>, B extends ReadonlyArray<unknown>, C, D, E, F, G, H, I, J, K>(
  abc: (...a: A) => (...b: B) => C,
  cd: (c: C) => D,
  de: (d: D) => E,
  ef: (e: E) => F,
  fg: (f: F) => G,
  gh: (g: G) => H,
  hi: (h: H) => I,
  ij: (i: I) => J,
  jk: (j: J) => K
): (...a: A) => (...b: B) => K

 

flow2.js

function flow2(abc, cd, de, ef, fg, gh, hi, ij, jk) {
  switch (arguments.length) {
    case 1:
      return abc;
    case 2:
      return function ab() {
        const argumentsAB = arguments
        return function bc() {
          return cd(abc.apply(this, argumentsAB).apply(this, arguments));
        }
      };
    case 3:
      return function ab() {
        const argumentsAB = arguments
        return function bc() {
          return de(cd(abc.apply(this, argumentsAB).apply(this, arguments)))
        }
      };
    case 4:
      return function ab() {
        const argumentsAB = arguments
        return function bc() {
          return ef(de(cd(abc.apply(this, argumentsAB).apply(this, arguments))))
        }
      };
    case 5:
      return function ab() {
        const argumentsAB = arguments
        return function bc() {
          return fg(ef(de(cd(abc.apply(this, argumentsAB).apply(this, arguments)))))
        }
      };
    case 6:
      return function ab() {
        const argumentsAB = arguments
        return function bc() {
          return gh(fg(ef(de(cd(abc.apply(this, argumentsAB).apply(this, arguments))))))
        }
      };
    case 7:
      return function ab() {
        const argumentsAB = arguments
        return function bc() {
          return hi(gh(fg(ef(de(cd(abc.apply(this, argumentsAB).apply(this, arguments)))))))
        }
      };
    case 8:
      return function ab() {
        const argumentsAB = arguments
        return function bc() {
          return ij(hi(gh(fg(ef(de(cd(abc.apply(this, argumentsAB).apply(this, arguments))))))))
        }
      };
    case 9:
      return function ab() {
        const argumentsAB = arguments
        return function bc() {
          return jk(ij(hi(gh(fg(ef(de(cd(abc.apply(this, argumentsAB).apply(this, arguments)))))))))
        }
      };
  }
  return;
}

module.exports.flow2 = flow2

 

구현 방식은 fp-ts의 flow를 참고하였다. 

 

저렇게 만든 flow2를 사용한다면... 짠!

 

const f = (a: number) => (b: string) => new Array<string>(a).fill(b)
const g = (arr: string[]) => arr.map(str => ({data: str}))

const fg = flow2(f, g)
fg2(3)('b') // [{data: 'b'}, {data: 'b'}, {data: 'b'}]

 

다음과 같이 아주 깔끔하게 함수 합성을 할 수 있다.

 

3차 함수의 결과를 가지고 합성을 하려면 flow3도 만들어야겠지만.... flow3까지는 쓸 일이 좀 있을 것 같은데 flow4는 사용할 일이 거의 없을 것 같다.