함수형 프로그래밍
사실 함수형 프로그래밍은 정말… 설명하기가 힘들다. (아직도 힘들다.) 하지만 자바스크립트에서 함수형 프로그래밍이 어떤 식으로 사용되는 지 간단하게 나마 남겨보고자 한다.
함수형 프로그래밍이 뭔데?
그러게 말이다… 단순히 말하자면 함수를 조합하여 작업을 수행한다.라고 말하고 싶다. 말 그대로다 프로그램을 만들기 위해 프로그래밍을 한다면, 함수형 프로그래밍은 함수를 이용해서 프로그래밍을 하겠다는 이야기니 말이다. 중요한 점은 작업 동안에 데이터나 상태의 변화 없이 오로지 함수만 변화하고 연산의 대상이 된다는 점이다.
함수에는 외부에 아무런 영향을 끼치지 않는 순수함수와 함수를 하나의 값으로 인식하여 함수의 인자나 반환값으로 사용하는 고차함수가 있다.
자바스크립트에서의 함수형 프로그래밍
자바스크립트의 함수는 일급 객체이면서 클로저를 활용할 수 있다.
암호화 예제를 자바스크립트로 표현해 보았다.
var f1 = function(input) {
var result;
result = 1;
return result;
}
var f2 = function(input) {
var result;
result = 2;
return result;
}
var f3 = function(input) {
var result;
result = 3;
return result;
}
var get_encrypted = function(func) {
var str = 'Jang';
return function() {
return func.call(null, str);
}
}
var encrypted_value = get_encrypted(f1)();
console.log(encrypted_value);
var encrypted_value = get_encrypted(f2)();
console.log(encrypted_value);
var encrypted_value = get_encrypted(f3)();
console.log(encrypted_value);
출력결과는 1,2,3 이다. 변수 str은 외부에서 접근이 불가능하다(영향을 끼칠 수 없다.). 또한 함수는 일급 객체이기 때문에 함수의 인자로 함수를 넘기고 결과로 함수를 반환할 수도 있다.
함수형 프로그래밍 예제
- 시그마
배열의 각 원소의 합을 구하는 알고리즘을 구현해보자. (코드를 보지 않고 직접 해보길 추천한다.)
function sum(arr) {
var len = arr.length;
var i = 0, sum = 0;
for (; i < len ; i++) {
sum += arr[i];
}
return sum;
}
var arr = [1,2,3,4];
console.log(sum(arr)); // 10
이를 응용하여 곱셈값을 구하면,
function multiply(arr) {
var len = arr.length;
var i = 0, result = 1;
for(; i < len; i++) {
result *= arr[i];
}
return result;
}
var arr = [1,2,3,4];
console.log(multiply(arr)); // 24
이를 함수형 프로그래밍을 이용해보자.
function reduce(func, arr, memo) {
var len = arr.length,
i = 0;
accum = memo;
for(; i < len; i++) {
accum = func(accum, arr[i]);
}
return accum;
}
var arr = [1,2,3,4];
var sum = function(x, y) {
return x+y;
};
var multyply = function(x, y) {
return x*y;
};
console.log(reduce(sum, arr, 0)); // 10
console.log(reduce(multyply, arr, 1)); // 24
reduce 함수에서 함수와 배열, 기본값을 인자로 받아 넘기고 루프를 돌며 함수를 실행시킨다. 결과값은 변수 accum에 저장된다.
- 팩토리얼
함수형 프로그래밍을 이용해서 팩토리얼을 구현해보자.
function fact(num) {
if (num === 0 ) return 1;
else return num * fact(num-1);
}
console.log(fact(10)); // 3628800
간단한 재귀함수를 통해 팩토리얼을 구현했다. 이를 함수형 프로그래밍으로 하면, (사실 그냥 하려면 이게 더 쉽지만 재사용성 등을 위해서..)
var factorial = function() {
var cache = {'0' : 1};
var func = function(n) {
var result = 0;
if(typeof(cache[n]) === 'number') {
result = cache[n];
} else {
result = cache[n] = n * func(n-1);
}
return result;
}
return func;
}();
console.log(factorial(1)); // 1
console.log(factorial(10)); // 3628800
console.log(factorial(20)); // 2432902008176650000
변수 factorial은 cache에 접근할 수 있는 클로저를 반환받아, 캐시에 저장된 값이 있으면 곧바로 그 값을 반환하는 방식으로 만들었다. (아직도 어렵다.) 한 번 연산된 값을 cache에 저장하고 있기 때문에 중복 연산을 피할 수 있다.
- 피보나치 수열
var fibo = function() {
var cache = {'0' : 0, '1' : 1};
var func = function(n) {
if (typeof(cache[n]) === 'number') {
result = cache[n];
} else {
result = cache[n] = func(n-1) + func(n-2);
}
return result;
}
return func;
}();
console.log(fibo(10)); // 55
클로저를 활용하여 cache를 캐시로 활용한다.
함수형 프로그램밍의 주요함수
- 커링
커링이란 특정 함수에서 정의된 인자의 일부를 넣어 고정시켜, 나머지를 인자로 받는 새로운 함수를 받는 것을 의미한다.
function calc(a, b, c) {
return a*b+c;
}
function curry2(func) {
var args = Array.prototype.slice.call(arguments, 1);
return function() {
var arg_index = 0;
for (var i = 0; i < args.length && arg_index < arguments.length; i++)
if (args[i] === undefined ) args[i] = arguments[arg_index++];
return func.apply(null, args);
}
}
var new_func = curry2(calc, 1, undefined, 4);
console.log(new_func(3)); // 7 1 * 3 + 4
var new_func2 = curry2(calc, 1, 3, undefined);
console.log(new_func2(5));
- bind
bind 함수는 특정 함수에 원하는 객체를 바인딩시켜서 새로운 함수를 사용할 때 bind 함수를 사용하는 것이다.
var print_all = function(arg) {
for(var i in this ) console.log(i + " : " + this[i]);
for(var i in arguments ) console.log(i + " : " + arguments[i]);
}
var myObj = { name : 'Jang' };
var myfunc = print_all.bind(myObj);
myfunc(); // name : Jang // 2
var myfunc1 = print_all.bind(myObj, "I am Jang", "others");
myfunc1("inside"); // 0 : I am Jang, 1 : others, 2: inside // 3
console.log(print_all); // [Function: print_all] // 1
1번째는 변수 print_all의 로그값. 2번째는 myfunc라는 변수에 print_all 변수에 myObj를 바인딩한 것을 할당한 것이다.
3번째는 myfun1에 3가지 인수를 바인딩해서 할당한 것이다.
- 반복함수
- map
배열의 각 요소를 꺼내서 사용자 정의 함수를 적용시켜 새로운 값을 얻은 후, 새로운 배열에 넣는 함수이다. 이 때 원본 배열은 변형되지 않는다.
즉, 배열을 순회하여 요소 값을 다른 값으로 매핑하기 위한 함수이다.
var numbers = [1, 4, 9];
var roots = numbers.map(function (item) {
return Math.sqrt(item);
});
console.log(roots); // [1,2,3]
console.log(numbers); // [1,4,9] -> 원본 배열은 변하지 않는다.
numbers = [1,4,9];
roots = numbers.map(function (item) {
return ++item;
});
console.log(roots); // [2,5,10]
또한 두 번째 인자로 this를 전달할 수 있다.
function Prefix(prefix) {
this.prefix = prefix;
}
Prefix.prototype.prefixArray = function (arr) {
return arr.map(function (x) {
return this.prefix + x; // 2번째 인자가 없다면 this는 전역 객체
}, this);
};
var pre = new Prefix('-webkit-');
var preArr = pre.prefixArray(['liner-gredient', 'border-radius']);
console.log(preArr); // [ '-webkit-liner-gredient', '-webkit-border-radius' ]
- forEach
배열을 순회하며 배열의 각 요소로 주어진 인자에 콜백함수를 실행한다. 매개변수를 통해 배열 요소의 값, 인덱스, 순회할 배열을 전달받을 수 있다. for와 다른 점은 break가 없다는 점이다. 또한 forEach 역시 원본 배열을 변경하지 않는다.
function Counter() {
this.sum = 0;
this.count = 0;
}
Counter.prototype.add = function (array) {
array.forEach(function (entry) {
this.sum += entry;
this.count++;
}, this);
};
var counter = new Counter();
counter.add([2,5,9]);
console.log(counter.count); // 3
console.log(counter.sum); // 16
- filter
배열을 순회하며 각 요소의 인자로 주어진 값에서 콜백함수의 실행결과가 true인 것만 반환한다. 역시 원본 배열은 변경되지 않는다.
var result = [1,2,3,4,5].filter(function (item, index, array) {
console.log('[' + index + '] = ' + item);
return item % 2; // 각 배열 요소, 인덱스, 순회할 배열을 인자로 받아 2로 나눴을 때 나머지가 있는 것만 반환
});
console.log(result); // [0] = 1
// [1] = 2
// [2] = 3
// [3] = 4
// [4] = 5
// [ 1, 3, 5 ]
- reduce
각 배열을 순회하여 콜백 함수를 적용시킨 뒤, 이전 콜백함수의 값을 누적시키는 함수이다.
var result = [1,2,3,4,5].reduce(function (previouValue, currentValue, currentIndex, array) {
console.log(previouValue + '+' + currentValue + '=' + (previouValue + currentValue));
return previouValue + currentValue;
});
console.log(result);
// 1+2=3
// 3+3=6
// 6+4=10
// 10+5=15
// 15