함수형 프로그래밍의 개념

  • 함수의 조합으로 작업을 수행함. 작업이 이루어지는 동안 작업에 필요한 데이터와 상태는 변하지 않는다. 변할 수 있는 건 오로지 함수뿐이다. 이 함수가 바로 연산의 대상이 된다.
    ( <-> 명령형 프로그래밍 : 컴퓨터가 수행할 일의 명령을 순서대로 기술하는 프로그래밍 방식 )
// 특정 문자열을 암호화하는 함수형 프로그래밍

// f1, f2, f3은 입력값이 정해지지 않고, 서로 다른 암호화 알고리즘만 있다.
f1 = encrypt1;
f2 = encrypt2;
f3 = encrypt3;

// pure_value는 암호화할 문자열, encrypted_value는 암호화된 문자열.
// get_encrypted()는 암호화 함수를 받아서 입력받은 함수로 pure_value를 암호화한 후 반환.
pure_value = 'abcde';
encrypted_value = get_encrypted(x);

// 다음과 같이 처리
encrypted_value = get_encrypted(f1);
encrypted_value = get_encrypted(f2);
encrypted_value = get_encrypted(f3);

1) pure_value는 작업에 필요한 데이터고 작업이 수행되는 동안 변하지 않는다.
2) get_encrypted()가 작업하는 동안 변할 수 있는 것은 오로지 입력으로 들어오는 함수뿐이다.
3) f1, f2, f3는 외부('abcde'라는 변수)에 아무런 영향을 미치지 않는 함수라고 할 수 있다. 이를 순수 함수(pure function)라고 한다.
4) get_encrypted() 함수는 인자로서 f1, f2, f3 함수를 받고, 결과값을 또 다른 형태의 함수로서 반환할 수도 있다.
5) 이렇게 함수를 또 하나의 값으로 간주하여 함수의 인자 혹은 반환값으로 사용할 수 있는 함수를 고계 함수(higher-order function)라고 한다.
6) 이와 같이 내부 데이터 및 상태는 그대로 둔 채 제어할 함수를 변경 및 조합함으로써 원하는 결과를 얻어내는 것이 함수형 프로그래밍의 중요한 특성이다.
7) 이 특성은 높은 수준의 모듈화가 가능하다는 점과 순수 함수의 조건을 충족하는 함수 구현으로 모듈 집약정이 프로그래밍이 가능하다.

자바스크립트에서의 함수형 프로그래밍

  • 자바스크립트에서도 함수형 프로그래밍이 가능하다.(일급 객체로서의 함수, 클로저의 지원)
<script>
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 = 'abcde';

    return function () {
        return func.call(null, str);
    }
}

var encrypted_value = get_encrypted(f1)();
console.log(encrypted_value);	// (출력값) 1
var encrypted_value = get_encrypted(f2)();
console.log(encrypted_value);	// (출력값) 2
var encrypted_value = get_encrypted(f3)();
console.log(encrypted_value);	// (출력값) 3
</script>

1) 이처럼 함수형 프로그래밍 슈도(pseudo) 코드를 구현할 수 있다.
2) 함수가 일급 객체로 취급되어 함수의 인자로 함수를 넘기고, 결과로 함수를 반환할 수 있다.
3) 변수 str 값이 영향을 받지 않게 하려고 클로저를 사용하였다. get_encrypted() 함수에서 반환하는 익명 함수가 클로저이다. 이 클로저에서 접근하는 변수 str은 외부에서는 접근할 수 없으므로 클로저로 함수형 프로그래밍의 개념을 정확히 구현해낼 수 있다.

시그마

// 명령형 프로그래밍 방식으로 작성된 코드
<script>
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
</script>
// 함수형 프로그래밍 방식으로 작성된 코드
<script>
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));       // (출력값) 10
console.log(reduce(multiply, arr, 1));  // (출력값) 24
</script>

1) 함수형 프로그래밍을 이용하여 코드를 훨씬 간결하게 작성할 수 있다.
2) 다른 문제가 나오더라도 사용자가 해당 연산을 하는 함수를 작성하여 reduce() 함수로 결과를 얻을 수 있다.

