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

JavaScript는 Lisp이나 Haskell과 같은 기능적 프로그래밍 언어는 아니지만 JavaScript에서 기능을 객체로 조작할 수 있다는 사실은 JavaScript에서 기능적 프로그래밍 기술을 사용할 수 있음을 의미합니다. map() 및 reduce()와 같은 배열 메서드는 특히 함수형 프로그래밍 스타일에 적합합니다.

함수로 배열 처리

숫자 배열이 있고 해당 값의 평균과 표준 편차를 계산하려고 한다고 가정합니다. 다음과 같이 기능적이지 않은 스타일로 수행할 수 있습니다.

let data = [1,1,3,5,5];  // This is our array of numbers

// The mean is the sum of the elements divided by the number of elements
let total = 0;
for(let i = 0; i < data.length; i++) total += data[i];
let mean = total/data.length;  // mean == 3; The mean of our data is 3

// To compute the standard deviation, we first sum the squares of
// the deviation of each element from the mean.
total = 0;
for(let i = 0; i < data.length; i++) {
    let deviation = data[i] - mean;
    total += deviation * deviation;
}
let stddev = Math.sqrt(total/(data.length-1));  // stddev == 2



다음과 같이 배열 메서드 map() 및 reduce()를 사용하여 간결한 기능 스타일로 동일한 계산을 수행할 수 있습니다.

// First, define two simple functions
const sum = (x,y) => x+y;
const square = x => x*x;

// Then use those functions with Array methods to compute mean and stddev
let data = [1,1,3,5,5];
let mean = data.reduce(sum)/data.length;  // mean == 3
let deviations = data.map(x => x-mean);
let stddev = Math.sqrt(deviations.map(square).reduce(sum)/(data.length-1));
stddev  // => 2



이 새 버전의 코드는 첫 번째 코드와 상당히 다르게 보이지만 여전히 개체에 대한 메서드를 호출하므로 일부 개체 지향 규칙이 남아 있습니다. map() 및 reduce() 메서드의 기능적 버전을 작성해 보겠습니다.

const map = function(a, ...args) { return a.map(...args); };
const reduce = function(a, ...args) { return a.reduce(...args); };



이러한 map() 및 reduce() 함수를 정의하면 평균 및 표준 편차를 계산하는 코드는 이제 다음과 같습니다.

const sum = (x,y) => x+y;
const square = x => x*x;

let data = [1,1,3,5,5];
let mean = reduce(data, sum)/data.length;
let deviations = map(data, x => x-mean);
let stddev = Math.sqrt(reduce(map(deviations, square), sum)/(data.length-1));
stddev  // => 2



고차 함수

고차 함수는 하나 이상의 함수를 인수로 사용하고 새 함수를 반환하는 함수에 대해 작동하는 함수입니다.

// This higher-order function returns a new function that passes its
// arguments to f and returns the logical negation of f's return value;
function not(f) {
    return function(...args) {             // Return a new function
        let result = f.apply(this, args);  // that calls f
        return !result;                    // and negates its result.
    };
}

const even = x => x % 2 === 0; // A function to determine if a number is even
const odd = not(even);         // A new function that does the opposite
[1,1,3,5,5].every(odd)         // => true: every element of the array is odd



이 not() 함수는 함수 인수를 취하고 새 함수를 반환하기 때문에 고차 함수입니다. 또 다른 예로, 다음에 나오는 mapper() 함수를 고려하십시오. 함수 인수를 사용하여 해당 함수를 사용하여 하나의 배열을 다른 배열에 매핑하는 새 함수를 반환합니다. 이 함수는 앞에서 정의한 map() 함수를 사용하며 두 함수가 어떻게 다른지 이해하는 것이 중요합니다.

// Return a function that expects an array argument and applies f to
// each element, returning the array of return values.
// Contrast this with the map() function from earlier.
function mapper(f) {
    return a => map(a, f);
}

const increment = x => x+1;
const incrementAll = mapper(increment);
incrementAll([1,2,3])  // => [2,3,4]



다음은 f와 g의 두 함수를 사용하고 f(g())를 계산하는 새 함수를 반환하는 또 다른 보다 일반적인 예입니다.

// Return a new function that computes f(g(...)).
// The returned function h passes all of its arguments to g, then passes
// the return value of g to f, then returns the return value of f.
// Both f and g are invoked with the same this value as h was invoked with.
function compose(f, g) {
    return function(...args) {
        // We use call for f because we're passing a single value and
        // apply for g because we're passing an array of values.
        return f.call(this, g.apply(this, args));
    };
}

