함수형 프로그래밍은 코드를 값으로 다루어 표현력을 높이는 방법을 배울 수 있습니다.
- `go` 함수: 인자에 들어있는 함수들을 차례대로 실행하여 하나의 값으로 만들어나가는 함수
- `pipe` 함수: 여러 함수들을 합성하여 하나의 함수로 리턴해주는 함수
- `curry` 함수: 값으로 다루면서 받아둔 함수를 원하는 시점에 평가시키는 함수
차례대로 공부해봅시다. 앞으로의 모든 코드들에는 `reduce` 함수가 있으니 `reduce` 함수에 대해 이해가 안되신 경우에는 제 이전 글을 보시면 감사하겠습니다.
1. go 함수
코드를 값으로 다룰 수 있기 때문에, 함수가 다른 함수를 인자로 받아 평가하는 시점을 원하는 대로 다룰 수 있습니다. 이를 통해 코드의 표현력을 높일 수 있습니다.
`go` 함수는 인자들을 특정 함수로 축약해서 하나의 값으로 만들어나갑니다.
const add = (a: number, b: number) => a + b;
const reduce = (fn: Function, acc: any, iter?: any) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for (const item of iter) {
acc = fn(acc, item);
}
return acc;
};
function go(...args: unknown[]) {
return reduce((a: number, f: Function) => f(a), args);
}
go(
0,
(a: number) => a + 1,
(a: number) => a + 10,
(a: number) => a + 100,
console.log
);
go(
add(0, 1),
(a: number) => a + 1,
(a: number) => a + 10,
(a: number) => a + 100,
console.log
);
`go` 함수를 봅시다.
`go` 함수는 매개변수로 특정한 값과 함수들을 받습니다. `go`함수를 사용한다면, 매개변수로 중첩된 함수들을 순서대로 나열하여 가독성을 높일 수 있게 됩니다.
첫 번째 매개변수는 특정한 값이어야 합니다. 왜냐하면 `acc`로 사용해야하기 때문이죠.
이후, 뒤에 들어오는 매개변수들을 순서대로 실행하는데, `reduce`의 첫 번째 인자로 함수를 실행하는 함수를 넘겨주었기 때문입니다.
매개변수로 받은`args`는 `reduce` 함수 내에서 `acc`에 0, `iter`에 나머지 함수들의 목록을 넘겨주게 됩니다.
그리고 for ... of 문에서 `iter`에 들어있는 함수들을 순차적으로 실행해 주게 되는 것이죠. (`fn`이 `reduce`의 첫번째 인자이므로)
`go`함수를 사용하면 코드를 좀 더 가독성있게 표현할 수 있게 됩니다. 아래의 예시를 보시면 체감이 되실 겁니다!
type Product = {
name: string;
price: number;
};
const products: Product[] = [
{ name: "반팔티", price: 15000 },
{ name: "긴팔티", price: 20000 },
{ name: "핸드폰케이스", price: 15000 },
{ name: "후드티", price: 30000 },
{ name: "바지", price: 25000 },
];
log(
reduce(
add,
map(p => p.price,
filter(p => p.price < 20000, products))));
go(
products,
products => filter(p => p.price < 20000, products),
products => map(p => p.price, products),
price => reduce(add, prices),
console.log
)
2. pipe 함수
`pipe` 함수는 하나의 합성 함수로 만들어 줍니다.
먼저 코드를 보시죠.
const reduce = (fn: Function, acc: any, iter?: any) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for (const item of iter) {
acc = fn(acc, item);
}
return acc;
};
function go(...args: unknown[]) {
return reduce((a: number, f: Function) => f(a), args);
}
// 함수를 리턴하는 함수여야 함
function pipe(...fns: Function[]) {
return <T>(a: T) => go(a, ...fns);
}
// 3개의 함수를 연속적으로 실행하면서 축약하는 함수를 만듦
const fn = pipe(
(a: number) => a + 1,
(a: number) => a + 10,
(a: number) => a + 100
);
console.log(fn(0)); // 111
`pipe` 함수는 `go` 함수를 이용하여 만드는데, 차이점은 초기값을 나중에 넣을 수 있도록 해준다는 점입니다.
`go`함수는 인자로 `초깃값, 실행함수1, 실행함수2, ...` 이렇게 넘겨서 함수 실행 즉시 값을 반환하지만 `pipe` 함수는 인자로 `실행함수1, 실행함수2, ...` 를 넘겨 하나의 합성함수를 반환한 후에 실행하고 싶은 곳에서 초깃값을 인자로 넘겨서 사용합니다.
코드가 조금 더 깔끔해진 것처럼 보입니다. 하지만 지금의 `pipe` 함수는 `fn(0, 1)`처럼 2개의 인자를 넣어도 제대로 작동되지 않습니다.
그렇다면 `pipe` 함수를 조금 변형을 해보도록 합시다.
const add = (a: number, b: number) => a + b;
const pipe = (f: Function, ...fns: Function[]) => {
return <T>(...a: T[]) => go(f(...a), ...fns);
};
const fn = pipe(
add,
(a: number) => a + 1,
(a: number) => a + 10,
(a: number) => a + 100
);
console.log(fn(0, 1));
`pipe`의 매개변수를 분리함으로써 앞에서 이야기했던 문제는 해결할 수 있었습니다.
3. curry 함수
`curry` 함수는 함수를 값으로 다루면서 받아둔 함수를 원하는 시점에 평가시키는 함수입니다.
더 자세히 말하자면 함수를 받아서 함수를 리턴하는데, 인자를 받아서 원하는 개수의 인자가 들어온 경우, 실행시키는 함수입니다.
이 때, 인자는 계속 내부적으로 저장해둡니다.
먼저 `curry` 함수를 봅시다.
const curry =
(fn: Function) =>
(a: any, ..._: any[]) => {
return _.length ? fn(a, ..._) : (..._: any[]) => fn(a, ..._);
};
함수를 매개변수로 받아서 특정한 함수로 리턴해줍니다.
반환된 함수에 넣는 인자에 따라 반환된 값이 다릅니다.
- 인자를 하나만 넣은 경우는 인자를 첫번째 인자에 넣어둔 함수를 다시 반환 (클로저)
- 2개 이상을 넣은 경우는 함수를 실행해줍니다.
const mult = curry((a: number, b: number, c: number) => a * b);
console.log(mult(3)(2)); // 6
console.log(mult(3, 2)); // 6
go, currry를 같이 사용하여 코드를 좀 더 깔끔하게 작성할 수 있습니다.
먼저 `filter`, `map`, `reduce` 함수를 `curry` 함수로 감쌉시다.
const curry =
(fn: Function) =>
(a: any, ..._: any[]) => {
return _.length ? fn(a, ..._) : (..._: any[]) => fn(a, ..._);
};
const reduce = curry((fn: Function, acc: any, iter?: any) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for (const item of iter) {
acc = fn(acc, item);
}
return acc;
});
const filter = curry((f: Function, iter: Iterable<any>) => {
const res: Product[] = [];
for (const a of iter) {
if (f(a)) res.push(a);
}
return res;
});
const map = curry((f: Function, iter: Iterable<any>) => {
const res: any[] = [];
for (const item of iter) {
res.push(f(item));
}
return res;
});
`curry` 함수를 감싸게 되면 코드를 다음과 같이 실행할 수 있게 됩니다.
// curry 함수 적용 이전
go(
products,
(products: Product[]) => filter((p: Product) => p.price < 20000, products)
(products: Product[]) => map((p: Product) => p.price, products)
(price: number) => reduce(add, prices),
console.log
)
// curry 함수 적용 이후
go(
products,
(products: Product[]) => filter((p: Product) => p.price < 20000)(products),
(products: Product[]) => map((p: Product) => p.price)(products),
(price: number) => reduce(add)(price),
console.log
);
그냥 함수 호출을 2번으로 분리해준 것 이외에 차이점이 없어보일 수 있습니다. 하지만 다음과 같이 단축 호출을 하게 된다면 가독성과 표현력 면에서 많은 차이를 낼 수 있습니다.
go(
products,
filter((p: Product) => p.price < 20000),
map((p: Product) => p.price),
reduce(add),
console.log
);