팩토리얼

// 명령형 프로그래밍 방식
<script>
function fact(num) {
    var val = 1;
    for(var i = 2; i <= num; i++)
        val = val * i;
    return val;
}

console.log(fact(10));  // (출력값) 3628800

------------------------------------------

function fact(num) {
    if(num == 0) return 1;
    else return num * fact(num - 1);
}

console.log(fact(10));  // (출력값) 3628800
</script>
// 함수형 프로그래밍 방식
<script>
var fact = 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(fact(10));  // (출력값) 3628800
console.log(fact(20));  // (출력값) 2432902008176640000
</script>

1) fact는 cache에 접근할 수 있는 클로저를 반환받는다. 클로저로 숨겨지는 cache에는 팩토리얼을 연산한 값을 저장하고 있다.
2) 연산을 수행하는 과정에서 캐시에 저장된 값이 있으면 곧바로 그 값을 반환하는 방식이다.
3) 한 번 연산된 값을 캐시에 저장하고 있으므로, 중복된 연산을 피하여 보다 나은 성능의 함수를 구현할 수 있다.
4) 메모이제이션 (memoization)

피보나치 수열

// 함수형 프로그래밍 방식
<script>
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
</script>

-------------------------------------------------------

<script>
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));  // (출력값) 3628800
console.log(fibo(10));  // (출력값) 55
</script>

1) cacher 함수는 사용자 정의 함수와 초기 cache 값을 받아 연산을 수행한다.
2) 사용자는 이 함수의 인자로 피보나치 수열을 연산하는 함수 혹은 팩토리얼을 연산하는 함수를 정의하여 사용할 수 있다.

함수형 프로그래밍을 활용한 주요 함수

커링

  • 특정 함수에서 정의된 인자의 일부를 넣어 고정시키고, 나머지를 인자로 받는 새로운 함수를 만드는 것
<script>
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));    // (출력값) 5 // 1 x 2 + 3 = 5
var new_func2 = curry(calculate, 1, 3);
console.log(new_func2(3));      // (출력값) 6 // 1 x 3 + 3 = 6
</script>

1) calculate() 한수는 인자 세 개를 받아 연산을 수행하고 결과값을 반환
2) curry() 함수로 첫 번째 인자를 1로 고정시킨 새로운 함수 new_func1()과 첫 번째, 두 번째 인자를 1과 3으로 고정시킨 new_func2() 함수를 새롭게 만든다.
3) curry() 함수로 넘어온 인자를 args에 담아 놓고, 새로운 함수 호출로 넘어온 인자와 합쳐서 함수를 적용한다.

------------------------------------------------------------

// 첫 번째 인자와 세 번째 인자를 고정하고 싶을 때
<script>
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));   // (출력값) 7 // 1 x 3 + 4 = 7
</script>

- 함수의 부분 적용 (Partially applying functions)

bind

<script>
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 : "LEE" };

var myfunc = print_all.bind(myobj);
myfunc();   // (출력값) "name : LEE"

var myfunc1 = print_all.bind(myobj, "KIM", "others");
myfunc1("insidejs");
// (출력값)
// name : LEE
// 0 : KIM
// 1 : others
// 2 : insidejs
</script>

1) myfunc() 함수는 myobj 객체를 this에 바인딩시켜 print_all() 함수를 실행하는 새로운 함수이다.
2) myfunc1()을 실행하면 인자도 bind() 함수에 모두 넘겨진다. 이와 같이 특정 함수에 원하는 객체를 바인딩시켜 새로운 함수를 사용할 때 bind() 함수가 사용된다.

래퍼(wrapper)

  • 특정 함수를 자신의 함수로 덮어쓰는 것
  • 사용자는 원래 함수 기능을 잃어버리지 않은 상태로 자신의 로직을 수행할 수 있어야 한다.
  • 객체지향 프로그래밍에서 다형성의 특성을 살리려면 오버라이드를 지원하는데, 이와 상당히 유사하다.
