지난 포스트(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
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
Function.prototype.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')
)
마치며
Reference
- A Beginner’s Guide to Currying in Functional JavaScript
- Currying in ES6
- Javascript Currying과 예제
- React-Redux
- Vuex
- 람다, 익명함수, 클로저
'JavaScript' 카테고리의 다른 글
알아두면 쓸만한 Chrome Console 기능 (0) | 2017.12.31 |
---|---|
React v16 무엇이 바뀌었나 (1) | 2017.10.08 |
Functional Programming in Javascript (8) | 2017.07.22 |
TDD & Mocha - Javascript Test Framework (0) | 2017.05.14 |
Javascript Image Filter - convolution (0) | 2017.05.02 |