함수형 자바스크립트의 기초

2017년 03월 15일


핵심요소 직접 만들기

앞서 알아본 문법과 몇 가지 자바스크립트의 특성을 조금만 응용하면 함수형 프로그래밍의 기초가 되는 거의 모든 기능을 직접 구현할 수 있습니다.

값은 가능한 const로 선언하고, 확실한 상황에서만 트랜션트를 사용합니다.

고차함수

고차함수(Higher Order Function)는 함수를 인자로 받는 함수 혹은 함수를 반환하는 함수를 의미합니다.

사용할 수 있는 고차함수가 많아질수록 함수를 직접 정의할 필요가 줄어듭니다.

다음은 숫자 n과 함수 f를 받아 새로운 함수를 반환하는 고차함수 applyN입니다 applyN은 f를 n번 반복해서 호출하는 새로운 함수를 만듭니다.

재귀를 사용해서 구현할 수도 있지만 여기서는 간단히 트랜션트를 사용했습니다.

function applyN(n, f) {
  return (x) => { 
    for (let i = 0; i < n; ++i) 
      x = f(x);

    return x;
  }
}

const inc = (x) => x + 1;
const add10 = applyN(10, inc);
add10(90); // 100;

이 트랜션트는 살짝 트릭이 있습니다. x는 지역변수가 아니지만, x에 대한 할당이 값이 아닌 참조만을 변경하기 때문에 할당이 함수 밖에 영향을 주지 않습니다.

또 다른 고차함수로는 인자로 함수(콜백)을 받는 함수가 있습니다. 동작의 일부를 유연하게 파라미터화 하고 싶을때 자주 사용하는 방법입니다.

const xs = [0, 1, 2, 3];
const ys = xs.map((x) => x * 10); // [0, 10, 20, 30]

고차함수를 이용하면 함수를 직접 정의하지 않고 새로운 함수를 파생시키거나, 여러 함수를 조합해서 새로운 함수를 만들 수 있습니다.

이 두 가지 기능은 함수형 프로그래밍의 가장 기본이 되는 부분입니다. 원하는 기능을 직접 정의할 수 있지만, 파생과 조합을 이용해서 짜맞추는 방법도 있습니다.

함수 합성

함수 합성은 고차함수 응용의 한 가지입니다. 함수를 순서대로 호출할 때 사용합니다.

const compose2 = (f, g) => (x) => f(g(x));
const inc = (x) => x + 1;
const double = (x) => x * 2;

const f = compose2(inc, double);
const g = compose2(double, inc);

f(0); // inc(double(0)) -> 1
g(0); // double(inc(0)) -> 2

좀 더 유연하게 동작하도록 가변인자를 추가할 수 있습니다.

const compose2 = (f, g) => (...args) => f(g(...args));

const add = (x, y) => x + y;
const double = (x) => x * 2;

const f = compose2(double, add);
f(1, 1); // double(add(1, 1)) -> 4

작은 함수를 조합해 새로운 함수를 만드는 것은 함수형 프로그래밍의 핵심입니다.

방금 만든 compose2 함수와 자바스크립트 빌트인 메서드 Array.reduce를 조합해서 가변 갯수의 함수를 합성하는 compose 함수를 만들어 보겠습니다.

const compose2 = (f, g)  => (...args) => f(g(...args));
const compose  = (...fs) => (...args) => fs.reduce(compose2)(...args);

const inc = (x) => x + 1;
const double = (x) => x * 2;
const square = (x) => x * x;

const f = compose(inc, double, square);
const g = compose(square, double, inc);

f(0); // inc(double(square(0))) -> 1
g(0); // square(double(inc(0))) -> 4

상당히 밀도가 높은 코드입니다. reduce에 익숙하더라도 함수를 원소로 하는 배열에 reduce를 호출하는 것은 낯설 수 있습니다.

동작을 풀어쓰면 다음과 같습니다.

const compose2 = (f, g)  => (...args) => f(g(...args));
const compose  = (...fs) => (...args) => fs.reduce(compose2)(...args);

const inc = (x) => x + 1;
const double = (x) => x * 2;
const square = (x) => x * x;

const f = compose(inc, double, square);

// compose가 호출됩니다
compose(inc, double, square);

// 다음과 같은 형태로 변경됩니다, 초기값이 생략된 reduce입니다
(...args) => [inc, double, square].reduce(compose2)(...args);

// inc를 초기값으로 reduce를 시작합니다
[double, square].reduce(compose2, inc);

// inc에 double이 합성됩니다
[square].reduce(compose2, compose2(inc, double));

// 합성된 함수에 square가 합성된 뒤 reduce가 종료됩니다
[].reduce(compose2, compose2(compose2(inc, double), square));

// 중첩된 compose2를 전개합니다
(...args) => inc(double(square(...args)));

f(10); // 201

지금은 단순히 호출을 연속으로 이어주는 함수 합성을 만들었지만, 이어주는 과정에 특별한 동작을 추가할 수 있습니다.

대표적인 것이 비동기 합성입니다. Promise가 반환되는 것을 확신할 수 있다면 Promise.then을 이용해서 함수를 합성할 수 있습니다.

커링

커링(currying)은 인자가 여러개인 함수를 인자가 하나인 함수의 연속으로 바꾸는 것을 의미합니다.

화살표 함수가 우측연관이라는 성질을 이용해서 명시적으로 커링을 표현할 수 있습니다.

커링된 함수는 인자가 완성될 때 까지 새로운 함수를 반환하기 때문에 자연스럽게 고차함수를 만드는 특징이 있습니다.

const add = (x) => (y) => x + y;
const inc2 = add(2);

inc2(4);   // 6
add(2)(4); // 6
add(2, 4); // (y) => 2 + y

자바스크립트는 기본적으로 파라미터의 갯수를 검사하지 않기 때문에, 명시적인 커링이 그렇게 유용한 것은 아닙니다.

마지막 add(2, 4)가 대표적입니다. 이 함수는 2개의 인자를 사용했지만 하나의 인자로 커링됩니다.

이 방식의 문제점은 기존의 방식과 호환되지 않는다는 점입니다. 2개의 인자를 모두 전달하기 위해선 add(2)(4)라는 이상한 방식을 따라야합니다.

이러한 문제를 해결하기 위한 방법이 자동 커링입니다.

function curry(f) {
  const arity = f.length;
  const iter = (...args) => args.length < arity
    ? (...nextArgs) => iter(...args.concat(nextArgs))
      : f(...args);

  return iter;
}

const add3 = (x, y, z) => x + y + z;
const curriedAdd3 = curry(add3);

curriedAdd3(1)(2)(3); // 6
curriedAdd3(1, 2)(3); // 6
curriedAdd3(1)(2, 3); // 6
curriedAdd3(1, 2, 3); // 6
함수의 length 프로퍼티를 통해 함수 선언시 결정된 인자의 갯수를 파악할 수 있습니다.
예를 들어 add3.length는 3입니다.

이 함수는 약간 이해하기 어렵습니다. 구현을 이해하기 보단, 커링을 할 수 있다는 사실에 집중합시다.

curry 함수는 고차함수입니다. curry를 이용하면 커링된 새로운 함수를 파생시킬 수 있습니다.