Generator의 소개와 문제 풀어보기[Functional Programming]

Dengo·2023년 3월 24일
0

Functional Programming

목록 보기
2/2
post-thumbnail

Generator란?

제너레이터는 이터레이터이자 이터러블을 생성하는 함수 입니다.
저의 언어로 조금 더 풀어서 설명하면
마치 나만의 이터러블을 쉽게 만든다는 느낌입니다.

function* gen() {
  yield 10;
  yield 20;
  yield 30;
}

이런 제너레이터 함수를 선언한 후에

const iter = gen();
iter.next();

이런식으로 사용이 가능한 것이죠.
별로 어렵게 생각할 것이 없습니다.

range 함수 만들어보기

infinity함수를 먼저 만들어보자.

다음과 같은 infinity 함수가 있다고 생각해보죠

function* infinity(i = 0) {
  while (true) yield i++;
}

예상 가능하다시피 0부터 시작하여 값을 계속 무한대로 뽑을 수 있는 함수입니다.
만약에 제너레이터가 아니라 배열을 만들어서 반환을 하는 함수였다면 어땠을까요?
아마 에러가 났을 것 입니다.

limit함수와 range함수

여기에서 이터러블을 본격적으로 활용하는 것을 맛볼건데요.

function* limit(num, iter) {
  for (const v of iter) {
    yield v;
    if (v === num) return;
  }
}

function* range(num) {
  for (const v of limit(num, infinity(0))) {
    yield v;
  }
}

limit함수와 함께 range 함수를 만들었습니다.
여기서 '왜 range함수를 굳이 이렇게 만들었어?'라는 생각이 들 수 있습니다만,
이번 예제를 통해서는 이터러블의 동작 방법에 주목을 해주셨으면 좋겠습니다

limit함수는 원하는 갯수만큼을 앞에서 부터 잘라서 yield 해주는 제너레이터 함수인데요.

만약에 infinity와 limit의 구현에 있어서 이터러블과 제너레이터와 같은 개념없이 만들었다고 생각을 해보죠. 어땠을까요?

  1. [1, 2, 3, 4, .... 무한대]의 배열을 먼저 만든다. (여기서 바로 오류)
  2. 앞에서 원하는 갯수 만큼 자른다.

우리가 기존에 이해하고 있는 (이터러블과 제너레이터를 활용을 안하는) 방식대로라면 위의 흐름이 자연스럽습니다. 말이 안된다고 바로 깨닫고 다른 방법으로 구현을 하겠죠.
하지만 여기서 사용한 방식은 흥미로운 점이 있는데요.

바로 2번. 앞에서 원하는 갯수 만큼 자른다에서 시작이 된다는 점 입니다.

range함수에서 값을 가져오는 과정 (지연 평가)

function* range(num) {
  for (const v of limit(num, infinity(0))) {
    yield v;
  }
}

여기 range함수에서 보시다시피 for of 는 limit으로부터 하나의 값을 원하고 있습니다.
즉 limit이라는 제너레이터 함수에 대하여 next값을 요청하고 있는 것이죠.

하지만 흥미롭게도 limit함수안에서도 limit이 받은 이터러블의 값을 for of 가 요구하고 있습니다.
자연스럽게 이 바통은 inifinity에게 넘어가게 되고 이제서야 비로소 infinity는 0을 limit에게 전달, 그리고 그것을 range에서 사용한 for of에게 전달해줍니다.

이런 방식이기 때문에 limit함수 선에서 infinity함수의 값을 원하는 만큼만 사용가능하게 된 것 이죠.

이것을 이터러블 프로그래밍의 '지연 평가 (Lazy Evaluation)'라고 부릅니다. 제너레이터가 이를 가능케 해주는 것이죠.

지연평가를 활용해서 알고리즘 문제 풀어보기


문제 : 프로그래머스 - 모의고사

문제의 내용은 간단합니다.
주어진 패턴으로 찍었을때 주어진 답과 비교하여 얼마나 맞았을거냐는 문젠데요,

저는 이 문제를 풀 때 각 수포자가 찍는 값의 데이터가 answers의 길이 만큼 주어졌으면 좋겠다고 생각했습니다.

하지만 각 수포자의 패턴으로 answers의 길이만큼 잘린 배열을 구하고 answers와 비교할 생각을 하니 매우 귀찮았습니다.

각 수포자가 내놓는 답을 제너레이터를 통해서 지연적으로 받는다면 어떨까요?
아래와 같이 구현할 수 있겠습니다.

function* gen(arr) {
    while(true){
        for (const a of arr) {
            yield a;
        }
    }
}

const grade = (a, b) => a === b ? 1 : 0;

function solution(answers) {
    const firstPattern = gen([1, 2, 3, 4, 5]);
    const secondPattern = gen([2, 1, 2, 3, 2, 4, 2, 5]);
    const thirdPattern = gen([3, 3, 1, 1, 2, 2, 4, 4, 5, 5]);
    
    let scores = [0, 0, 0];
    
    answers.forEach(answer => {
        scores[0] += grade(answer, firstPattern.next().value);
        scores[1] += grade(answer, secondPattern.next().value);
        scores[2] += grade(answer, thirdPattern.next().value);
    })
    
    return [...scores
                .entries()]
                .filter(([_, score]) => score === Math.max(...scores))
                .map(([index, _]) => index + 1);
}

여기서 answers를 순회하는 부분만 주목하면 좋을 것 같습니다.

answers.forEach(answer => {
    scores[0] += grade(answer, firstPattern.next().value);
    scores[1] += grade(answer, secondPattern.next().value);
    scores[2] += grade(answer, thirdPattern.next().value);
})

원래 계획대로 풀려고 했다면 간단한 로직임에도 코드가 길어졌을 것 입니다.
제너레이터를 이용해서 값을 끝도 없이 반복하여 뽑을 수 있는 코드를 작성했습니다. answers의 길이만큼만 수포자들의 값을 구해 간단하게 해결할 수 있습니다.

profile
Software Engineer (전산쟁이)

0개의 댓글