이 포스트는 유인동님의 https://www.inflearn.com/course/functional-es6/dashboard 강의를 참조하여 작성되었습니다.
함수형프로그래밍은 코드를 값으로 다루는 경우가 많다.
그래서 어떤 함수가 코드인 함수를 받아서 평가하는 시점을 원하는대로 다룰 수 있다.이는 코드의 표현력(가독성)을 높이는 등 다양한 시도를 해볼 수 있게한다.
let log = console.log;
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;
}
이전에 만들었던 reduce함수는 위처럼 어떤 이터러블 받아 그 이터러블을 순회하면서 아규먼트로 받은 함수의 리턴 값을 계속 축적해 나가는 함수다. 이를 이용해 go라는 함수를 만들 수 있다.
const go = (...args) => reduce((a, f) => f(a), args);
go(
0, // 초기 값 0이 다음 함수의 인자가 된다.
a => a + 1, // 함수의 리턴 값이 다음함수의 인자가 된다.
a => a + 10, // 함수의 리턴 값이 다음함수의 인자가 된다.
a => a + 100, // 함수의 리턴 값이 다음함수의 인자가 된다.
log // 최종 값을 log로 보여준다.
// 111
); // 결국 이것은 하나의 값으로 만들어가는 reduce동작이다.
코드를 보면 go라는 함수는 여러가지 아규먼트들을 받아 reduce를 실행하게 되는데, 이는 곧 아규먼트들로 입력한 값 혹은 함수들이 입력한 순서대로 실행되면서 최종적으로 log함수에 값이 넘어가는 것을 알 수 있다.
이렇게 go함수는 어떠한 값을 즉시 평가할 때 사용한다.
go와 비슷하면서 함수를 리턴하는 함수를 즉, 합성된 함수를 만드는 것을 pipe라고 한다.
const pipe = (f, ...fs) => (...as) => go(f(...as), ...fs);
// 함수들을 인자로 받아 go를 리턴하는 형태
// 첫번째 인자를 두 개 이상의 인자를 받아 실행하는 함수로 넣어준다.
const f = pipe( // go로 합성된 함수인 pipe를 f에 담는다.
(a, b) => a + b,
a => a + 10,
a => a + 100,
// 입력받은 함수를 합성한다.
)
log(f(0, 1)); // pipe를 할당한 f에 인자를 주면 비로소 값이 리턴된다.
이처럼 pipe는 함수를 반환하므로 f에 할당한 후 f에 인자를 넣어 호출 할 때, 값이 리턴된다.
const products = [
{ 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))));
// 1. products의 price를 20000미만으로 필터한다.
// 2. 1을 통해 걸러진 값을 map을 통해 price만 뽑아낸다.
// 3. 뽑아낸 price를 add로 출력을 한다.
// go를 이용한 가독성 높은 코드 작성
go(
products,
// products array를 받는다.
products => filter(p => p.price < 20000, products),
// products array를 인자로 받아 filter를 실행한다.
products => map(p => p.price, products),
// filter된 products를 인자로 받아 map을 실행하여 price로 mapping한다.
price => reduce(add, price),
// price로 mapping된 인자를 받아 reduce를 실행한다.
log
// 최종 값을 log에 담아 실행한다.
)
go를 이용하면 이처럼 위에서부터 아래로 순차적으로 읽기 좋게 코드를 작성할 수 있다.
커리는 함수를 값으로 다루면서 받아둔 함수를 원하는 시점에 평가시키는 함수이다.
커리는 함수를 받아서 함수를 리턴하고, 인자를 받아서 인자가 원하는 개수만큼의 인자가 들어왔을 때 받아두었던 함수를 나중에 평가시키는 함수다.
const curry = f =>
(a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._);
// 1. 함수를 받아서 (a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._);
// 함수를 리턴한다.
// 2. 리턴된 함수가 실행되었을 때, 받은 인자가 2개 이상
// (_.length가 존재한다는 것은 받은 인자가 총 2개 이상이라는 것이다.)
// 일 경우 받아둔 함수를 즉시 실행하고, 인자가 2개 이하라면 함수를 다시 리턴한다.
// 함수를 리턴한 후 그 이후에 받은 인자를 합해서 함수를 실행한다.
const mult = curry((a, b) => a * b);
log(mult); // (a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._)
log(mult(1)); // (..._) => f(a, ..._)
log(mult(1)(2)); // 2;
const mult3 = mult(3); // 인자가 2개 필요한 mult에 미리 인자 1개를 담아둔다.
log(mult3(10)); // mult(3, 10);이 되어 30을 리턴한다.
log(mult3(5)); // mult(3, 10);이 되어 30을 리턴한다.
log(mult3(3)); // mult(3, 10);이 되어 30을 리턴한다.
이렇게 받아야하는 인자를 받을 때까지 함수로 대기하다가 인자가 채워지는 순간 값이 리턴되는 형태다.
const map = curry((f, iter) => {
// f : 함수 파라미터, iter : 이터러블 프로토콜을 따르는 파라미터
let result = [];
for (const a of iter) {
result.push(f(a)); // 어떤 값을 수집할 것인지는 f에게 위임한다.
}
return result;
});
const filter = curry((f, iter) => {
let result = [];
for (const a of iter) {
if (f(a)) result.push(a);
}
return result;
});
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;
});
map, reduce, filter를 curry로 감싸주게될 경우 인자를 하나만 받으면 이후 인자를 더 받을 수 있는 함수를 리턴하게 된다.
// 위에서 적은 go
go(
products,
// products array를 받는다.
products => filter(p => p.price < 20000, products),
// products array를 인자로 받아 filter를 실행한다.
products => map(p => p.price, products),
// filter된 products를 인자로 받아 map을 실행하여 price로 mapping한다.
price => reduce(add, price),
// price로 mapping된 인자를 받아 reduce를 실행한다.
log
// 최종 값을 log에 담아 실행한다.
)
// curry로 감싸진 go의 활용1
go(
products,
// products array를 받는다.
products => filter(p => p.price < 20000)(products),
// 1. products array를 인자로 받아 추가 인자를 받을 수 있는 함수를 리턴한다.
// 2. 그 이후 인자를 products를 받아 실행시킬 수 있다.
products => map(p => p.price)(products),
// 위와 동일하다.
price => reduce(add)(price),
// 위와 동일하다.
log
// 최종 값을 log에 담아 실행한다.
)
// curry로 감싸진 go의 활용2
go(
products,
filter(p => p.price < 20000),
// curry를 통해 filter의 인자로 products를 받기 때문에 이런 형태가 가능하다.
map(p => p.price),
// 위와 동일하다.
reduce(add),
// 위와 동일하다.
log
// 최종 값을 log에 담아 실행한다.
)
// 서로 다른 일을 하지만 중복되는 요소가 많은 경우.
go(
products,
filter(p => p.price < 20000),
map(p => p.price),
reduce(add),
log
);
go(
products,
filter(p => p.price >= 20000),
map(p => p.price),
reduce(add),
log
);
// pipe를 통해 합성함수를 total_price에 할당하여 활용한다.
const total_price = pipe(
map(p => p.price),
reduce(add),
);
go(
products,
filter(p => p.price < 20000),
total_price,
log
);
go(
products,
filter(p => p.price >= 20000),
total_price,
log
);
// 함수를 하나 받아서 pipe를 반환하는 함수다.
const base_total_price = predi => pipe(
filter(predi),
total_price
);
go(
products,
base_total_price(p => p.price < 20000),
// pipe에 함수를 할당했으니 합성된 함수인 filter와 total_price가 도출된다.
log
);
go(
products,
base_total_price(p => p.price >= 20000),
log
);
const products = [
{ name: '반팔티', price: 15000, quantity: 1 },
{ name: '긴팔티', price: 20000, quantity: 2 },
{ name: '핸드폰케이스', price: 15000, quantity: 3 },
{ name: '후드티', price: 30000, quantity: 4 },
{ name: '바지', price: 25000, quantity: 5 },
];
const add = (a, b) => a + b;
// products를 입력하면 총 수량을 반환하는 함수.
// 추상화1
const total_quantity_go = products => go(products,
map(p => p.quantity),
// quantity만 추출
reduce(add),
// 순차적으로 1+2 -> 3+3 -> 6+4 -> 10+5 = 15
);
log(total_quantity_go(products)); // 15
// 추상화2 : 이 함수는 products에 의존적인 문제가 있다.
// pipe를 통해 합성된 함수들을 total_quantity_pipe에 할당.
const total_quantity_pipe = pipe(
map(p => p.quantity),
// quantity만 추출
reduce(add),
// 순차적으로 1+2 -> 3+3 -> 6+4 -> 10+5 = 15
);
log(total_quantity_pipe(products)); // 15
// 전체 상품의 가격을 반환하는 함수
const total_price = pipe(
map(p => p.price * p.quantity),
reduce(add)
);
log(total_price(products)); // 34500
// 추상화3 : 이를 통해 products에 대한 의존성을 제거했다.
const sum = curry((f, iter) => go(
iter,
map(f),
reduce(add)
));
log(sum(p => p.quantity, products));
log(sum(p => p.price * p.quantity, products));
const total_quantity_pipe_추상화3 = products =>
sum(p => p.quantity, products);
log(total_quantity_pipe_추상화3(products)); // 15
const total_price_추상화3 = products =>
sum(p => p.price * p.quantity, products);
log(total_price_추상화3(products)); // 34500
// 이렇게 추상화 레벨이 높은 sum을 다음과 같이 활용할 수 있다.
log(sum(user => user.age, [ // 60
{ age: 30 },
{ age: 20 },
{ age: 10 }
]));