7.3 자바스크립트에서의 함수형 프로그래밍을 활용한 주요 함수

vencott·2021년 6월 2일

이 장에서는 자바스크립트에서 함수형 프로그래밍을 사용한 여러 가지 함수를 소개한다

이를 통해 자바스크립트를 이용한 함수형 프로그래밍의 감을 잡아 나가자

7.3.1 함수 적용

Function.prototype.apply 함수로 함수 호출을 수행할 수 있다

왜 이름이 우리에게 익숙한 "call"이 아닌 "apply"일까?

함수 적용(Applying functions)은 함수형 프로그래밍에서 사용하는 용어다

함수형 프로그래밍에서는 특정 데이터를 여러 가지 함수를 적용시키는 방식으로 작업을 수행한다

여기서 함수는 단순히 입력을 넣고 출력을 받는 기능을 수행하는 것 뿐만 아니라, 인자 혹은 반환 값으로 전달된 함수를 특정 데이터에 적용시키는 개념으로 이해해야 한다

따라서 func.apply(Obj, Args)와 같은 함수 호출을 "func 함수를 Obj 객체와 Args 인자 배열에 적용시킨다" 라고 표현한다

7.3.2 커링

커링이랑 특정 함수에서 정의된 인자의 일부를 넣어 고정시키고, 나머지를 인자로 받는 새로운 함수를 만드는 것을 의미한다

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); // args = [1]
console.log(new_func1(2, 3)); // calculate(1, 2, 3)
var new_func2 = curry(calculate, 1, 3); // args = [1, 3]
console.log(new_func2(3)); // calculate(1, 3, 3)

calculate() 함수는 인자 세 개를 전달받아 연산을 수행한다

여기서 curry() 함수로 첫 번째 인자를 1로 고정시킨 뒤 새로운 함수 new_func1()과 첫번째, 두번째 인자를 1과 3으로 고정시킨 new_func2() 함수를 새롭게 만들 수 있다

curry() 함수는 넘어온 인자를 args에 담아 놓고, 새로운 함수 호출로 넘어온 인자와 합쳐서 함수를 적용한다

이러한 커링은 함수형 프로그래밍 언어에서 기본적으로 제공하지만 자바스크립트에서는 제공하지 않는다

다음과 같이 Function.prototype에 커링 함수를 정의하여 사용할 수 있다

Function.prototype.curry = function () {
    var fn = this;
    var args = Array.prototype.slice.call(arguments);

    return function () {
        return fn.apply(this, args.concat(Array.prototype.slice.call(arguments)));
    };
}

예제에서 첫번째 인자와 세번째 인자를 고정시키고 싶다면 어떻게 해야할까?

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); // args = [1, undefined, 4]
console.log(new_func(3)); // calculate(1, 3, 4)

curry2() 함수를 사용할 때는 calculate() 함수가 원하는 인자를 전부 넣어주되, 그 중에서 고정시키지 않을 인자를 undefined로 넘기면 된다

이와 같이 함수를 부분적으로 적용하여 새로운 함수를 반환받는 방식을 함수의 부분 적용(Partially applying functions)이라고 한다

7.3.3 bind

bind 함수는 이미 4장에서 언급하였다

Function.prototype.bind = function (thisArg) {
    var fn = this;
    var slice = Array.prototype.slice;
    var args = slice.call(arguments, 1);
    return function () {
        return fn.apply(thisArg, args.concat(slice.call(arguments)))
    }
}

앞서 설명된 curry() 함수와 상당히 유사하다

커링과 같이 사용자가 고정시키고자 하는 인자를 bind() 함수를 호출할 때 인자로 넘겨주고 반환받은 함수를 호출하면서 나머지 가변 인자를 넣어줄 수 있다

curry()와 다른 점은 함수를 호출할 때 this에 바인딩시킬 객체를 사용자가 너허줄 수 있다는 점이다

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: "zzoon"}; // name : zzoon

var myFunc = print_all.bind(myObj); // this가 myObj로 바인딩 된 print_all 함수 반환
myFunc();

var myFunc1 = print_all.bind(myObj, "iamjoo", "others");
myFunc1("insideJs");

/*
name : zzoon
0 : iamjoo
1 : others
2 : insideJs
*/

myFunc 함수는 myObj 객체를 this에 바인딩시켜 print_all() 함수를 실행하는 새로운 함수이다

myFunc1 함수는 인자도 bind() 함수에 모두 넘겨진다

이와 같이 특정 함수에 원하는 객체를 바인딩시켜 새로운 함수를 사용할 때 bind() 함수가 사용된다

7.3.4. 래퍼

래퍼(Wrapper)란 쉽게 말하면 특정 함수를 자신의 함수로 덮어쓰는 것을 말한다

물론 여기서 사용자는 우너래 함수 기능을 잃어버리지 않은 상태로 자신의 로직을 수행할 수 있어야 한다

객체지향 프로그래밍에서 다형성의 특성을 살리기 위해 오버라이드를 지원하는데, 이와 상당히 유사하다고 보면 된다

