[JavaScript] 고차함수

Hannahhh·2022년 7월 21일
0

JavaScript

목록 보기
24/47

일급 객체

다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체를 가리킨다.


👀 일급 함수

함수는 JavaScript의 대표적인 일급 객체 중 하나로, 아래와 같은 특징을 갖는다.

  • 변수에 할당(assignment)할 수 있다.
  • 다른 함수의 전달인자(argument)로 전달될 수 있다.
  • 다른 함수의 결과로서 리턴될 수 있다.

✔ 변수에 함수를 할당하는 경우

함수를 변수에 할당할 수 있기 때문에, 함수를 배열의 요소나 객체의 속성값으로 저장할 수 있다. 즉, 함수를 데이터(string, number, array, object)처럼 다룰 수 있다.

/*
 * 아래는 변수 square에 함수를 할당하는 함수 표현식입니다.
 * JavaScript에서 함수는 일급 객체이기 때문에 변수에 할당할 수 있습니다.
 *
 * 함수 표현식은 할당 전에 사용할 수 없습니다.
 * square(7); // --> ReferenceError: Can't find variable: square
 */

const square = function (num) {
  return num * num;
};

// 변수 square에는 함수가 할당되어 있으므로 (일급 객체), 함수 호출 연산자 '()'를 사용할 수 있습니다.
output = square(7);
console.log(output); // --> 49

🔍 고차함수

함수를 전달인자로 받을 수 있고, 함수를 리턴할 수 있는 함수를 의미한다.
함수는 일급 객체이기 때문에 고차함수로 사용할 수 있다.

이 때, 다른 함수(고차함수: caller)의 전달인자로 전달되는 함수를 콜백 함수(callback function)라고 한다.

✔ 다른 함수를 전달인자로 받는 함수

function double(num) {
  return num * 2;
}

function doubleNum(func, num) {
  return func(num);
}

/*
 * 함수 doubleNum은 다른 함수를 인자로 받는 고차 함수입니다.
 * 함수 doubleNum의 첫 번째 인자 func에 함수가 들어올 경우
 * 함수 func는 함수 doubleNum의 콜백 함수입니다.
 * 아래와 같은 경우, 함수 double은 함수 doubleNum의 콜백 함수입니다.
 */
let output = doubleNum(double, 4);
console.log(output); // -> 8

함수를 return하는 함수는 고안해 낸 논리학자 하스켈 커리의 이름을 따, '커링 함수'라고 한다.

해당 용어를 따로 사용하는 경우는 고차함수라는 용어를 '함수를 전달인자로 받는 함수'로 한정해 사용하기도 하지만 정확하게는 고차함수가 커링함수를 포함하는 것이다.

✔ 함수를 리턴하는 함수

function adder(added) {
  return function (num) {
    return num + added;
  };
}

/*
 * 함수 adder는 다른 함수를 리턴하는 고차 함수입니다.(caller)
 * adder는 인자 한 개를 입력받아서 함수(익명 함수)를 리턴합니다.
 * 리턴되는 익명 함수는 인자 한 개를 받아서 added와 더한 값을 리턴합니다.
 */

// adder(5)는 함수이므로 함수 호출 연산자 '()'를 사용할 수 있습니다.
let output = adder(5)(3); // -> 8
console.log(output); // -> 8

// adder가 리턴하는 함수를 변수에 저장할 수 있습니다.
// javascript에서 함수는 일급 객체이기 때문입니다.
const add3 = adder(3);
output = add3(2);
console.log(output); // -> 5

✔ 함수를 인자로 받고, 함수를 리턴하는 경우

function double(num) {
  return num * 2;
}

function doubleAdder(added, func) {
  const doubled = func(added);
  return function (num) {
    return num + doubled;
  };
}

/*
 * 함수 doubleAdder는 고차 함수입니다.
 * 함수 doubleAdder의 인자 func는 함수 doubleAdder의 콜백 함수입니다.
 * 함수 double은 함수 doubleAdder의 콜백으로 전달되었습니다.
 */

// doubleAdder(5, double)는 함수이므로 함수 호출 기호 '()'를 사용할 수 있습니다.
doubleAdder(5, double)(3); // -> 13

// doubleAdder가 리턴하는 함수를 변수에 저장할 수 있습니다. (일급 객체)
const addTwice3 = doubleAdder(3, double);
addTwice3(2); // --> 8




👀 고차 함수를 사용하는 이유?

높은 수준의 추상화를 통해 생산성을 향상할 수 있기 때문이다.


⭐ 추상화?
복잡한 어떤 것을 압축해서 핵심만 추출한 상태로 만드는 것


아래의 코드를 보면, getAverage 함수가 배열을 인자로 받아 평균값을 return하는 것을 알 수 있다.

function getAverage(data) {
  let sum = 0;
  for (let i = 0; i < data.length; i++) {
    sum = sum + data[i];
  }
  return sum / data.length;
}

let output = getAverage([1, 2, 3]);
console.log(output); // --> 2

