두 개의 함수가 있다고 해보자.
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는 사용할 일이 거의 없을 것 같다.
'개발 > typescript, web' 카테고리의 다른 글
Module Augmentation와 Interface Merge를 활용한 bottom - up 식 구현 (1) | 2022.07.24 |
---|---|
remix ssr에서 esmodule 패키지 사용하기 (ex: swiper) (0) | 2022.06.16 |
typescript generic의 사용 예와 identity function을 사용한 제네릭 인자 추론 (0) | 2022.05.15 |
fp-ts를 통해 함수형 프로그래밍 용어 정리 (0) | 2022.05.08 |
식별할 수 있는 문자열(문자열 리터럴) 확인하는 방법 (0) | 2022.04.22 |