코드 평가
코드가 계산되어 값을 만드는 것을 의미한다.
일급 (First Class)
자바스크립트에서 일급이란 다음과 같은 의미가 있다.
- 값으로 다룰 수 있다.
- 변수에 담을 수 있다.
- 함수의 인자로 사용될 수 있다.
- 함수의 결과로 사용될 수 있다.
자바스크립트에서 일급 객체는 다음의 예시처럼 사용될 수 있다.
const a = 10;
const add10 = (a) => a + 10;
const r = add10(a);
자바스크립트에서 함수는 일급 객체에 속한다. 이와 관련된 내용은 모던 자바스크립트 딥다이브 정리에서 다룬 적 있다.
즉, 자바스크립트에서는 함수를 값으로서 다룰 수 있다. 이는 곧, 함수가 조합성과 추상화의 도구로 이용될 수 있음을 의미한다.
const add5 = (a) => a + 5;
log(add5);
log(add5(5));
const f1 = () => () => 1;
log(f1());
const f2 = f1();
log(f2);
log(f2());
고차 함수 (Higher order function)
- 함수를 값으로서 다루는 함수
- 함수를 인자로 받아서 실행할 수 있는 함수
다음의 예시처럼, 어떠한 함수를 값으로서 다루거나, 다른 함수를 인자로 받아 실행하는 함수를 고차 함수라고 한다.
const apply1 = (f) => f(1);
const add2 = (a) => a + 2;
log(apply1(add2));
log(apply1((a) => a - 1));
const times = (f, n) => {
let i = -1;
while (++i < n) f(i);
};
times(log, 3);
times((a) => log(a + 10), 3);ㅎ
또는 새로운 함수를 만들어서 반환하는 함수를 고차 함수라고 할 수 있다.
const addMaker = (a) => (b) => a + b;
const add10 = addMaker(10);
log(add10(5));
log(add10(10));
이터러블 (Iterable)
이터레이터를 반환하는[Symbol.iterator]()
를 가진 값
해당 객체들은Symbol.iterator
를 실행하면 이터레이터를 리턴한다.
이터레이터 (Iterator)
{value, done}
의 객체를 반환하는next()
를 가진 값
이터러블은 다음과 같이 구현해볼 수 있다.
순회가 완료되기 전 까지는 done이 false이며 해당 인덱스의 value가 반환된다. 이후 순회가 완료되면 마지막 차례의 value와 함께 done이 true가 된다.
해당 순회는 next()
메소드로 한 차례식 호출할 수 있다.
const iterable = {
[Symbol.iterator]() {
let i = 3;
return {
next() {
return i == 0 ? {done: true} : {value: i--, done: false};
},
[Symbol.iterator]() {
return this;
}
}
}
};
let iterator = iterable[Symbol.iterator]();
iterator.next();
iterator.next();
// log(iterator.next());
// log(iterator.next());
// log(iterator.next());
for (const a of iterator) {
log(a);
}
제너레이터 (Generator)
- 이터레이터이자 이터러블을 생성하는 함수
- 어떠한 값이든 순회가 가능하도록 만들 수 있다.
function* gen() {
yield 1;
if (false) yield 2;
yield 3;
}
let iter = gen();
log(iter[Symbol.iterator]() == iter);
log(iter.next());
log(iter.next());
log(iter.next());
log(iter.next());
for (const a of gen()) {
console.log(a);
};
배열을 순회하여 새로운 배열을 생성하는 함수
배열의 순서와 길이가 보장된다.
map
함수는 이터러블 프로토콜을 따르기 때문에 다형성을 갖고 있다.
가령, document.querySelectorAll()
의 경우 Array
를 상속받지 않기 때문에 map
함수를 사용할 수는 없다. 하지만 이터러블 프로토콜을 따르고 있기 때문에 순회 자체는 가능하다.
const map = (f, iter) => {
let res = [];
for (const a of iter) {
res.push(f(a));
}
return res;
};
console.log(map((el) => el.nodeName, document.querySelectorAll("*")));
제너레이터 또는 ES6의 Map 객체 역시도 이터러블 프로토콜을 따르고 있기 때문에 순회가 가능하다.
function* gen() {
yield 2;
if (false) yield 3;
yield 4;
}
console.log(map((a) => a * a, gen()));
let m = new Map();
m.set("a", 10);
m.set("b", 20);
console.log(new Map(map(([k, a]) => [k, a * 2], m)));
filter
배열을 순회하여 조건에 맞는 item만 골라낼 수 있는 함수
filter
로 생성한 배열의 길이가 원본과 같다는 보장이 없다.
const filter = (f, iter) => {
let res = [];
for (const a of iter) {
if (f(a)) res.push(a);
}
return res;
};
let under20000 = [];
for (const p of products) {
if (p.price < 20000) under20000.push(p);
}
console.log(...under20000);
console.log(...filter((p) => p.price < 20000, products));
reduce
배열을 순회하여 하나의 값으로 누적하여 만들어가는 함수
단 하나의 값을 반환한다.
reduce
에 함수를 인자로 전달하여 그 결과값을 누적하며 하나의 값으로 만들어낼 수도 있다.
const reduce = (f, acc, iter) => {
for (const a of iter) {
acc = f(acc, a);
}
return acc;
};
const add = (a, b) => a + b;
console.log(reduce(add, 0, [1, 2, 3, 4, 5]));
console.log(add(add(add(add(add(0, 1), 2), 3), 4), 5));
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((total_price, product) => total_price + product.price, 0, products));
함수형 프로그래밍의 핵심은 코드를 값으로 다룬다
는 점이다.
즉, 값으로 평가될 것들을 함수로 만들어서 사용할 수 있다.
앞서 다뤘던 map
, filter
, reduce
를 활용하면 다음과 같이 함수를 값으로서 사용할 수 있다.
const log = console.log;
const map = (f, iter) => {
let res = [];
for (const a of iter) {
res.push(f(a));
}
return res;
};
const filter = (f, iter) => {
let res = [];
for (const a of iter) {
if (f(a)) res.push(a);
}
return res;
};
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;
};
// index.js
const products = [
{ name: "반팔티", price: 15000 },
{ name: "긴팔티", price: 20000 },
{ name: "핸드폰케이스", price: 15000 },
{ name: "후드티", price: 30000 },
{ name: "바지", price: 25000 },
];
const add = (a, b) => a + b;
log(
reduce(
add,
map(
(p) => p.price,
filter((p) => p.price < 20000, products)
)
)
);
log(
reduce(
add,
filter(
(n) => n >= 20000,
map((p) => p.price, products)
)
)
);
reduce의 경우, 두 번째 인자로 정수형의 배열이 올 것을 기대할 수 있다. 즉, 어떠한 방식으로던간에 정수형의 배열만 전달하면 된다는 의미.
이를 map 또는 filter 함수 자체를 전달하여 그 반환값을 정수형 배열로 사용할 수 있다.
즉, 함수의 인자로서 올 값을 무조건 값
이라고만 여기지 않고, 해당 값을 평가해낼 수 있는 표현식 또는 함수를 대신 사용할 수 있다.
이것이 바로 함수형 프로그래밍에서 코드를 값으로 다룬다는 것을 의미한다.
자바스크립트에서 함수형 프로그래밍 하면 대표적으로 떠오르는 함수가 go
, pipe
등이 있다.
go
reduce를 활용해서 여러가지 함수나 인자를 순서대로 중첩해가며 실행하여, 하나의 값으로 만들어내는 함수
go는 다음의 예시처럼 생성할 수 있다.
const go = (...args) => reduce((a, f) => f(a), args);
go(
add(0, 1),
(a) => a + 10,
(a) => a + 100,
log
);
이처럼 reduce의 인자로 초기값과 함수를 전달하여, 초기값에 함수를 적용하는 것을 시작으로 여러 함수를 차례로 적용한 다음, 마지막에 한 개의 값을 반환할 수 있다.
함수들이 나열되어 있는 합성된 함수를 만드는 함수
go는 값을 반환하지만 pipe는 함수를 반환하는 함수이다.
즉, pipe는 인자로 받은 함수들을 모두 합쳐서 합성된 함수를 반환한다.
const go = (...args) => reduce((a, f) => f(a), args);
const pipe = (f, ...fs) => (...as) => go(f(...as), ...fs);
go(
add(0, 1),
(a) => a + 10,
(a) => a + 100,
log
);
const f = pipe(
(a, b) => a + b,
(a) => a + 10,
(a) => a + 100
);
log(f(0, 1));