function wrap(object, method, wrapper) {
    var fn = object[method];
    return object[method] = function () {
        return wrapper.apply(this, [fn].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("zzoon");
// value : zzoon
// wrapper value : 20

Function.prototype에 original이라는 함수가 있고, 이는 인자로 넘어온 값을 value에 할당하고 출력한다

이를 사용자가 덮어쓰기 위해 wrap 함수를 호출하였다

세번째 인자로 넘긴 자신의 익명 함수를 Function.prototype.original에 덮어쓰는 것이다

여기서 사용자는 자신의 익명 함수의 첫 번째 인자로 원래 함수의 참조를 받을 수 있다

이 참조로 원래 함수를 실행하고 자신의 로직을 수행할 수 있다

wrap() 함수는 curry()나 bind() 함수와 유사하다

중요한 것은 사용자가 세 번째 인자로 받은 함수 wrapper()를 apply() 함수로 호출한다는 것인데, 인자의 첫 번째가 [[fn]]이라는 것에 집중하자

이렇게 기존 함수의 참조를 첫 번째 인자로 넘김으로써 사용자는 이 함수에 접근할 수 있다

클로저를 절묘하게 사용한 함수형 프로그래밍의 한 방법이다

그런데 여기서 원래 함수 original()이 호출될 때의 this와 반환되는 익명 함수가 호출될 때의 this가 다르다

이를 해결하려면 우리는 앞서 살펴본 bind 함수를 적절히 이용해야 한다

apply 를 호출할 때 첫 번째 인자로 [[fn]] 대신 [[fn.bind(this)]]를 쓰면 이 문제는 해결된다

원래 함수에 반환되는 익명 함수의 this를 바인딩하는 것이다

래퍼는 기존에 제공되는 함수에서 사용자가 원하는 로직을 추가하고 싶다거나, 기존에 있는 버그를 피해가고자 할 때 많이 사용된다

특히 특정 플랫폼에서 버그를 발생시키는 함수가 있을 경우 이를 컨트롤할 수 있으므로 상당히 용이하다

7.3.5 반복 함수

반복 함수는 자바스크립트에서 함수형 프로그래밍 특성을 활용하여 매우 유용하게 사용될 수 있으므로 잘 기억해 두자

7.3.5.1 each

each()함수는 배열의 각 요소 혹은 객체의 각 프로퍼티를 하나씩 꺼내어 차례대로 특정 함수에 인자로 넣어 실행시키는 역할을 한다

보통 each 혹은 forEach라는 이름으로 제공된다

다음 코드는 jQuery 1.0의 each() 함수이다

function each(obj, fn, args) {
    if (obj.lenght == undefined) {
        for (var i in obj)
            fn.apply(obj[i], args || [i, obj[i]]);
    } else {
        for (var i = 0; i < obj.lenght; i++)
            fn.apply(obj[i], args || [i, obj[i]]);
    }

    return obj;
}

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

var zzoon = {
    name: "zzoon",
    age: 30,
    sex: "Male"
};

each(zzoon, function (idx, value) {
    console.log(idx + " : " + value);
});

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

each() 함수는 다양한 언어에서 기본적으로 제공되는데, 자바스크립트에서도 앞과 같은 방법으로 정의하여 사용될 수 있음을 알아두자

다만, each() 함수에서 사용자 함수를 호출할 때 넘기는 인자의 순서나 구성이 라이브러리에 따라 다를 수 있으므로 이 점만 주의하면 된다

사실 each()의 인자로 넘기는 익명 함수가 순수 함수가 아닌 프로시져이므로 순수한 의의 함수형 프로그래밍은 아니나 다음에 설명할 map()과 reduce()의 이해를 돕기 위해 설명하였다

7.3.5.2 map

map() 함수는 주로 배열에 많이 사용되는 함수이다

배열의 각 요소를 꺼내어 사용자 정의 함수를 적용시켜 새로운 값을 얻은 후, 새로운 배열에 넣는다

Array.prototype.map = function (callback) {
    // this가 null인지, 배열인지 체크
    // callback이 함수인지 체크

    var obj = this;
    var value;
    var 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]

배열 각 요소의 제곱값을 새로운 요소로 하는 배열을 반환받는 예이다

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

7.3.5.3 reduce

reduce()는 배열의 각 요소를 하나씩 꺼내어 사용자의 함수를 적용시킨 뒤, 그 값을 계속하여 누적시키는 함수이다

Array.prototype.reduce = function(callback, memo) {
    // this가 null인지, 배열인지 체크
    // callback이 함수인지 체크

    var obj = this;
    var value;
    var 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);
// accumulated_value
// for 1번 돌았을 때 : 1
// for 2번 돌았을 때 : 5
// for 3번 돌았을 때 : 14

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

각 요소를 사용자가 원하는 특정 연산으로 누적된 값을 반환받고자 할 때 유용하게 사용된다


출처: 송형주, 고현준 저, 『인사이드 자바스크립트』, 한빛미디어(2014)

profile
Backend Developer

0개의 댓글