TIL | 함수형 JS 기본

noopy·2021년 8월 9일
3

TIL

목록 보기
3/21

Week2. 함수형 JS 기본 - 일급 함수, 고차함수, generator, iterator

함수형 JS 기본기

평가

코드가 계산되는 것.

일급 시민의 조건

  • 으로 다룰 수 있음.
  • 변수에 담을 수 있음.
const add10 = num => num + 10; // add10이라는 변수에 함수를 담을 수 있음.
  • 함수의 인자로 사용될 수 있음.
const a = 10; // a = 10이란 변수가 있을 때
const add20 = func => func(a) + 10; // 함수의 인자로 func(함수)가 사용됨.
  • 함수의 결과(return 값) 로 사용될 수 있음.
console.log(add20(add10)) // 40

일급 함수

JS의 함수는 일급 함수이다. 일급 함수는 일급 시민의 조건들을 충족하고 있다는 뜻이다.

조합성과 추상화의 도구

  • 조합성: 일급 함수의 특징을 활용해 함수 조합이 쉽기 때문에 확장성이 뛰어나다.
  • 추상화: 함수형 프로그래밍은 선언형 프로그래밍의 추상화를 적극적으로 활용해 개발자가 문제해결에만 집중할 수 있게 도와준다.

프로그래밍 패러다임에서 명령형선언형의 차이를 예제를 통해 설명했으니 참고바란다.

고차 함수

고차 함수: 일급 함수의 특징 중 (1) 함수를 인자로 받아 실행하는 함수 (2) 함수의 결과값으로 함수를 사용하는 함수 (클로저를 만들어 리턴하는 함수)들을 이용해 고차 함수를 만들 수 있다.

  1. 함수를 인자로 받아 실행하는 함수
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의 수학적 이해

  1. 함수의 결과값으로 함수를 사용하는 함수 (클로저를 만들어 리턴하는 함수)
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 값이 출력된다는 것은 인덱스로 접근하지 않는다는 의미이다.

Symbol.iterator

  • Array[Symbol.iterator] = function () {};
  • Map[Symbol.iterator] = function () {};
  • Set[Symbol.iterator] = function () {};

Array, Map, Set 모두 Symbol.iterator 메서드를 갖고 있다.
iterable: iterator를 리턴하는 [Symbol.iterator]()를 가진 값.
iterator: {value, done}을 키로 가진 객체를 리턴하는 next()를 가진 값.
iterable / iterator 프로토콜: iterablefor 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

generator와 iterator

generator

  • 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};
  • generator 함수는 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를 반환하기 때문에 iteratorfor of 문을 돌릴 수 있는 것이다.
만약 well-formed iterator가 구현되어 있지 않다면 gen()이 반환한 iterator를 저장한 변수 iteratorfor 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
}

최대한 일목요연하게 작성하고 싶은데 자꾸만 코드로 설명하게 된다 🥲. generatoryield로 값을 설정할 경우, if 문을 통해서 순회될 것들만 추릴 수도 있다.

generator의 의의

generator는 위 예시와 같이 yield, if문의 문장을 통해 순회할 수 있다. 이는 JS에서 함수형 프로그래밍을 사용할 때 generator로 문장을 통해 어떠한 값도 순회할 수 있다는 상징성을 갖는다. 어떠한 값이든 generator를 통해 순회할 수 있고, 또 조작하여 원하는 값만 조회할 수도 있으므로 조합성에 큰 도움이 된다.

generator 예시 - odds

// 홀수만 조회
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 활용버전에서 슬슬 앞 개념이 헷갈리기 시작해서 다시 한 번 복습을 하고 나면 내일 강의도 재미있게 수강할 수 있을 것 같다. 나는 역시 원리를 이해하고 사용해야 답답한 마음이 사라지고 눈도 맑아지고 세상을 긍정적으로 바라보게 된다는 것을 오늘 또 깨닫는다.

참고 사이트)

profile
💪🏻 아는 걸 설명할 줄 아는 개발자 되기

0개의 댓글