JavaScript

Currying in Javascript

La.place 2017. 7. 23. 16:38



 지난 포스트(Functional Programming in Javascript)에서 함수형 패러다임에 대해 간략히 소개했었는데, 이번에는 그 중 Currying에 대해서 좀 더 자세히 알아보도록 하자. 함수형 프로그래밍이 뭔지 궁금한 사람은 이전 포스트를 참고하길 바란다.

Currying ?

 우선 커링에 대한 정의에 대해 알아보도록 하자.
수학과 컴퓨터 과학에서 커링이란 다중 인수 (혹은 여러 인수의 튜플)을 갖는 함수를 단일 인수를 갖는 함수들의 함수열로 바꾸는 것을 말한다. 모지즈 쇤핑클에 의해 도입되었고, 이후 하스켈 커리에 의해 발전하였다.

 무슨말인지 잘 모르겠다. 쉽게 풀어 쓰면 이렇다. 여러개의 인자(parameter)를 갖는 함수를 단일 인자를 갖는 함수들의 연결로 바꾸는 것을 의미한다. 한가지 예를 들어보자. 두 수를 더하는 함수를 작성해 보도록 하자.

basic.01.js

let sum = function (x, y) {
return x + y;
};

console.log(sum(5, 7)); // 12

 함수란, 첫 번째 집합의 임의의 한 원소를 두 번째 집합의 오직 한 원소에 대응시키는 대응 관계를 말한다. sum 함수는 두 수를 인자로 받아서 합을 출력하는 역할을 한다. 이제 이 함수를 앞에서 언급한 정의에 따라 커링을 해보도록 하자. 정의에 따라 우리가 하고자 하는 일을 정리하면 x, y(여러개의 인자)를 단일 인자 x와 y를 가지는 함수로 나누면 된다.

let sum = function (x) {
return function (y) {
return x+y;
}
};
console.log(sum(5)(7)); // 12

 자 이제 앞에서 알아본 커링의 정의대로 단일 인자의 함수의 연결로 바꾸었다. 이 과정에서 클로저(Closure), 1급 객체 함수(First-class Citizen Function), 익명 함수(Anonymous Function)이 쓰인 것을 볼 수 있는데 이 문법들에 대한 히스토리는 다음 포스팅을 참고하도록 하자. 람다, 익명함수, 클로저.

 위 표현식은 ES2015에 추가된 arrow function을 이용하면 좀 더 간결하고 쉽게 표현이 가능하다. 또한 다음과 같이 변수에 담아서 사용할 수도 있다.

let sum = x => y => x+y;

let sum5 = sum(5);
let sum12 = sum5(7);

console.log(sum12, sum(5)(7)); // 12 12

 그럼 도대체 커링은 왜 해야하는것이며, 함수의 인자를 나눠서 얻는 이득이 무엇일까? 위 예제를 보고 눈치 챘을 수도 있겠지만 가장 큰 장점은 역시나 재사용성이다. 함수를 만드는 이유는 여러가지가 있겠지만 가장 큰 이유는 다시 사용하기 위해서이다. 재사용할수록 생산성도 좋아지고 나중에 유지보수할때도 고쳐야할 부분이 적어지니 여러모로 좋은 코드를 만들 수 있다. 말로 들으면 이해가 잘 안될테니 예제를 들어보도록 하자.

Currying Example

소속과 이름을 입력받아 출력하는 함수가 있다. 일반적인 함수로 표현하자면 다음과 같다.

example.01.js
let printInfo = function(group, name){
console.log(group + ', ' + name);
};

printInfo('dev-momo', 'haegul'); // dev-momo, haegul

 만약 소속이 같은 여러사람이 존재한다고 하면 이 사람들을 모두 출력할 때 위 함수로는 이런식으로 사용한다.

// 출력
printInfo('dev-momo', 'haegul'); // dev-momo, haegul
printInfo('dev-momo', 'jiwon'); // dev-momo, jiwon
printInfo('dev-momo', 'sungcheon'); // dev-momo, sungcheon

 이제 커링을 사용해서 재사용성을 높혀보도록 하자.

// currying
let printInfo = group => name => console.log(group + ', ' + name);

