Week2. 함수형 JS 기본 - 일급 함수, 고차함수, generator, iterator
코드가 계산되는 것.
const add10 = num => num + 10; // add10이라는 변수에 함수를 담을 수 있음.
const a = 10; // a = 10이란 변수가 있을 때
const add20 = func => func(a) + 10; // 함수의 인자로 func(함수)가 사용됨.
console.log(add20(add10)) // 40
JS의 함수는 일급 함수이다. 일급 함수는 일급 시민의 조건들을 충족하고 있다는 뜻이다.
조합성과 추상화의 도구
조합성
: 일급 함수의 특징을 활용해 함수 조합이 쉽기 때문에 확장성이 뛰어나다.추상화
: 함수형 프로그래밍은 선언형 프로그래밍
의 추상화를 적극적으로 활용해 개발자가 문제해결에만 집중할 수 있게 도와준다. 프로그래밍 패러다임에서 명령형
과 선언형
의 차이를 예제를 통해 설명했으니 참고바란다.
고차 함수
: 일급 함수의 특징 중 (1) 함수를 인자로 받아 실행하는 함수 (2) 함수의 결과값으로 함수를 사용하는 함수 (클로저를 만들어 리턴하는 함수)들을 이용해 고차 함수를 만들 수 있다.
const apply1 = f => f(1); // 함수를 인자값으로 받고 있음!
const add2 = a => a + 2;
log(apply1(add2)); // apply1 함수는 add2 함수를 인자로 받아 안에서 add2(1)를 실행함.
위 예시를 통해 apply1 함수는 고차함수임을 알 수 있다.
const times = (func, num) => { // num만큼 func 함수를 실행하는 함수
let i = 0;
while (i++ < n) func(i);
};
times(console.log, 3); // 1, 2, 3
times 함수는 인자로 func 함수를 받아 내부에서 호출하기 때문에 고차함수이다.
times 함수는 보통 applicative 프로그래밍
이라고 부른다.
참고: Applicative Functor의 수학적 이해
const addMaker = a => b => a + b; // a와 b를 인자로 받아 둘을 더하는 함수.
const add10 = addMaker(10);
console.log(add10); // b => 10 + b 리턴.
console.log(add10(5)); // 15
addMaker(10)
은 b => 10 + b
로 함수를 리턴하고 있다. 함수의 결과값이 함수이며, 리턴된 함수가 10이라는 값을 계속 기억하고 있으므로 addMaker
함수는 클로저이다.
고차함수의 2번은 클로저
개념을 잘 숙달하고 있어야 이해하기 쉽다. 우리 팀원분께서 클로저에 대해 [TIL] JavaScript의 클로저 여기에 정리를 잘해주셔서 참고하고 이해해보자.
기존의 for
문과 달리 for of
문은 내부 로직이 추상화되어 Array, Map, Set을 순회할 수 있다. 하지만 for
문과 달리 인덱스를 통해 해당 요소에 접근하지 않는다. Map과 Set을 인덱스로 조회했을 때 undefined
값이 출력된다는 것은 인덱스로 접근하지 않는다는 의미이다.
Array, Map, Set 모두 Symbol.iterator
메서드를 갖고 있다.
iterable
: iterator
를 리턴하는 [Symbol.iterator]()
를 가진 값.
iterator
: {value, done}
을 키로 가진 객체를 리턴하는 next()를 가진 값.
iterable / iterator 프로토콜
: iterable
을 for of
, 전개 연산자 등과 함께 동작하도록 약속한 규약.
🧐 설명: [Symbol.iterator] 메서드를 실행했을 때 iterator
가 리턴되며, iterator.next()
를 통해 {value, done}
을 키로 가진 객체를 리턴한다.
// Array
const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator]();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
// {value: 1, done: false};
// {value: 2, done: false};
// {value: 3, done: false};
// {value: undefined, done: true};
즉, for of
문은 iterable / iterator 프로토콜
을 따르고 있기 때문에, iterator.next()의 value들을 조회하는 식으로 작동한다. 인덱스에 직접 접근하는 것이 아니기 때문에 Map과 Set도 같은 방식으로 조회가 가능한 것이다.
// Map
const map = new Map([['a', 1], ['b', 2]]);
const iterator2 = map[Symbol.iterator]();
console.log(iterator2.next());
console.log(iterator2.next());
console.log(iterator2.next());
// {value: ['a', 1], done: false};
// {value: ['b', 2], done: false};
// {value: undefined, done: true};
// Map의 keys(), values(), entries()
const keyIterator = map.keys();
const sameAsKeyIterator = keyIterator[Symbol.iterator](); // 자기자신 반환
console.log(keyIterator.next());
console.log(keyIterator.next());
console.log(keyIterator.next());
// {value: 'a', done: false}
// {value: 'b', done: false}
// {value: undefined, done: true}
Map의 경우, keys()
, values()
, entries()
를 메서드로 갖고 있다. 이를 실행하면 iterator가 반환이 되는데 이는 next 메서드를 통해 {value, done} 형태의 객체의 접근할 수 있다는 의미이며, for of
문 등의 iterable / iterator 프로토콜
을 따르는 것들을 순회할 수 있다.
이터러블을 직접 만들 땐, [Symbol.iterator]
메서드 호출 시 자기 자신이 반환되도록 구현하는 것이 제일 중요하다. iterable.next()로 어느 정도 진행되고 난 후 for of
문을 돌려도 이전 상태를 기억해 조회가 가능해야 한다.
// 배열 1, 2, 3을 출력하는 로직
const iterable = {
[Symbol.iterator]() {
let i = 1;
return { // iterator 반환
next() {
return (i > 3) ? {done: true} : {value: i++, done: false}
},
[Symbol.iterator]() { // ⭐️ 자기 자신을 반환 = well-formed iterator
return this;
}
}
}
}
const iterator = iterable[Symbol.iterator]();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
// {value: 1, done: false};
// {value: 2, done: false};
// {value: 3, done: false};
// {done: true};
for (const value of iterable) console.log(value); // 1, 2, 3
for (const value of iterator) console.log(value); // 1, 2, 3
iterator
를 리턴하는 함수.*
을 붙여 만듦.function *gen() {
// iterator 반환
yield 1; // {value: 1, done: false}
yield 2; // {value: 2, done: false}
yield 3; // {value: 3, done: false}
return 100; // done이 true일 때 return 값도 정할 수 있음.
}
let iterator = gen();
console.log(iterator.next()); // {value: 1, done: false};
console.log(iterator.next()); // {value: 2, done: false};
console.log(iterator.next()); // {value: 3, done: false};
console.log(iterator.next()); // {value: 100, done: true};
iterable
이기도 하다.iterable
은 [Symbol.iterator]
를 메서드로 갖고 있다.[Symbol.iterator]
() 시 next를 메서드로 가진 iterator를 반환하고, 그 안에서 [Symbol.iterator]
메서드를 또 호출해 자기 자신을 반환하기 때문에 well-formed iterator
를 반환한다 할 수 있다.console.log(iterator[Symbol.iterator]() === iterator); // true
for (const value of gen()) console.log(value) // 1, 2, 3
for (const value of iterator) console.log(value) // 1, 2, 3
well-formed iterator
를 반환하기 때문에 iterator
로 for of
문을 돌릴 수 있는 것이다.
만약 well-formed iterator
가 구현되어 있지 않다면 gen()
이 반환한 iterator를 저장한 변수 iterator
로 for of
문을 돌려도 순회가 되지 않을 것이다.
function *gen() {
// iterator 반환
yield 1; // {value: 1, done: false}
if (false) yield 2; // {value: 2, done: false}
yield 3; // {value: 3, done: false}
return 100;
for (const value of gen()) console.log(value); // 1, 3
}
최대한 일목요연하게 작성하고 싶은데 자꾸만 코드로 설명하게 된다 🥲. generator
의 yield
로 값을 설정할 경우, if 문을 통해서 순회될 것들만 추릴 수도 있다.
generator
는 위 예시와 같이 yield
, if
문의 문장을 통해 순회할 수 있다. 이는 JS에서 함수형 프로그래밍을 사용할 때 generator
로 문장을 통해 어떠한 값도 순회할 수 있다는 상징성을 갖는다. 어떠한 값이든 generator를 통해 순회할 수 있고, 또 조작하여 원하는 값만 조회할 수도 있으므로 조합성에 큰 도움이 된다.
// 홀수만 조회
function *infinity(i = 0) {
while (true) yield i++;
}
function *odds(limitedValue) {
for (const value of infinity(1)) {
if (value > limitedValue) return;
if (value % 2) yield value;
}
}
let iterator = odds(6);
console.log(iterator.next()) // {value: 1, done: false}
console.log(iterator.next()) // {value: 3, done: false}
console.log(iterator.next()) // {value: 5, done: false}
for (const value of odds(6)) console.log(value); // 1, 3, 5
함수형 프로그래밍에 대해 막연히 겁을 먹고 있었는데, 막상 generator나 이터러블, 고차함수 등 기본 개념을 알고 나니 너무너무 재미있다! 오늘 강의의 핵심은 generator인데 generator를 이용하면 이터러블을 순회할 때 문장으로 필요한 것만 골라 쓸 수 있다는 점이 신기하고, 이를 이용해 함수형 프로그래밍에서 조합성을 높일 수 있다는 것을 알았다.
generator 활용버전에서 슬슬 앞 개념이 헷갈리기 시작해서 다시 한 번 복습을 하고 나면 내일 강의도 재미있게 수강할 수 있을 것 같다. 나는 역시 원리를 이해하고 사용해야 답답한 마음이 사라지고 눈도 맑아지고 세상을 긍정적으로 바라보게 된다는 것을 오늘 또 깨닫는다.