[FP] - curry, pipe, go

2024. 7. 4.

함수형 프로그래밍은 함수들을 마치 블럭처럼 조립해 작성하는 모양을 띄곤 합니다. 다만 기본적인 JS에서 함수형 프로그래밍은 가독성이 좋지 않습니다.

console.log(map(filter([1,2,3,...,10], isEven),double))

함수를 인자로 넘겨주어야 하기 때문에 함수가 계속 중첩되는 모양이 되고 실행은 중첩의 중심부터 실행되기 때문에 가독성에 문제가 생깁니다.

오늘 다룰 curry, pipe, go 함수를 이용하면 가독성 좋은 함수형 프로그래밍 코드를 만들 수 있습니다.

// 적용 이후 코드
go(
	[1,2,3,....,10],
	filter(isEven),
	map(double),
	console.log,
)

어떤가요? 위에서 아래로 순차적으로 매우 쉽게 읽을 수 있지 않나요?

🧑‍💻

파이프 연산자 제안

현재 파이프 연산자는, TC39 프로세스 단계중 stage 2에 있습니다.

파이프 연산자란 연속된 함수의 조합을 가독성 있게 바꿔주는 연산자입니다.

	const result = (arr)=>{
		return arr
			|> filter(isEven)
			|> map(console.log)
	}

curry, pipe, go는 연속해서 실행하는 함수 조합을 쉽게 만들어 줍니다. 하나씩 알아보도록 하겠습니다.

⚠️

주의

앞으로 map, filter, reduce와 같은 함수들을 사용할 예정입니다. 이 글에서는 앞선 함수들의 구현체는 언급하지 않을 예정입니다.

이 함수들은 함수를 첫 번째 인자로 받고, 배열을 두 번째 인자로 받는 형태로 사용할 것입니다.

예를 들어 arr.map(i=> i*2)map(i=>i*2 , arr)로 사용합니다.

curry

커링은 여러 개의 인자를 가진 함수를 하나의 인자만 받고 나머지 인자를 받는 새로운 함수를 반환하는 기법입니다. 예시를 들어보겠습니다.

function add(a: number, b: number) {
  return a + b;
}
 
function curriedAdd(a: number) {
  return (b: number) => a + b;
}
 
add(1, 2); // 3
 
const addOne = curriedAdd(1);
addOne(2); // 3

예시에서 add()는 두 개의 인자를 받아 더한 값을 반환합니다. 반면, curriedAdd()는 첫 번째 인자 a를 받고, 그 후에 클로저를 통해 두 번째 인자 b를 받아 a + b 를 계산하는 새로운 함수를 반환합니다. 이렇게 하면 curriedAdd()를 호출한 뒤 반환된 함수에 다음 피연산자를 전달하여 결과를 얻을 수 있습니다.

curry()는 여러 개의 인자를 가진 함수를 단일인자를 받아 나머지 인자를 받는 새로운 함수를 반환하는 curring 함수로 변경하는 헬퍼함수입니다.

function curry(fn: (...args: any[]) => any) {
  return function curried(...args: any) {
    return args.length >= fn.length ? fn(...args) : (...args2: any) => curried(...args, ...args2);
  };
}
⚠️

Typescript에서의 curring, pipe, go

우리는 각각의 헬퍼함수를 사용하면서 타입 추론을 기대합니다. 하지만 타입스크립트의 특성상 각각의 헬퍼함수를 구현하면서 타입 추론을 위해서는 오버로딩을 통해 타입을 구현해야합니다. 이 글에서는 해당 오버로딩에 대한 코드를 생략합니다.

선언한 curry함수를 사용하면 쉽게 curring을 구현할 수 있습니다.

function add(a: number, b: number) {
  return a + b;
}
 
const curriedAdd = curry(add);
 
add(1, 2); // 3
 
const addOne = curriedAdd(1);
addOne(2); // 3

go

go 함수는 함수를 인자로 받아서 순차적으로 즉시 실행하게 도와주는 헬퍼 함수입니다.

export function go(f1: () => any, ...functions: any[]): any {
  return functions.reduce((value, func) => func(value), f1());
}

go()의 인자는 함수들을 받습니다. go()에 전달된 함수들은 이전 함수의 반환값이 다음 함수의 인자가 됩니다.

go(
  () => 1,
  (i) => i + 10,
  console.log
); // 11
 
// 보통 go를 구현할때 첫번째 인자는 value를 받는 경우가 많습니다.
 
go(1, (i) => i + 10, console.log); // 11

첫번째 ()=> 1의 반환값인 1이 다음 함수인 i=> i+ 10의 인자로 넘겨집니다. 타입스크립트를 사용하면 오버로딩을 통해 타입추론이 가능합니다.

이때 curry()와 함께 사용하면 매우 간결하고 가독성 좋은 프로그래밍이 가능해집니다.

const map = curry((fn, arr) => arr.map(fn));
 
go(
  () => [1, 2, 3, 4],
  map((n) => n + 1),
  console.log
); // [2,3,4,5]
🧑‍💻

JS 콜백함수의 축약형태

콜백 함수의 시그니처가 사용처인 함수에서 요구하는 형식과 일치할때, 호출문을 생략할 수 있는 JS의 특성입니다.

예를 들어, arr.forEach(console.log)arr 배열의 각 요소를 console.log 함수에 전달하여 출력하는 동작을 수행합니다. 이 경우에는 console.log 함수가 forEach 함수에서 요구하는 콜백 함수의 형식과 일치하기 때문에 별도로 함수 선언이나 화살표 함수를 사용하지 않고도 간결하게 사용할 수 있는 것입니다.

pipe

pipe() 는 여러 함수를 차례대로 합쳐서 하나의 함수를 반환합니다. go()와 다르게 즉시 실행하지 않습니다.

export function pipe(f1: () => any, ...functions: any[]): any {
  return (): any => {
    return functions.reduce((value, func) => func(value), f1());
  };
}
 
// go 함수를 사용해 나타내기도 합니다.
 
const pipe =
  (f, ...fs) =>
  (...as) =>
    go(f(...as), ...fs);

지금까지 알아본 헬퍼함수를 이용하면 보다 가독성 좋은 함수형 프로그래밍을 만들 수 있습니다.

참고자료

https://kagrin97-blog.vercel.app/js/FP-(curry,go,pipe)