const _ = {};
const MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
const isArrayLike = function(data){
let length = data == null ? 0 : data.length;
return typeof length == 'number' && length >= 0 && lenth <= MAX_ARRAY_INDEX;
}
_.identity = function(v){return v;}
_.array = function(){ return []; }
_.push = function(obj, val){
obj.push(val);
return obj;
}
_.push_to = function(val, obj){
obj.push(val);
return val;
}
_.map = bloop( _.array, _.push_to);
_.keys = function(obj){
const type = typeof obj;
return type == 'function' || type == 'object' && !!obj ?
Object.keys(obj) : [];
}
_.values = function(data){
return _.map(data, _.identity);
}
_.toArray = function(list){
return Array.isArray(list) ? list : _.values(list);
}
_.rest = function(list, num){
return toArray(list).slice(num || 1);
}
_.rester = function(func, num){
return function(){
return func.apply(null, _.rest(arguments, num));
}
}
function bloop(new_data, body){
return function(data, iter_predi){
const result = new_data(data);
if( isArrayLike(data) ){
for(let i = 0, len = data.length; i < len ; i++{
body( iter_predi(data[i], i, data), result, data[i] );
}
} else{
for(let i = 0, keys = _.keys(data), len = keys.length; i < len; i++){
body( iter_predi(data[keys[i]], keys[i], data), result, data[keys[i]] );
}
}
return result;
}
}
_.if = function(validator, func, alter){
return function(){
return validator.apply(null, arguments) ?
func.apply(null, arguments) :
alter && alter.apply(null, arguments);
}
}
_.filter = bloop( _.array, _.if(_.identity, _.rester(_.push )) )
Underscore.js를 책의 예시대로 작성해봤다. 예시 하나하나 따라 작성하고 있을 때는 많아보이지 않았는데, 공부하는 겸 옮겨 적어보니 지금에 와서는 많은 함수들이 구현이 되었다는 것을 느꼈다. 사실 위에 작성되어있는 코드 이외에도 작성된 것은 많다. 이번 글에서 다뤄볼 if문과 관련한 함수들만 모으기 위해서 예시들을 간추리거나 어떤 함수들은 하나의 함수로 옮겨적었다.
_.if = function(validator, func, alter){
return function(){
return validator.apply(null, arguments) ?
func.apply(null, arguments) :
alter && alter.apply(null, arguments);
}
}
함수형 프로그래밍을 배워갈수록 느끼는게 있다. “이렇게까지 함수로 구현한다고?”라는 의문이다. 특히 이번 if 함수가 그랬다. 책에서는 이렇게 함수형 프로그래밍을 소개한다.
함수를 보다 적극적으로 사용하고, 함수를 추상화의 단위로 사용하고, 상태 변경을 최소화하고, 로직을 함수로 고르고, 기본 객체를 많이 사용하며 함수의 응용을 중시하는 프로그래밍이 함수형 프로그래밍이다.
if문은 “로직을 함수로 고르고”에 해당할까.
프로그래밍을 해오는 동안 나는 줄곧 if문 사용을 이렇게 생각해왔다.
만약 ~ 라면 ~ 해라.
하지만 함수형 프로그래밍에 와서는 함수로 구현하는 if가 다음과 같이 생각되어지는 것 같다.
검증할건데, 판별은 ~하고, 옳을 시 ~하고, 아니면 ~해라.
_.filter = bloop( _.array, _.if(_.identity, _.rester(_.push )) )
if 함수를 응용해서 filter 함수를 만드는 예제코드다. 일부러 익명함수를 쓰는 일도 없이, 모두 미리 만들어진 함수를 조합해서 사용하고 있다. 이렇게 보면 몇몇을 제외하면 함수들의 이름만 보고도 해당 코드가 어떻게 작동할지 짐작할 수 있다. 이 부분은 장점이라고 생각한다.
그리고 filter 함수는 다음과 같이 사용하기만 하면된다.
let example1 = _.filter([1,2,3,4], function(v){
return v > 2;
})
console.log(example1);
// [3, 4]
대부분의 개발에서는 filter 함수가 어떻게 구현되었는지 보다는 _.filter를 어떻게 사용할지에 대해서 더 고민을 하게 될 것이다. 이 시점에서의 filter 함수는 사실 바로 위 예제에서 어떻게 구현되었는지에 대해서는 전혀 관심없이 filter의 기능만 이용하고 있다. 관심사를 분리시킨다는 것도 장점이다.
공부하다가 고민이 된 부분은, 함수형 프로그래밍이 어느 시점을 두고 적용하고 활용되었는지를 판별하는 부분이다. 다 완성된 filter의 사용 부분에서 함수형 프로그래밍을 논하고 있는 것일까, 아니면 _.filter 여러 함수를 조립해서 만드는 과정을 함수형 프로그래밍으로 얘기하는 것일까. “왜?”를 어디서 찾아야 하는지 더 지켜보도록 하겠다.
함수가 겹겹이 쌓여있고, 이곳저곳에서 호출이 발생하고 있는 것 같으면, ()
에 집중하면 좋다. 함수형 프로그래밍에서는 클로저를 적극적으로 사용하면서 함수를 리턴하는 함수, 함수를 인자로 받는 함수의 사용이 많다.
그러다보니 이번 if 함수가 이해하는데 어려움이 있었다. 하지만 바로 위에서 소개한 ()
의 사용에 집중해서 분석을 하니 도움이 되었다.
헷갈린 지점은 다음과 같다.
function bloop(new_data, body){
return function(data, iter_predi){
const result = new_data(data);
if( isArrayLike(data) ){
for(let i = 0, len = data.length; i < len ; i++{
body( iter_predi(data[i], i, data), result, data[i] );
}
} else{
for(let i = 0, keys = _.keys(data), len = keys.length; i < len; i++){
body( iter_predi(data[keys[i]], keys[i], data), result, data[keys[i]] );
}
}
return result;
}
}
_.if = function(validator, func, alter){
return function(){
return validator.apply(null, arguments) ?
func.apply(null, arguments) :
alter && alter.apply(null, arguments);
}
}
_.identity = v=>v;
_.filter = bloop( _.array, _.if(_.identity, _.rester(_.push )) )
_.filter([1,2,3,4], v => v > 2);
// [3, 4]
filter 함수를 만들기 위해서 bloop 함수를 사용하고 있고, bloop의 실행을 위해 여러 함수들을 인자로 넘겨주고 있는 것을 확인할 수 있다. bloop의 body 보조 함수로 넘겨질 함수는 if 함수이다. bloop 내부에서는 body를 for문 안에서 실행시키는 로직을 확인할 수 있다. 그렇다면 if 함수는 bloop 내부에서 실행되는 것일까
라는 지점이 헷갈렸던 지점이다. 특히 identity라는 자기 자신의 값을 돌려주는 함수를 validator라는 판별 함수로 사용하고 있는데, 왜 굳이 필요없는 것 같은 함수를 인자로 넘기고 있는지 의아했다.
잘 확인해보면 사실 if함수는 filter 함수 인자로 넘겨지면서 먼저 실행되고 있는 것을 확인할 수 있다. 이 때문에 결과적으로 if는 실행되어 사라지고 if 함수가 리턴하는 함수가 bloop함수의 인자로 넘겨져 body로 쓰이게 된다.
언뜻보면 그래서 iter_predi라는 bloop 함수의 내부 리턴함수가 최종적으로 실행하는 함수와 validator라는 if 함수 내부의 리턴함수가 실행하는 함수가 충돌되고 있는 것처럼 보인다.
하지만 iter_predi도 body 함수 내부에서 실행되고 있다는 점을 주목해야 한다. 필자는 이 지점을 놓쳐서 이해하는데 시간이 꽤 걸렸다. iter_predi는 filter 함수에서는 predicate
의 성격으로 쓰이기 때문에 참거짓 값을 돌려줄 것이라는 것을 짐작할 수 있다. body로 실행되는 if 함수의 내부 리턴함수는 해당 참거짓 값을 그대로 identity 함수로 사용을 하며 판별 후 알맞은 함수 실행을 하는 로직을 수행한다.
Rester 함수는 지정한 갯수만큼 앞에서부터 제외한 남은 값들에 대한 로직을 다룰 때 사용하는 함수다. 이 때문에 사실 filter 함수는 bloop을 통해서 루프를 돌며 조건에 맞는 값만 배열에 추가해서 반환을 하면 된다고 생각했다. 결국 Rester 같이 남은 값들에 대한 로직을 실행하는 것이 아닌 해당 루프에서 해당되는 값만 추가하면 된다고 생각했다. 이를테면 다음과 같이 filter 함수가 작성하기만 하면 되는 것이다.
_.filter = bloop( _.array, _.if(_.identity, _.push) )
근데 결국 이것도 함수가 작동하는 방식을 잘못 이해하고 있었던 거였다.
if 함수가 리턴하는 함수에서 실행되는 validator, func, alter 함수들은 apply를 통해서 실행이 된다. 그리고 apply에 arguments로 넘겨지는 값은 bloop에서 body가 실행이될 때 넘겨지는 [참거짓값, 새로 생성된 배열, 해당 루프의 배열값]이다. 이 때 넘겨지는 arguments를 참거짓값을 제외하고 나머지 값들만 전달되게 하는 것이 Rester 함수의 역할이었다.
함수형 프로그래밍은 말그대로 함수를 잘 다루는 프로그래밍 관점이다. 이 때문에 함수에 대한 이해가 필수적이다. 클로저 함수를 잘 이해하고 있다고 생각하면서도, 함수가 리턴되는 함수를 이해하고, 또 직접 적용해서 사용하는 것은 또 어려운 부분인 것 같다. 계속 공부해가면서 감을 익혀가야 할 것 같다.
- 유인동, ⌜함수형 자바스크립트 프로그래밍⌟, 프로그래밍 인사이트