자바스크립트로 알아보는 함수형 프로그래밍 정리노트#4, 함수형으로 전환하기

2019. 9. 22. 19:16Programming/JavaScript

반응형

응용형 함수

함수를 인자로 전달받아서, 원하는 시점에 호출하는 함수를 응용형 함수라고 하며,
응용형 함수를 이용해서 코드를 작성하는 방식을 적응형 프로그래밍이라고 한다.

var users = [
    {name: a, age: 23},
    {name: b, age: 31},
    {name: c, age: 24},
    {name: d, age: 32},
    {name: e, age: 25}
]

위와 같은 배열이 있을 때, 아래와 같은 응용형 함수들을 작성할 수 있다.

_filter

배열의 요소 중 특정 조건을 만족하는 요소만 반환하는 함수 _filter를 작성해보자.

function _filter(array, predict) {
    var filteredArray = [],
        index;

    for(index=0; index<array.length; index++) {
        if(predict(array[index])) {
            filteredArray.push(array[index]);
        }
    }

    return filteredArray;
}

함수 _filter는 배열 array와 함수 predict를 전달받아서, predict를 만족하는 array의 요소만을 반환하게 된다.
이 함수는 아래와 같이 사용할 수 있다.

_filter(users, function(user){return user.age >= 30;}); //30세 이상의 사용자만 반환한다.
_filter(users, function(user){return user.age < 30;}); //30세 미만의 사용자만 반환한다.
_filter([1, 2, 3, 4], function(num){return num%2;}); //짝수만 반환한다.
_filter([1, 2, 3, 4], function(num){return !(num%2);}); //홀수만 반환한다.

_map

배열의 요소에 동일한 계산결과들을 반환하는 함수 _map을 작성해보자.

function _map(array, mapper) {
  var mapperArray = [],
      index;

  for(index=0; index<mapperArray.length; index++) {
      mapperArray.push(mapper(array[i]));
  }

  return mapperArray;
}

함수 _map는 배열 array와 함수 mapper를 전달받아서, array의 각 요소에 대해 mapper를 실행한 결과를 반환하게 된다.
이 함수는 아래와 같이 사용할 수 있다.

_mapper(users, function(user){return user.name};); //사용자의 이름만 반환한다.
_mapper(users, function(user){return user.age};); //사용자의 나이만 반환한다.
_mapper([1, 2, 3, 4], function(num){return num*2;}); //배열의 요소에 2를 곱한 [2, 4, 6, 8]을 반환한다.

_each

위에서 작성한 _filter_map을 보면, 각 배열의 루프를 도는 코드가 반복되는 것을 알 수 있다.
이러한 반복코드를 제거하기 위해서, 각 배열을 순회하면서 동일한 동작을 처리해주는 _each를 작성해보자.

function _each(array, iteratee) {
    var eachArray = [],
        index;

    for(index=0; index<array; index++) {
        iteratee(array[i]);
    }

    return eachArray;
}

다음과 같이 위에서 작성한 _each를 이용하여 _map_filter의 중복코드를 제거할 수 있다.

function _filter(array, predict) {
    var filteredArray = _each(array, function(elem) {
        if(predict(elem)) {
            filteredArray.push(elem)
        }
    });
}

function _map(array, mapper) {
    var mapperArray = _each(array, function(elem) {
        mapperArray.push(mapper(elem));
    });
    return mapperArray;
}

다형성

위에서 작성한 _map, _filter, _each는 es6에서 Javascript 기본 함수로 제공되는 함수들이다. 그런데 왜 굳이 다시 만들었을까?
Javascript에서 기본으로 제공되는 _map, _filter, _eachArray라는 객체의 메서드이다. 즉, Array가 아니면 사용할 수 없다. 그러나 Javascript에서는 배열처럼 생겼지만 배열이 아닌 객체들이 존재하며, 이러한 객체들에는 기본으로 제공되는 _map, _filter, _each와 같은 메서드를 사용할 수 없다. 이러한 Arraylike의 대표적인 예는, document.querySelectAll을 이용하여 반환되는 값이 있다. (document.querySelectAll의 결과값은 NodeList이다.)