output = getAverage([4, 2, 3, 6, 5, 4]);
console.log(output); // --> 4

이 때, getAverage 함수는 값(배열)을 전달받아, 이 값을 가지고 복잡한 작업을 수행한다. => '값 수준의 추상화'

  • 함수 = 값을 전달받아 값을 return = 값에 대한 복잡한 로직은 감추어져 있다. = 값 수준의 추상화

고차함수는 이 추상화의 수준을 사고의 추상화 수준으로 끌어올리는데, 위에 예시와 같은 함수를 전달받아 처리하는 수준이다.

  • 고차 함수 = 함수를 전달받거나 return 한다 = 사고(함수)에 대한 복잡한 로직은 감추어져 있다 = 사고 수준에서의 추상화

  • 추상화의 수준이 높아지는 만큼 생산성은 비약적으로 상승한다.



👀 사고 수준의 추상화?


const data = [
  {gender: 'male',age: 24,},
  {gender: 'male',age: 25,},
  {gender: 'female',age: 27,},
  {gender: 'female',age: 22,},
  {gender: 'male', age: 29,},
];

위와 같은 데이터를 순차적으로 처리할 때, 모든 작업을 함수로 작성할 수 있다. ex) 남성들의 평균 나이을 구한다.

function getAverageAgeOfMaleAtOnce(data) {
  const onlyMales = data.filter(function (d) {
    // data.filter는 배열의 각 요소에 인자로 전달받은 함수를 적용하고,
    // 그 결과가 true인 요소만을 갖는 배열을 리턴합니다.
    return d.gender === 'male';
  });

  const numOfMales = onlyMales.length;

  const onlyMaleAges = onlyMales.map(function (d) {
    // onlyMales.map는 배열의 각 요소에 인자로 전달받은 함수를 적용하고,
    // 각 결과를 요소로 갖는 배열을 리턴합니다.
    return d.age;
  });

  const sumOfAges = onlyMaleAges.reduce(function (acc, cur) {
    // onlyMaleAges.reduce는 배열의 각 요소에 인자로 전달받은 함수를 적용하고,
    // 각 결과를 두 번째 인자로 전달받은 초기값(0)에 누적한 결과를 리턴합니다.
    return acc + cur;
  }, 0);

  return sumOfAges / numOfMales;
}

console.log(getAverageAgeOfMaleAtOnce(data)); // 26

위의 getAverageAgeOfMaleAtOnce 함수는 남성의 평균 나이를 구하는 작업에서 사용할 수 있다.

그러나, male을 매개변수화하여 더 일반적인 함수로 변경가능하며 수정 후, 남성 및 여성의 평균나이를 구하는 작업을 수행할 수 있다.


한편, filter, map, reduce 등의 배열 메서드는 다른 목적을 위해서 사용될 수도 있다.(남성 중 최연소, 여성 중 최연소 등) 따라서, 이미 작성된 로직을 재사용할 수 있다.


추상화는 고차 함수를 통해, 보다 쉽게 달성할 수 있으며 아래의 compose 함수는 입력받은 함수를 순서대로 결합하는 고차 함수이다.

각각의 작업(filter, map, reduce)은 별도의 함수로 분리되어 compose의 인자로 전달되는 callback 함수가 된다.

function getOnlyMales(data) {
  return data.filter(function (d) {
    return d.gender === 'male';
  });
}

function getOnlyAges(data) {
  return data.map(function (d) {return d.age;
  });
}

function getAverage(data){
  const sum = data.reduce(function(acc, cur){
    return acc + cur;
  },0);
  return sum / data.length
}

function compose(...funcArgs){
  // compose는 여러 개의 함수를 인자로 전달받아 함수를 리턴하는 고차 함수입니다.
  // compose가 리턴하는 함수(익명 함수)는 임의의 타입의 data를 입력받아,
  return function(data){
    // funcArgs의 요소인 함수들을 차례대로 적용(apply)시킨 결과를 리턴합니다.
    let result = data;
    for(let i = 0; i < funcArgs.length; i++){
      result = funcArgs[i](result);
    }
    return result;
  };
}


// compose를 통해 함수들이 순서대로 적용된다는 것이 직관적으로 드러납니다.
// 각각의 함수는 다른 목적을 위해 재사용(reuse) 될 수 있습니다.
const getAverageAgeOfMale = compose(
  getOnlyMales, // 배열을 입력받아 배열을 리턴하는 함수
  getOnlyAges, // 배열을 입력받아 배열을 리턴하는 함수
  getAverage // 배열을 입력받아 `number` 타입을 리턴하는 함수
);

const result = getAverageAgeOfMale(data);
console.log(result); // --> 26

바로 위의 예시처럼 고차 함수를 통해 사고 수준에서의 추상화를 달성할 수 있고, 각각의 작업은 다른 목적으로 재사용될 수 있으므로 생산성을 높일 수 있다.




Reference:
코드스테이츠
MDN: https://developer.mozilla.org/ko/docs/Glossary/First-class_Function

0개의 댓글