let momoGroup = printInfo('dev-momo');
momoGroup('haegul'); // dev-momo, haegul
momoGroup('jiwon'); // dev-momo, jiwon
momoGroup('sungcheon'); // dev-momo, sungcheon

 첫번째 인자에서 공통인 그룹 데이터를 받은 후 클로저로 저장해둔다. 이제 momGroup은 이름만 전달받아 'dev-momo'와 함께 출력해주는 새로운 함수가 되었다. 마찬가지로 다른 그룹네이밍을 가진 사람이름을 출력하려면, printInfo(group)으로 새로운 그룹을 만들어 사용하면 된다. 왠지 이것만 보아선 재사용성이 좋아진것 같지 않은 느낌이다. 그렇다면 다음 예제를 보도록 하자. 다음 예제는 A Beginner’s Guide to Currying in Functional JavaScript에서 가져와 보았다.

let greetDeeplyCurried = 
greeting => separator => emphasis => name =>
console.log(greeting + separator + name + emphasis);

let greetAwkwardly = greetDeeplyCurried("Hello")("...")("?");
greetAwkwardly("momo"); // Hello...momo?

let sayHello = greetDeeplyCurried("Hello")(", ");
sayHello(".")("momo"); // Hello, momo.

let askHello = sayHello("?");
askHello("momo"); // Hello, momo?

 curry function 하나가지고 정말 다양한 함수를 정의해서 사용할 수 있다. 또 부분적으로 정의한 함수를 다시 정의해서 사용하는 패턴도 보인다. 사용하기에 따라선 중복을 크게 줄일 수 있다.

Tranditional

 자바스크립트에서는 기본적으로 커리함수를 지원하지 않는다. 위처럼 ES6 문법을 사용하면 쉽게 표현이 가능하지만, 전통적인 방법으로는 어떻게 할까? 위 포스팅에도 답이 있지만 다음과 같이 사용할 수 있다. call, apply를 이용해서 클로저를 만든다. 더 자세한 설명은 Javascript Currying에도 나와있으니 참고하도록 하자.

example.04.js

var curry = function(uncurried) {
var parameters = Array.prototype.slice.call(arguments, 1);
return function() {
return uncurried.apply(this, parameters.concat(
Array.prototype.slice.call(arguments, 0)
));
};
};

Usage

 그럼 실제로는 currying이 어떻게 사용되고 있고, 또 어떻게 사용할 수 있을까? 몇가지 예제를 알아보도록 하자.

Function.prototype.bind

  javascript native method인 bind를 사용하면 위에서 사용한 커리의 특징인 cache와 re-usable을 사용할 수 있다. . 아래 예제를 보도록 하자. 해당 예제에 대한 자세한 설명은 Currying in JavaScript using bind 에서 다루고 있다.

function add(x, y) {
return x+y;
}

let increment = add.bind(undefined, 1);
console.log(increment(4) === 5);

Redux

 react-redux에서 사용하는 connect() 함수가 pure curry function으로 구현되어 있다.

// connect
export default connect()(TodoApp)

// connect with action creators
import * as actionCreators from './actionCreators'
export default connect(null, actionCreators)(TodoApp)

// connect with state
export default connect(state => state)(TodoApp)

Vuex

 Vuex에서 사용하는 getters에서 parameter를 넘겨받을때 curry를 이용한다. 

getters: {
// ...
getTodoById: (state, getters) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }

Event Handler

 event handler에서 이런식으로 커링을 사용할 수도 있다.

const handleChange = (fieldName) => (event) => {
saveField(fieldName, event.target.value)
}
<input type="text" onChange={handleChange('email')} ... />

Rendering HTML

 Rendering 함수를 만들때 다음과 같이 재사용할수도 있다.

renderHtmlTag = tagName => content => `<${tagName}>${content}</${tagName}>`

renderDiv = renderHtmlTag('div')
renderH1 = renderHtmlTag('h1')

console.log(
renderDiv('this is a really cool div'),
renderH1('and this is an even cooler h1')
)

마치며

 curry에 대해서 간단하게(?) 알아보았다. 실제 함수형 패러다임을 접해보지 않은 사람이 개발 할 때 적용하려면 익숙하지가 않다. 그러나 개인적인 의견은 적절하게 프로젝트에 녹여낸다면 더욱 가독성과 유연성, 생산성 높은 코드를 만들 수 있을 것이라 생각한다. 이번 포스팅은 javascript curry에 대한 다양한 포스트를 참고하면서 작성하였는데 관련된 포스트는 아래에 따로 정리해 두도록 하겠다. 관심이 있는 사람들은 읽어보면 도움이 많이 될 것이라 생각한다. 다른 포스팅에서는 함수형 프로그래밍에 대한 철학과 특징에 대해서 정리해 보도록 하자.


Reference