2024. 3. 19. 19:02ㆍ함수형 프로그래밍
함수형 프로그래밍은 코드를 값으로 다룰 수 있다고 했다.
이 말이 무슨 말일까?
go(
0,
(a) => a + 1,
(a) => a + 10,
(a) => a + 100,
console.log
);
go 함수를 실행하면 0 + 1 + 10 + 100 의 연산을 거쳐 111을 리턴하게끔 구현하고 싶다.
정의한 reduce 함수를 활용해서 파라미터로 전달한 함수 코드를 값처럼 다뤄보자.
const reduce = (f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for (const a of iter) {
acc = f(acc, a);
}
return acc;
};
const go = (...args) => reduce((a, f) => f(a), args);
go(
0,
(a) => a + 1,
(a) => a + 10,
(a) => a + 100,
console.log
);
먼저 go 함수는 rest parameter를 이용해 인자로 들어오는 요소를 args 배열에 담았다.
즉, 이 코드와 동일한 셈이다.
const go = (...args) => reduce((a, f) => f(a), args);
const go = () => reduce((a, f) => f(a), [0, (a) => a + 1, (a) => a + 10, (a) => a + 100, console.log]);
그리고 전달한 함수들을 하나씩 순회하면서 보조함수의 파라미터로 acc와 함께 들어가고
전달된 함수의 갯수만큼 보조함수를 반복적으로 실행하게된다.
go 함수는 함수들을 전달해서 즉시 값으로 평가한다. 즉, 리턴값이 함수가 아닌 하나의 값이다.
이제는 함수를 리스트로 받아서 하나의 함수를 리턴해주는 파이프 함수를 구현해보겠다.
const reduce = (f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for (const a of iter) {
acc = f(acc, a);
}
return acc;
};
const go = (...args) => reduce((a, f) => f(a), args);
const pipe = (...fs) => {
return (a) => go(a, ...fs);
};
go(
0,
(a) => a + 1,
(a) => a + 10,
(a) => a + 100,
console.log
);
const f = pipe(
(a) => a + 1,
(a) => a + 10,
(a) => a + 100
);
console.log(f(0));
파이프 함수는 함수를 값으로 전달해서 하나의 함수를 리턴한다.
이 코드에서는 파이프 함수가 리턴하는 하나의 함수에 인자값을 전달하면 내부적으로 go 함수가 실행되도록 구현했다.
const reduce = (f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for (const a of iter) {
acc = f(acc, a);
}
return acc;
};
const add = (a, b) => a + b;
const go = (...args) => reduce((a, f) => f(a), args);
const pipe = (f, ...fs) => {
return (...as) => go(f(...as), ...fs);
};
go(
add(0, 1),
(a) => a + 10,
(a) => a + 100,
console.log
);
const f = pipe(
(a, b) => a + b,
(a) => a + 10,
(a) => a + 100
);
console.log(f(0, 1));
지금은 go함수와 pipe함수의 실용성을 전혀 느끼지 못하겠다.
그렇다면 기존의 코드를 go함수로 리팩토링하면서 얻을 수 있는 장점들을 찾아보자.
const products = [
{ name: '반팔티', price: 15000 },
{ name: '긴팔티', price: 20000 },
{ name: '핸드폰케이스', price: 15000 },
{ name: '후드티', price: 30000 },
{ name: '바지', price: 25000 },
];
const add = (a, b) => a + b;
const map = (f, iter) => {
const ret = [];
for (const a of iter) ret.push(f(a));
return ret;
};
const filter = (f, iter) => {
const ret = [];
for (const a of iter) {
if (f(a)) ret.push(a);
}
return ret;
};
const reduce = (f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for (const a of iter) {
acc = f(acc, a);
}
return acc;
};
console.log(
reduce(
add,
map(
(p) => p.price,
filter((p) => p.price < 20000, products)
)
)
);
기존에 2만원 미만의 상품 가격의 누적합을 구하기 위해선 다음과 같이 작성했다.
const go = (...args) => reduce((a, f) => f(a), args);
go(
products,
(products) => filter((p) => p.price < 20000, products),
(products) => map((p) => p.price, products),
(prices) => reduce(add, prices),
console.log
);
위의 코드에 go함수를 정의해서 리팩토링한 코드이다.
코드의 가독성이 한결 수월해졌다.
상품을 받고, 2만원 미만을 필터링하고, 가격만 맵핑해서, add함수를 이용해 누적하고 출력한다.
이렇게 전달되는 함수 리스트만 가지고 코드의 의도를 여실히 보여준다.
이번에는 go + curry 를 활용해서 더 읽기 좋은 코드를 만들고자 한다.
curry 함수의 형태를 살펴보자.
const curry = (f) => (a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._);
curry 함수는 함수를 받고 함수를 리턴하는데, 만약 리턴한 함수의 인자 갯수가 2개 미만이면 새로운 함수를 다시 리턴하고 두 개 이상이면 전달받은 함수를 호출한다.
const curry =
(f) =>
(a, ..._) =>
_.length ? f(a, ..._) : (..._) => f(a, ..._);
const mult = curry((a, b) => a * b);
console.log(mult(1));
이 코드는 함수를 출력한다. curry가 반환한 함수인 mult에 인자가 1뿐이므로 새로운 함수가 리턴된 것이다.
const curry =
(f) =>
(a, ..._) =>
_.length ? f(a, ..._) : (..._) => f(a, ..._);
const mult = curry((a, b) => a * b);
console.log(mult(1)(3));
리턴한 새로운 함수에 3을 넣었더니 전달한 (a, b) => a * b 함수가 호출되어 3을 출력한다.
const curry =
(f) =>
(a, ..._) =>
_.length ? f(a, ..._) : (..._) => f(a, ..._);
const mult = curry((a, b) => a * b);
const mult3 = mult(3);
console.log(mult3(10));
console.log(mult3(5));
console.log(mult3(3));
이렇게 활용할 수 있다.
mult 함수에 3을 전달했으니 mult 함수가 리턴한 mult3에 인자를 전달하면 인자*3의 값이 출력된다.
const curry = f =>
(a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._);
const map = curry((f, iter) => {
let res = [];
for (const a of iter) {
res.push(f(a));
}
return res;
});
const filter = curry((f, iter) => {
let res = [];
for (const a of iter) {
if (f(a)) res.push(a);
}
return res;
});
const reduce = curry((f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for (const a of iter) {
acc = f(acc, a);
}
return acc;
});
지금까지 만든 함수들을 curry 함수로 감쌌다.
curry 함수는 함수를 리턴하는데 리턴한 함수의 인자 갯수를 확인해서 2개 미만이면 새로운 함수를 반환한다.
// curry를 활용
go(
products,
(products) => filter((p) => p.price < 20000)(products),
(products) => map((p) => p.price)(products),
(prices) => reduce(add)(prices),
console.log
);
// 기존의 go 함수
go(
products,
(products) => filter((p) => p.price < 20000, products),
(products) => map((p) => p.price, products),
(prices) => reduce(add, prices),
console.log
);
따라서 go 함수에 전달하는 함수를 기존과 다르게 표현할 수 있다.
filter 함수를 예시로 들면 curry로 감싼 filter 함수는 인자로 보조함수만 받고 있다.
인자의 갯수가 2개 미만이기 때문에 filter 함수는 새로운 함수를 리턴할 것이고,
그 함수에 products를 전달하면 curry 함수의 인자로 전달한 함수에 보조함수와 products가 들어가 실행된다.
go(
products,
filter((p) => p.price < 20000),
map((p) => p.price),
reduce(add),
console.log
);
그리고 최종적으로 다음과 같이 표현할 수 있다.
[ '1', '2', '3' ] 배열의 모든 요소를 숫자로 매핑하려면 어떻게 하는가.
["1", "2", "3"].map((c) => Number(c));
그런데 콜백함수의 인자와 Number로 전달하는 인자가 같으면 생략이 가능하다.
["1", "2", "3"].map(Number);
products를 filter가 반환한 함수의 인자로 전달하기에 같은 원리이다.
이해가 어렵다면 filter가 반환한 함수를 Number라고 생각하면 된다.
'함수형 프로그래밍' 카테고리의 다른 글
map, filter, reduce (0) | 2024.03.11 |
---|---|
제너레이터 (0) | 2024.03.10 |
이터러블/이터레이터 프로토콜 (0) | 2024.03.10 |
일급 함수와 고차 함수 (0) | 2024.03.10 |