2015.01.07
Kim-down자바스크립트로 함수형 프로그래밍에서 제시하는 방법론 중 일부는 구현가능하지만. 순수한 함수형 프로그래밍 언어라고는 말하지않는다.
함수의 조합으로 작업을 수행함을 의미한다. 작업이 이루어 지는 동안 작업에 필요한 데이터와 상태는 변하지 않는다.
순수 함수형 언어는 변수가 없다. 대입이 아닌 정의로 볼 수 있다. a = 3 이라는것을 명령형에서는 a에 3을 대입하는게 되겠지만 함수형에서는 a는 3 이라고 정의한것이다!.
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));
이 함수는 단순히 배열의 각 원소의 합을 구하는 함수이다.
이번에는 배열의 각 원소를 곱한 값을 구하고 싶어졌다..
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)); // Result 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; }
reduce 함수는 함수와 배열을 인자로 받고 루프를 돌면서 함수를 실행시킨다.
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 multiply = function(x, y) { return x*y; }; console.log(reduce(sum, arr, 0)); console.log(reduce(multiply, arr, 1));예 에서 보는바와 같이 함수형 프로그래밍을 이용하여 코드를 훨씬 간결하게 작성하고, 기존 프로그래밍 방식보다 한단계 위의 모듈화를 이룰 수 있다.
function fact(num) { if (num == 0) return 1; else return num * fact(num-1); } console.log(fact(100)); // Result : 9.33262154439441e+157
큰 무리없이 팩토리얼을 구현하는데 성공하였다. 하지만 이런종류의 함수 구현은 항상 성능을 고려하여야한다. 이를 함수형 프로그래밍으로 성능을 고려하여보자.
처음 10! 을 실행한 후 20! 을 실행한다고 가정해보자. 20!을 실행할 떄에는 앞에서 실행한 10!을 중복하여 계산한다. 먼저 계산한 값을 캐시에 저장하여 사용할수 있는 함수를 만들어 작성해보자
var fact = function() { var cache = {'0' : 1}; var func = function(n) { var result = 0; if (typeof(cache[n]) === 'number') { result = cache[n]; } else { cache[n] = n * func(n-1); result = cache[n] } return result; } return func; }(); console.log(fact(10)); console.log(fact(20));
fact 는 cache 에 접근할 수 있는 클로저를 반환받는다. cache에는 팩토리얼을 연산한 값을 저장하고있다. 연산을 수행하는 과정에서 캐시에 저장된 값이 있으면 곧바로 그 값을 반환하는 방식이다.
일반 프로그래머의 상식으로는 도저히 이해가 안 될 결정인데 일부러 난해한 프로그래밍 언어라도 만들 생각으로 만들었을까?
그게 아니고 함수형 언어는 수학의 '함수'를 프로그래밍 언어 설계에 적극적으로 반영한 것이다. 수학의 함수는 입력이 같으면 출력도 같다. 그러니까 f(x)=x+1 인 함수를 정의했다면 f(1)=2다. 다른 답은 없다. 함수형 언어의 함수도 마찬가지로 function boo(1)의 수행 결과가 2였다면 언제 어느때든 boo(1)은 2만 나온다. 하지만 함수형 언어가 아닌 언어에서는 boo(1)이 3도 나올 수 있고 4도 나올 수 있다.
Memoization만 건든게 함수형과 무슨관계인가?. 함수형 프로그램은 위의 특징으로 얻는 이득으로 인해 캐싱이 가능하다. 그 함수를 호출한 파라메터를 알고있고 그 수행한 결과를 안다면 계산할 필요가 있는가?
결과를 아는이상 계산은 필요가없스!
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(5));이전 팩토리얼 함수와 다른점은 캐시의 초기값과 재귀호출시 산술식이 다르다는것. 함수형 프로그래밍 으로 피보나치 수열과 팩토리얼을 계산하는 함수를 인자로받는 함수를만들어보았다.
var cacher = function(cache, func) { var calculate = function(n) { if (typeof(cache[n]) === 'number') { result = cache[n]; } else { result = cache[n] = func(calculate, n); } return result; } return calculate; }; var fact = cacher({ '0' : 1 }, function(func, n) { return n * func(n-1); }); var fibo = cacher({ '0' : 0, '1' : 1}, function(func, n) { return func(n-1) + func(n-2); }); console.log(fact(10)); console.log(fibo(10));
function calculate(a, b, c) { return a*b+c; } function curry(func) { var args = Array.prototype.slice.call(arguments, 1); return function() { return func.apply(null, args.concat(Array.prototype.slice.call(arguments))); } } var new_func1 = curry(calculate, 1); console.log(new_func1(2, 3)); var new_func2 = curry(calculate, 1, 3); console.log(new_func2(3));curry함수의 역할은 간단하다. 인자를 args에 담아놓고 새로운 함수호출로 넘어온 인자와 합쳐서 함수를 적용. 함수형 프로그래밍언어에서는 기본적으로 지원하는함수지만 자바스크립트에는 없기에 Function.prototype에 넣어서 사용하는게 좋다.
function calculate(a, b, c) { return a*b+c; } function curry2(func) { var args = Array.prototype.slice.call(arguments, 1); return function() { var arg_idx = 0; for (var i = 0; i < args.length && arg_idx < arguments.length; i++){ if (args[i] === undefined) { args[i] = arguments[arg_idx++]; } } return func.apply(null, args); } } var new_func = curry2(calculate, 1, undefined, 4); console.log(new_func(3));
curry2에서는 호출시 넘어온 인자로 루프를 돌면서 undefined인 요소를 새로운 함수를 호출할 때 넘어온 인자로 대체한다. 이와같이 함수를 부분적으로 적용햐여 새로운 함수를 반환받는 방식을 함수의 부분적용 이라고 부른다.
Function.prototype.bind = function (thisArg) { var fn = this, slice = Array.prototype.slice, args = slice.call(arguments, 1); return function() { return fn.apply(thisArg, arg.concat(slice.call(arguments))); }; }bind 는 위와같이 커링기법을 활용한 함수이다. 커링과 같이 사용자가 고정시키고자 하는 인자를 bind()함수를 호출할 때 인자로 넘겨주고 반환받은 함수를 실행할 때 나머지 가변인자를 넣어줄 수 있다. curry()함수와 다른점은 함수를 호출할때 this바인딩시킬 객체를 사용자가 넣을 수 있다. curry함수가 자바스크립트 엔진에 내장되지않은것도 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 : "down" }; var myfunc = print_all.bind(myobj); myfunc(); /* name : down */ var myfunc1 = print_all.bind(myobj, "kim", "others"); myfunc1("inside js"); /* name : down 0 : kim 1 : others 2 : inside js */
이와 같이 특정함수에 원하는객체를 바인딩시켜 새로운 함수를 사용할 때 bind()함수가 사용된다.
function wrap(object, method, wrapper) { var fn = object[method]; return object[method] = function() { return wrapper.apply(this, [fn.bind(this)].concat( Array.prototype.slice.call(arguments) )); }; } Function.prototype.original = function(value) { this.value = value; console.log("value :" + this.value); } var mywrap = wrap(Function.prototype, "original", function(orig_func, value) { orig_func(value); this.value = 20; console.log("wrapper value : " + this.value); } ); mywrap("down")간단히 알아두고 넘기자. 이전 함수를 사용자가 원하는함수로 덮어씌우기 위한 함수이다. 기존에 제공되는 함수에 사용자가 원하는 로직을 추가하고 싶거나. 기존에 있는 버그를 피해가고자 할 때 사용된다.