document.querySelectAll.map()을 호출하게 되면 에러가 발생하지만, 위에서 작성한 _mapdocument.querySelectAll을 넘겨주게되면 제대로 실행되는 것을 알 수 있다. 이를 통해 위와같이 응용형 함수를 사용하게 되면, 다형성이 높아진다.

document.querySelectAll.map(function(node){return node.nodeName;}); //에러가 발생한다.
_map(document.querySelectAll, function(node){return node.nodeName;}); //에러가 발생하지 않는다.

또한 객체지향에서는 객체를 생성한 이후에야 해당 메서드를 호출할 수 있다는 점을 알 수 있다. 하지만 함수형 프로그래밍에서는 함수에 데이터만 전달하면 되기 때문에, 좀 더 유연한 평가시간을 갖는다고 할 수 있다. 이를 통해서 더 높은 조합성을 가지게 된다.

#_curry, _curryR
인자가 모일때까지 평가를 미루고, 원하는만큼 인자가 모였을 때 최종적으로 평가하는 함수 _curry를 구현해보자. javascript에서는 기본적으로 curry를 제공하지 않지만, 함수가 일급객체이기 때무넹 _curry와 같은 기법을 어렵지 않게 구현할 수 있다.

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

위에서 작성한 _curry를 사용해서, 두 개의 변수를 더해주는 _add라는 함수를 만들어보도록 하자.

var _add = _curry(function(a, b) {
    return a+b;
});

var _add10 = _add(10);
console.log(_add10(5)); //15를 반환한다.
console.log(_add(10)(5)); //15를 반환한다.

위에서 _curry를 이용해서 작성한 _add에 인자를 1개만 전달했을 때, _add10에 할당된 것처럼 함수가 반환되는 것을 볼 수 있다. 따라서 _add(10, 5)처럼 두 개의 인자를 전달했을 때는 올바르게 동작하지 않는다. 인자가 두 개 전달됐을 때는 즉시 함수의 실행결과를 리턴하도록 보완해보도록 하자.

function _curry(fn) {
    return function(a, b) {
        return (arguments.length == 2) ? fn(a, b) //인자가 두 개이면 즉시 fn(a, b)를 호출한다.
               :function(b) {return fn(a, b);}
    }
}

위에서 작성한 _curry의 경우에는 항상 왼쪽의 인자부터 오른쪽의 인자 순서로 실행하게 된다. 아래와 같이 두 개의 인자가 주어졌을 때, 뺄셈을 하는 함수 _sub를 작성해보도록 하자.

var _sub = _curry(a, b) {
    return a-b;
}

var _sub10 = _sub(10);
console.log(_sub(10, 5)); //10-5를 반환한다.
console.log(_sub10(5)); //10-5를 반환한다.

sub의 경우에는 자연스럽게 10-5이 계산된다는 것을 예측할 수 있다. 반면 _sub10의 경우에는 함수의 이름으로 인해, 인자로 전달된 값에서 10을 뺀 결과를 리턴하는 것처럼 보여서 부자연스럽다. _curry가 인자의 좌측부터 계산하기 때문에 발생하는 상황이다. 그렇다면 오른쪽 인자부터 계산하는 함수를 작성하면 어떨까. 아래와 같이 _curryR을 작성해보도록 하자.

function _curryR(fn) {
    return function(a, b) {
        return (arguments.length == 2) ? fn(a, b) //인자가 두 개이면 즉시 fn(a, b)를 호출한다.
               :function(b) {return fn(b, a);} //_curry와는 반대로 오른쪽 인자부터 평가한다.
    }
}

var _sub = _curry(a, b) {
    return a-b;
}

var _sub10 = _sub(10);
console.log(_sub(10, 5)); //10-5를 반환한다.
console.log(_sub10(5)); //인자로 전달된 5에서, 10을 빼준다.

_get

배열이나 객체의 요소를 읽어오는 _get 함수를 작성해보도록 하자.

function _get(obj, key) {
    return obj == null ? undefined : obj[key];
}