const sum = (x,y) => x+y;
const square = x => x*x;
compose(square, sum)(2,3)  // => 25; the square of the sum



기능의 부분적 적용

함수 f의 bind() 메서드는 지정된 컨텍스트에서 지정된 인수 집합을 사용하여 f를 호출하는 새 함수를 반환합니다. 함수를 객체에 바인딩하고 인수를 부분적으로 적용한다고 말합니다. bind() 메서드는 부분적으로 왼쪽에 인수를 적용합니다. 즉, bind()에 전달한 인수는 원래 함수에 전달된 인수 목록의 시작 부분에 배치됩니다.

// The arguments to this function are passed on the left
function partialLeft(f, ...outerArgs) {
    return function(...innerArgs) { // Return this function
        let args = [...outerArgs, ...innerArgs]; // Build the argument list
        return f.apply(this, args);              // Then invoke f with it
    };
}

// The arguments to this function are passed on the right
function partialRight(f, ...outerArgs) {
    return function(...innerArgs) {  // Return this function
        let args = [...innerArgs, ...outerArgs]; // Build the argument list
        return f.apply(this, args);              // Then invoke f with it
    };
}

// The arguments to this function serve as a template. Undefined values
// in the argument list are filled in with values from the inner set.
function partial(f, ...outerArgs) {
    return function(...innerArgs) {
        let args = [...outerArgs]; // local copy of outer args template
        let innerIndex=0;          // which inner arg is next
        // Loop through the args, filling in undefined values from inner args
        for(let i = 0; i < args.length; i++) {
            if (args[i] === undefined) args[i] = innerArgs[innerIndex++];
        }
        // Now append any remaining inner arguments
        args.push(...innerArgs.slice(innerIndex));
        return f.apply(this, args);
    };
}

// Here is a function with three arguments
const f = function(x,y,z) { return x * (y - z); };
// Notice how these three partial applications differ
partialLeft(f, 2)(3,4)         // => -2: Bind first argument: 2 * (3 - 4)
partialRight(f, 2)(3,4)        // =>  6: Bind last argument: 3 * (4 - 2)
partial(f, undefined, 2)(3,4)  // => -6: Bind middle argument: 3 * (2 - 4)



이러한 부분적 적용 기능을 통해 이미 정의한 기능 중에서 흥미로운 기능을 쉽게 정의할 수 있습니다.

const increment = partialLeft(sum, 1);
const cuberoot = partialRight(Math.pow, 1/3);
cuberoot(increment(26))  // => 3



부분 적용은 다른 고차 함수와 결합할 때 훨씬 더 흥미로워집니다. 예를 들어 다음은 구성 및 부분 적용을 사용하여 방금 표시된 이전 not() 함수를 정의하는 방법입니다.

const not = partialLeft(compose, x => !x);
const even = x => x % 2 === 0;
const odd = not(even);
const isNumber = not(isNaN);
odd(3) && isNumber(2)  // => true



구성 및 부분 적용을 사용하여 극단적인 기능 스타일로 평균 및 표준 편차 계산을 다시 실행할 수도 있습니다.

// sum() and square() functions are defined above. Here are some more:
const product = (x,y) => x*y;
const neg = partial(product, -1);
const sqrt = partial(Math.pow, undefined, .5);
const reciprocal = partial(Math.pow, undefined, neg(1));

// Now compute the mean and standard deviation.
let data = [1,1,3,5,5];   // Our data
let mean = product(reduce(data, sum), reciprocal(data.length));
let stddev = sqrt(product(reduce(map(data,
                                     compose(square,
                                             partial(sum, neg(mean)))),
                                 sum),
                          reciprocal(sum(data.length,neg(1)))));
[mean, stddev]  // => [3, 2]



평균과 표준 편차를 계산하는 이 코드는 전적으로 함수 호출입니다. 관련된 연산자가 없으며 괄호의 수가 너무 많아 이 JavaScript가 Lisp 코드처럼 보이기 시작했습니다.

다시 말하지만 이것은 내가 JavaScript 프로그래밍을 옹호하는 스타일은 아니지만 JavaScript 코드가 얼마나 깊이 기능할 수 있는지 알아보는 흥미로운 연습입니다.

좋은 웹페이지 즐겨찾기