ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [함수형 프로그래밍] reduce, go, pipe, curry 로 풀어보는 코딩테스트
    JavaScript 2022. 3. 9. 10:29

     

    https://programmers.co.kr/learn/courses/30/lessons/87389

    이 문제의 답은 사실 아래와 같이 정말 간단하게 풀어볼 수 있습니다. 하지만 reduce, map, go를 활용하여 이를 풀어보도록 하겠습니다!

    const solution = n => {
        for(let i = 1; i<n; i+=1) {
            if(n%i===1) return i
        }
    }
    

     

    우선 제너레이터를 통하여 우리가 호출을 할 때에만 값을 내놓는 함수를 만들어보겠습니다.

    function* infinite(n) {
      let i = 1;
      while (i < n) {
        yield i;
        i += 1;
      }
    }
    

    위의 함수는 단순히 1씩 수를 증가시키면서 값을 반환하는 함수입니다.

    하지만 우리가 원하는 결과는 n을 x로 나누었을 때 나머지가 1이 되는 가장 작은 수입니다.

    이를 표현해 보자면

    function* minNum(n) {
      for (const num of infinite(n))
        if (n % num === 1) {
          return yield num;
        }
    }
    

    다음과 같이 나타낼 수 있습니다.

    그렇다면 이제 원하는 결과를 출력해야하는데요, minNum을 실행 한 결과는 이터레이터가 나올것입니다.

    처음 조건이 맞는 결과를 return하고 종료하기 때문에 하나의 value가 나올텐데요, 이터레이터이기 때문에 reduce를 시켜준다면 원하는 답을 반환할 수 있을것 같습니다.

    reduce

    reduce는 이터레이터를 받아서 원하는 행위를 연속적으로 수행하는 동작을 합니다. 그렇다면 어떤 행위를 할 지 외부에 위임해보도록 하겠습니다.

    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의 경우 두 번째 인자에 초기값을 넣어주지 않는다면 이터레이터의 첫 번째 값을 초기값으로 설정하는데요, 첫번째 분기에서 이 처리를 해 줍니다.

    다음으로 이터레이터를 순회하며 함수의 인자에 지속적으로 이터레이터의 값을 연산합니다.

    한 번 활용해 보겠습니다.

    const people = [
      { name: "jay", age: 15 },
      { name: "roy", age: 22 },
      { name: "joy", age: 11 },
      { name: "ray", age: 16 },
    ];
    const add = (a, b) => a + b;
    
    console.log(
      reduce(
        add,
        map(
          (p) => p.age,
          filter((p) => p.age < 22, people)
        )
      )
    );
    

    다음의 코드를 보면 reduce의 함수로 add를 넣어줬습니다. 다음으로 Iterator는 map을 넣어주었습니다.

    그렇다면 map은 첫번째 인자로 평가할 함수를, 두번째 인자로 이터레이터를 넣어줍니다. 그럼, filter함수를 다시 보겠습니다.

    filter함수는 map과 동작은 동일하지만 원하는 조건만 반환합니다.

    즉 위의 코드를 해석하자면, add를 할건데, 어떤 이터레이터를 더하냐면, people중에서 22살보다 어린 사람들중에서 age만을 더해라! 라는 뜻입니다.

    원하는 결과를 문장처럼 표현할 수 있어 표현력은 좋아졌지만, 아래에서 위로 읽어야하는 순서때문에 약간 어색해 보이기도 합니다. 이를 한번 go를 사용해서 해결해 보겠습니다.

    go

    바로 코드부터 보겠습니다.

    const go = (...args) => reduce((a, f) => f(a), args)
    

    go의 경우 받은 인자들을 reduce의 두 번째 인자로 전달하고, 행위는 값과 함수를 받아서 실행합니다.

    그렇다면 reduce를 사요했던 코드를 go를 통해 바꿔보겠습니다.

    go(
      people,
      (people) => filter((people) => people.age < 22, people),
      (people) => map((people) => people.age, people),
      log
    );
    

    아래와 같이 선언할 수 있는데요, people을 받아서 filter를 하고 map을해라! 이렇게 해석할 수 있습니다. reduce를 썼을 때 보다 훨씬 사람이 읽기 좋은 코드가 된것 같습니다.

    동작 원리를 보자면

    1. go에 들어온 인자들은 reduce의 두 번째 인자의 args에 들어가게 됩니다.
    2. reduce의 iter가 없기 때문에 acc는 iter의 첫번째, 즉 people이 됩니다.
    3. 다음으로 이터레이터를 순회하면서 reduce의 첫번째 인자의 함수를 실행하는데요, 실행시킬 함수는 (a,f) ⇒ f(a)입니다. 즉 이터레이터에 들어있는 함수들을 하나씩 꺼내면서 acc를 인자로 전달하고 그 결과를 다시 반환하는 함수입니다.

    여기까지만 해도 정말 표현력이 좋아졌다고 할 수 있는데요, 하지만 약간은 부족한 감이 있습니다. 위의 함수들은 원하는 결과를 얻기 위해 한번에 인자들을 넣어주어야 합니다. 하지만 내가 원하는 시점에 인자들을 따로 넣고 싶다는 생각을 할 수 있습니다.

    예를 들어 const plus = (a, b) ⇒ a+b 라는 함수가 있을 떄 a과 b의 값을 원하는 시점에 따로 받는다면 더 재사용성이 높을 수 있고, const plus3 = plus(3) 다음과 같은 형태로 만들어 plus3(2) 이런 방식으로도 사용할 수 있을것 같습니다.

    이렇게 하고 싶을 때 문제점이 생기는데 어떻게 먼저 넣은 인자를 기억하느냐 입니다.

    우리는 클로저라는 개념을 알고 있습니다. 따라서 클로저를 활용한다면 먼저 넣은 인자를 기억할 수 있을것 같습니다. 그럼 한번 만들어 보도록 하겠습니다.

    curry

    const curry =
      (f) =>
      (a, ..._) =>
        _.length ? f(a, ..._) : (..._) => f(a, ..._);
    

    다음과 같이 할 수 있는데요, curry는 함수를 반환하는 함수입니다.

    그렇다면 반환되는 함수는 어떤 함수일까요?

    인자들을 받은 다음, 인자가 하나 아니라 여러개라면 처음 받은 함수를 바로 실행합니다.

    하지만 인자가 하나 받았다면, 다시 함수를 반환하는데 그 함수는 인자들을 받은 다음 함수를 실행합니다.

    const mult = curry((a, b) => a * b);
    const mult3 = mult(3);
    log(mult3(2));
    

    즉, curry는 처음에 함수를 받습니다. 그 함수는 인자 두개를 받아 그 곱을 반환하는 함수입니다.

    첫번째 줄에서 평가된 결과는 아래와 같습니다.

    (a, ..._) =>
        _.length ? mult(a, ..._) : (..._) => mult(a, ..._);
    

    즉, 인자들을 받아야 하는데요, mult3의 경우 a는 있지만 나머지 파라미터는 없습니다. 따라서 다시 함수를 반환하는데요, 그 결과는 (...) ⇒ mult(3, ...) 가 됩니다. 마지막줄에서 mult3에서 2를 인자로 전달해 주면 curry함수의 생명주기가 종료하게 되며 6이라는 결과를 반환하게 됩니다.

    이처럼 커리함수는 내부의 생명주기가 종료되지 않았다면, 외부함수의 스코프를 스코프체인에 따라 참조할 수있는 자바스크립트 문법을 활용하여 만들 수 있습니다.

    그럼 우리가 만든 reduce를 curry로 만들어준다면 더 쉽게 사용할 수 있습니다.

    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를 curry로 감싸주게 된다면, 우리는 기존에 만들었던 go 함수를 더 축약해서 사용할 수 있습니다.

    go(
      people,
      filter((p) => p.age < 22),
      map((p) => p.age),
      log
    );
    

    그럼 우리가 만든 유틸 함수들을 활용한다면 결과는 다음과 같습니다.

    const curry =
      (f) =>
      (a, ..._) =>
        _.length ? f(a, ..._) : (..._) => f(a, ..._);
    
    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;
    });
    
    const go = (...args) => reduce((a, f) => f(a), args);
    
    const pipe =
      (f, ...fs) =>
      (...as) =>
        go(f(...as), ...fs);
    
    function* infinite(n) {
      let i = 1;
      while (i < n) {
        yield i;
        i += 1;
      }
    }
    
    function* minNum(n) {
      for (const num of infinite(n))
        if (n % num === 1) {
          return yield num;
        }
    }
    const solution = n => go(
        n,
      minNum,
      reduce((a, b) => a + b)
    );
    
    solution;
    

    이렇게만 해도 모든 케스트케이스는 통과합니다.

    하지만 진짜 마지막으로 pipe함수에 대해 알아보도록 하겠습니다.

    pipe

    pipe함수는 내부적으로 go함수를 사용합니다. go함수의 문제점은 한번에 여러개의 초기값을 넣어주지 못합니다.

    따라서 이를 해결 할 수 있는 pipe함수를 만들어 보겠습니다.

    pipe함수 또한 curry와 유사한 개념입니다.

    const pipe =
      (f, ...fs) =>
      (...as) =>
        go(f(...as), ...fs);
    

    다음과 같이 사용할 수 있는데요, 함수를 반환하는 함수입니다. 그리고, 그 반환된 함수에 여러 인자를 전달하여 처음 pipe를 선언할 때 인자로 전달했던 f에 그 인자들을 넣어서 실행한 결과를 go의 파라미터로 넣어주는 역할을 합니다.

    아래와 같이 활용할 수 있습니다.

    const f = pipe(
      (a, b) => a + b,
      (a) => a + 10,
      (a) => a + 100
    );
    log(f(1, 3));
    

    pipe까지 활용한다면 최종코드는 다음과 같습니다.

    최종 코드

    const curry =
      (f) =>
      (a, ..._) =>
        _.length ? f(a, ..._) : (..._) => f(a, ..._);
    
    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;
    });
    
    const go = (...args) => reduce((a, f) => f(a), args);
    
    const pipe =
      (f, ...fs) =>
      (...as) =>
        go(f(...as), ...fs);
    
    function* infinite(n) {
      let i = 1;
      while (i < n) {
        yield i;
        i += 1;
      }
    }
    
    function* minNum(n) {
      for (const num of infinite(n))
        if (n % num === 1) {
          return yield num;
        }
    }
    
    const add = (a,b) => a + b
    
    const solution = pipe(
      minNum,
      reduce(add)
    );
    
    solution;
    

    느낀점

    사실 정말 간단하게 풀 수 있는 문제지만 여러 유틸함수들을 사용하다보니 길어진 느낌이 있습니다.

    하지만 이런 함수형 패러다임을 적용한다면 더 표현력이 높고, 추상화 및 재사용성이 있는 함수들을 만들 수 있다고 생각합니다.

    (marpple팀의 fx라이브러리 사랑합니다...)

     

    출처:

    https://github.com/marpple/FxJS/tree/master/Strict

    댓글

Designed by Tistory.