console.log(users[0], 'name'); //users의 첫번째 인자가 가지고 있는 name값을 출력한다.

obj 내에 key에 해당하는 값이 없을 경우, undefined를 반환하도록 하여 오류가 발생하는 상황을 회피할 수 있다. _get함수에 _curryR를 사용하면 다음과 같이 사용하는 것이 가능하다.

var _get = _curryR(function(obj, key) {
  return obj == null ? undefined: obj[key];
});

console.log(_get('name')(users[0])); //users의 첫번째 인자가 가지고 있는 name값을 출력한다.

curryR을 적용하고나니 _get('name')이 객체에서 name인자를 가져오는 함수가 됐다. 동일한 방법으로 _get('age')를 사용하면 나이를 가져오는 함수가 된다는 것을 알 수 있다.

여기서 작성한 _get을 이용해서 _map_filter를 사용하면, 좀 더 코드를 간결하게 만들 수 있다.

console.log(
    _map(
        _filter(
            users, function(user) {return user.age>=30;}
        ),
        _get('name')
    );
);

위에서 작성한 코드를 실행시키면, age가 30이상인 요소들의 name을 출력하게 된다.

#_reduce
초기값이 주어졌을 때, 초기값에 대해 각 배열의 요소에 대한 연산을 수행한 결과를 리턴한 함수 _reduce를 작성해보도록 하자.

function _reduce(list, iter, memo) {
    _each(list, function(val) {
        memo = iter(memo, val);
    });

    return memo;
}

console.log(_reduce([1, 2, 3], _add, 0)); // 0+1+2+3의 결과를 반환한다.
console.log(_reduce([1, 2, 3], _add, 10)); // 10+1+2+3의 결과를 반환한다.

위에서 작성한 _reduce에는 세 번째 인자가 반드시 필요하다. 하지만 초기값이 주어지지 않았을 경우에도 배열의 요소만을 이용해서 _reduce를 사용하는 경우가 있을 수 있다. 세 번째 인자가 주어지지 않았을 경우에 대해, _reduce를 보완해보도록 하자.

function _reduce(list, iter, memo) {
    if(arguments.length == 2) {
        memo = list[0];
        list = list.slice(1);
    }

    _each(list, function(val) {
        memo = iter(memo, val);
    });

    return memo;
}

console.log(_reduce([1, 2, 3], _add)); // 1+2+3의 결과를 반환한다.

위와 같은 방법을 사용하면 세 번째 인자가 주어지지 않았을 때도 _reduce를 사용하여 연산이 가능하다. 하지만 Array객체의 메서드인 slice를 사용하고 있기 때문에, _map, _fliter, _each가 배열이 아닌 객체에도 사용할 수 있었던 것을 생각해보면 다형성이 떨어진다는 것을 알 수 있다. 이러한 내용을 보완하기 위해, 아래와같이 _rest함수를 적용한 후 _reduce함수를 보완해보자.

function _rest(list, num) {
    return Array.prototype.slice.call(list, num || 1); //num이 주어지지 않았을 때는 1을 넘겨준다.
}

function _reduce(list, iter, memo) {
    if(arguments.length == 2) {
        memo = iter[0];
        list = _rest(list);
    }

    _each(list, function(val) {
        memo = iter(memo, val)
    });

    return memo;
}

_pipe

_pipe는 함수들을 인자로 받아서, 연속적으로 함수들을 실행하는 함수를 만들어주는 함수이다.

function _pipe() {
    var fns = arguments;
    return function(arg) {
        return _reduce(fns, function(arg, fn) {
            return fn(arg);
        }, arg);
    }
}

var f1 = _pipe(
    function(a) {return a+1;},
    function(a) {return a*2;}.
    function(a) {return a*a;}
);
console.log(f1(1));

_pipe로 전달받은 함수들은 fns값에 저장되에 _reduce로 전달되며, _reduce를 통해 전달된 모든 함수들이 실행하게 된다. 이 때 각 함수의 실행 결과는 arg에 저장되게 되며, arg값은 최종적으로 _pipe의 실행결과로써 반환되게 된다.

