2019. 9. 22. 19:16ㆍProgramming/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
, _each
는 Array
라는 객체의 메서드이다. 즉, Array가 아니면 사용할 수 없다. 그러나 Javascript에서는 배열처럼 생겼지만 배열이 아닌 객체들이 존재하며, 이러한 객체들에는 기본으로 제공되는 _map
, _filter
, _each
와 같은 메서드를 사용할 수 없다. 이러한 Arraylike의 대표적인 예는, document.querySelectAll
을 이용하여 반환되는 값이 있다. (document.querySelectAll
의 결과값은 NodeList
이다.)
document.querySelectAll.map()
을 호출하게 되면 에러가 발생하지만, 위에서 작성한 _map
에 document.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
를 이용해서 작성된 함수 f1
에 1
을 인자로 넘겨주면, 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
값이 그대로 반환되게 된다. _each
에 null
값이 넘어가더라도 오류가 발생하지 않도록 수정했으므로, 내부적으로 _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가 된다.
'Programming > JavaScript' 카테고리의 다른 글
[JavaScript] Object Literal Property Value Shorthand (0) | 2021.12.08 |
---|---|
[Javascript] 배열 초기화 시 new Array()보다는 []를 사용하자 (0) | 2019.11.28 |
자바스크립트로 알아보는 함수형 프로그래밍 정리노트 #3 (0) | 2019.09.15 |
자바스크립트로 알아보는 함수형 프로그래밍 정리노트 #2 (0) | 2019.09.15 |
자바스크립트로 알아보는 함수형 프로그래밍 정리노트 #1 (0) | 2019.09.15 |