쾌락코딩

Currying

|

javascript 라이브러리들을 사용하다보면, react-redux의 connect 함수와 같이 함수의 인자를

export default connect(mapStateToProps)(TodoApp)

함수(인자1)(인자2)

와 같이 받는 함수들이 꽤 있다. 이렇게 하는 이유와, 이 것의 용어, 내부 동작 방식이 궁금해서 여러 포스트를 보고, 유인동 님의 강의를 보면서 살짝 정리해 보았다. 우선 이 것의 용어는 Currying이다.

Currying

Adam Bene의 블로그에 따르면 currying이란 두개 이상의 인자를 갖는 함수를 인자를 하나만 갖게 하는 함수로 줄여나가는 방법이라고 한다.

f(n, m) --> f'(n)(m)
const multiply = (n, m) => (n * m)
multiply(3, 4) === 12 // true

// ------ currying -------

const curryedMultiply = (n) => {
  return (m) => multiply(n, m)
}
const triple = curryedMultiply(3)
triple(4) === 12 // true

우선 위의 curryedMultiply함수를 보면 처음으로 인자 n을 받고, n을 간직하고 있는 새로운 익명 함수를 반환하게 된다. 즉, 클로저가 된다.

조금 일반화 해서 보자면, curry함수는 아래와 같을 수 있다.

function _curry(fn) {
  return function(a) {
    return function(b) {
      return fn(a, b)
    }
  }
}

fn은 우리가 _curry함수를 호출할 넘겨줄 함수가 된다. 두 가지 수를 더하는 add 함수를 만들고자 할 경우 우리는 _curry의 힘을 빌려 이렇게 만들 수 있다.

const add = _curry((a,b) => (a + b))

이제 add의 구현체 부분은 곧

function(a) {
  return function(b) {
    return fn(a, b)
  }
}

이 부분인 것이며 이는 인자를 하나 받는다는 뜻이다. 결과적으로 우리는

add(10)(5)

와 같이 할 수 있다.

그런데 조금만 더 생각해 보면 add(a,b) 와 add(a)(b)를 혼용해서 사용할 수 있는 방법이 있다.

function _curry(fn) {
  return function(a,b) {
    return arguments.length == 2 ? fn(a,b) : function (b) { return fn(a,b); }
  }
}

이렇게 arguments의 갯수를 파악해서 조건에 따라 함수를 즉시 실행할지, 다른 함수를 반환할지를 결정하면 된다.

만약 a와 b의 위치를 변경하고 싶다면, 즉 b가 좌변에 오도록 하고 싶다면 _curryr이라는 함수를 아래와 같이 만들면 된다.

function _curry(fn) {
  return function(a,b) {
    return arguments.length == 2 ? fn(a,b) : function (b) { return fn(b,a); }
  }
}

사실 여기까지만 보아서는 왜 curry를 사용하는지, 어떤 이점이 있는지 몰랐다. 그냥 add함수를 만들고 사용하면 되지 왜 굳이 currying을 할까? 라는 생각이 가시질 않았다. 이후에 _get함수를 사용하는 사례를 보니 조금은 알 것 같았다.

_get함수는 어떤 object에 key값으로 접근하는 간단한 함수로 아래와 같다.

users = [{name:'leezep', age: '18'}, {name: 'elecoder', age: '15'}];

const _get = _curryr((obj,key) => (obj == null ? undefined : obj[key]));

이렇게 했을 경우 우리는 아래처럼 _get을 활용할 수 있다.

_get(user[0], 'name');
// 또는
_get('name')(user[0]);

이 말은 즉,

const get_name = _get('name');

이렇게 할 경우에 get_name은 어떤 객체가 주어지기만 한다면 name을 가져오는 함수가 된다. 즉 currying을 통해 재사용성이 증가하게 되었고 그로인해 생긴 편리한 함수인 것이다. 이 get_name은 object를 인자로 받으면 되니까 map이나 각종 루프에서 활용하면 코드가 훨씬 깔끔해질 것이다. 바로 아래처럼. (_get함수에 인자 하나가 (‘name’)으로 들어가 실행되면 곧 get_name함수이다.)

// _get함수가 없으면 사용할 방법
map(users, (user) => (user.name))

// _get 함수 사용시 깔끔해진 함수
map(
  users, _get('name')
)

참고자료

Comments