_pipe를 이용해서 작성된 함수 f11을 인자로 넘겨주면, a+1, a*2, a*a가 순차적으로 실행되며 최종적으로 16이라는 값이 반환된다.

_go

_go_pipe와 비슷하나, 즉시 실행된다는 점이 다르다. 위의 예시에서 _pipe를 통해 만들어진 값 f1은 함수였다. 하지만 _go의 경우에는 넘겨준 함수들의 계산 결과값을 반환하게 된다는 점이 다르다.

function _go() {
    var fns = _rest(arguments);

    return _pipe.apply(null, fns)(arg);
}

_go(1, 
    function(a) {return a+1;},
    function(a) {return a*2;}.
    function(a) {return a*a;},
    console.log
)

위의 실행결과는 _pipe의 예제와 동일하다.

_map_filter를 실행하는 코드에 _go를 적용하면 좀 더 간결하게 표현하는 것이 가능하다.

_go(users,
    function(uesrs) {
        return _filter(users, function(user) {
            return user.age >= 30;
        });
    },
    function(users) {
        return _map(users, _get('name'));
    },
    console.log);

위의 실행결과는 users의 요소 중 age값이 30이상인 요소들의 이름을 출력하게 된다.

_map과 _filter에 _curryR을 적용하여, 코드를 좀 더 간결하게 표현해보자.

아래와 같이 선언하게 되면, 이전에 작성했던 _map_filter_curryR이 적용된다.

var _map = _curryR(_map),
    _filter = _curryR(_filter);

위와 같이 _curryR이 적용되면, 기존의 _map_filter를 사용해서 age값이 30 이상인 요소들의 이름을 출력하는 코드를 간결하게 표현해보자.

_go(users,
    _filter(function(user) {return user.age >= 30;}),
    _map(_get('name')),
    console.log);

users를 참조하여 _filter가 실행되고, 실행된 결과는 _map으로 전달된다. 마지막으로 console.log가 실행되면서 결과가 출력되기 때문에, 최종적으로는 users의 요소 중 age가 30이상인 요소들의 name값을 출력하게 된다.

다형성 높이기

_each에 null처리를 추가하여 다형성을 높혀보자.

function _each(list, iter) {
    var length = _get('length')(list), //_get이 null에 대한 처리를 해준다.
        index;

    for(index=0; index<length; index++) {
        iter(list[index]);
    }

    return list;
}

length값이 undefined가 되면 index<undefined값의 평가가 false이므로, 동작이 처리되지 않고 list값이 그대로 반환되게 된다. _eachnull값이 넘어가더라도 오류가 발생하지 않도록 수정했으므로, 내부적으로 _each를 사용하고 있는 _map_filter도 마찬가지로 null값을 넘겨주더라도 오류가 발생하지 않게 된다.

_keys를 통해 Object.keys의 다형성을 높여보자.

_function _keys(obj) {
    return (typeof obj == 'object' && !!obj) ? return Object.keys(obj) : [];
}

Object.keys에 객체가 아닌 값이 전달되더라도 문제가 없도록 처리하여 다형성을 높힐 수 있다. 위에서 작성한 _keys를 사용하여, _each가 객체에서도 동작할 수 있도록 수정해보자.

function _each(list, iter) {
    var keys = _keys(list),
        index = 0;

    for(index=0; index<keys.length; index++) {
        iter(list[index]);
    }

    return list;
}

_keys는 객체가 아닐 경우에 빈 배열을 반환하도록 했기 때문에, _get('length')를 사용하지 않더라도 null에 대한 에러가 발생하지 않는다. 또한 내부적으로 _each를 사용하고 있던 _map_filter의 경우에도, 객체에 대해서 사용할 수 있는 함수가 된다. _go_map을 사용하여, 객체의 각 요소가 가지고 있는 값을 소문자로 변환하여 출력하는 코드를 작성해보자.

_go({
        13: 'ID',
        19: 'HD',
        29: 'YD'
    },
    _map(function(name) {
        return name.toLowerCase();
    }),
    console.log);

위의 실행결과는 id, hd, yd가 된다.

반응형