<script>
function wrap(object, method, wrapper) {
    var fn = object[method];
    return object[method] = function () {
        return wrapper.apply(this, [ fn ].concat(
        // 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) {
    this.value = 20;
    orig_func(value);
    console.log("wrapper value : " + this.value);
});

var obj = new mywrap("LEE");
// (출력값)
// value : LEE
// wrapper value : 20
</script>

1) Function.prototype에 original이라는 함수가 있고, 이는 인자로 넘어온 값을 vlaue에 할당하고 출력하는 기능을 한다.
2) 이를 사용자가 덮어쓰기 위해 wrap 함수를 호출하였다. 세 번째 인자로 넘긴 자신의 익명 함수를 Function.prototype.original에 덮어쓰련느 것이다. 
3) 여기서 사용자는 자신의 익명 함수의 첫 번째 인자로 원래 함수의 참조를 받을 수 있다.
4) 이 참조로 원래 함수를 실행하고 자신의 로직을 수행할 수 있다.

반복 함수

each

  • each() 함수는 배열의 각 요소 혹은 객체의 각 프로퍼티를 하나씩 꺼내서 차례대로 특정 함수에 인자로 넣어 실행시키는 역할을 한다.
<script>
function each(obj, fn, args) {
    if(obj.length == undefined)
        for(var i in obj)
            fn.apply( obj[i], args || [i, obj[i]] );
    else
        for(var i = 0; i < obj.length; i++)
            fn.apply( obj[i], args || [i, obj[i]] );
    return obj;
};

each([1,2,3], function (idx, num) {
    console.log(idx + " : " + num);
});
// (출력값)
// 0 : 1
// 1 : 2
// 2 : 3

var lee = {
    name : "LEE",
    age : 30,
    gender : "MALE"
};

each(lee, function (idx, value) {
    console.log(idx + " : " + value);
});
// (출력값)
// name : LEE
// age : 30
// gender : MALE
</script>

1) obj에 length가 있는 경우(보통 배열)와 없는 경우(보통의 경우 객체)로 나누어서, 루프를 돌면서 각 요소를 인자로 하여 차례대로 함수를 호출한다.

map

  • map() 함수는 주로 배열에 많이 사용되는 함수이다. 배열의 각 요소를 꺼내서 사용자 정의 함수를 적용시켜 새로운 값을 얻은 후, 새로운 배열에 넣는다.
<script>
Array.prototype.map = function (callback) {
    // this가 null인지, 배열인지 체크
    // callback이 함수인지 체크

    var obj = this;
    var value, mapped_value;
    var A = new Array(obj.length);

    for (var i = 0; i < obj.length; i++) {
        value = obj[i];
        mapped_value = callback.call(null, value);
        A[i] = mapped_value;
    }

    return A;
};

var arr = [1, 2, 3];
var new_arr = arr.map(function (value) {
    return value * value;
});

console.log(new_arr);   // (출력값) [1, 4, 9]
</script>

1) 배열 각 요소의 제곱값을 새로운 요소로 하는 배열을 반환받는다.
2) 사용자는 map() 함수를 이용하여 각 요소의 제곱값을 반환하는 순수 함수를 넣어 실행시킨 뒤 새로운 배열을 반환받을 수 있다.

reduce

  • reduce()는 배열의 각 요소를 하나씩 꺼내서 사용자의 함수를 적용시킨 뒤, 그 값을 계속해서 누적시키는 함수이다.
<script>
Array.prototype.reduce = function (callback, memo) {
    // this가 null인지, 배열인지 체크
    // callback이 함수인지 체크

    var obj = this;
    var value, accumulated_value = 0;

    for( var i = 0; i < obj.length; i++ ) {
        value = obj[i];
        accumulated_value = callback.call(null, accumulated_value, value);
    }

    return accumulated_value;
};

var arr = [1, 2, 3];
var accumulated_val = arr.reduce(function (a, b) {
    return a + b*b;
});

console.log(accumulated_val);   // (출력값) 14 // 1 x 1 + 2 x 2 + 3 x 3 = 14
</script>

1) 배열의 각 요소를 순차적으로 제곱한 값을 더해서 누적된 값을 반환받는다.

0개의 댓글

Powered by GraphCDN, the GraphQL CDN