[S2U1] JS 고차함수

👽·2024년 2월 1일
0
post-thumbnail

CH1. 고차 함수

📌 일급 객체 (first-class citizen)

🔸 JavaScript에서 대표적인 일급 객체 중 하나인 함수는 아래와 같이 특별하게 취급됨.
🔸 변수에 할당(assignment) 가능

  • 함수를 배열의 요소나 객체의 속성 값으로 저장 가능.
  • 함수를 데이터(string, number, boolean, array, object)처럼 다룰 수 있음.

🔸 다른 함수의 전달인자(argument)로 전달 가능.
🔸 다른 함수의 결과로써 리턴 가능.

📌 고차 함수의 이해

🔸 고차 함수 (higher order function) : 함수를 전달인자(argument)로 받을 수 있고, 함수를 리턴할 수 있는 함수.
🔸 콜백 함수 (callback fuction) : 다른 함수(caller)의 전달인자로 전달되는 함수.

  • 콜백 함수를 전달받은 고차함수(caller)는 함수 내부에서 콜백 함수를 호출(invoke)할 수 있음.

🔸 커링 함수 (curring function) : 함수를 리턴하는 함수 (고차함수가 커링함수를 포함함)

1. 다른 함수를 인자로 받는 경우

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

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

// 아래와 같은 경우, 함수 double은 함수 doubleNum의 콜백 함수.
let output = doubleNum(double, 4);
console.log(output); // -> 8

2. 함수를 리턴하는 경우

// adder는 인자 한 개를 입력받아서 함수(익명 함수)를 리턴
function adder(added) {   // 1. added = 5
  return function (num) { // 3. num = 3
    return num + added;   // 2. num + 5; 4. 3 + 5
  };
}
let output = adder(5)(3); // -> 8
console.log(output); // -> 8

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

function double(num) { // 3. num = 5
  return num * 2;      // 4. 5 * 2 = 10
}

function doubleAdder(added, func) { // 1. added = 5; func = f double
  const doubled = func(added);      // 2. doubled = double(5) ; 10
  return function (num) {           // 5. num = 3     // 
    return num + doubled; 			// 6. 3 + 10 ; 13 // 
  };
}
doubleAdder(5, double)(3) // 13

const addTwice3 = doubleAdder(3, double); // 6
addTwice3(2); // 8
// doubleAdder가 리턴하는 함수를변수에 저장할 수 있음.

CH2. 내장 고차 함수

📌 내장 고차 함수의 이해

🔸 JavaScript에는 기본적으로 내장된 고차 함수가 여러개 있으며, 그 중 배열 메서드들 중 일부가 대표적인 고차 함수에 해당함.

📌 filter 메서드

🔸 Array.filter(callbackFn) : 모든 배열의 요소 중에서 특정 조건을 만족하는 요소를 걸러내는 메서드.

  • callbackFn : 걸러내는 기준이 되는 특정 조건으로 함수의 형태임.
  • 따라서 filter 메서드는 함수를 전달인자로 받는 고차 함수.

🔸 filter 메서드는 배열의 각 요소를 콜백 함수에 전달.
🔸 콜백 함수는 전달받은 배열의 요소를 받아 함수를 실행하고, 콜백 함수 내부의 조건에 따라 참(true) 또는 거짓(false)을 리턴함.

🔸 조건에 맞는 데이터만 분류(filtering) 할 때 사용.

실제 filter 활용 예시

  1. 배열의 각 요소가
  2. 특정 논리(함수)에 따르면,
  3. 사실(true)일 때 따로 분류함(filter).
// 단행본 모음
const cartoons = [
  {
    id: 1,
    bookType: 'cartoon',
    title: '식객',
    subtitle: '어머니의 쌀',
    createdAt: '2003-09-09',
    genre: '요리',
    artist: '허영만',
    averageScore: 9.66,
  },
  {
    id: 2,
    // .. 이하 생략
  },
  // ... 이하 생략
]; 
// 단행본 한 권의 출판 연도가 2003인지 확인하는 함수
const isCreatedAt2003 = function (cartoon) {
  const fullYear = new Date(cartoon.createdAt).getFullYear()
  return fullYear === 2003;
}; 
// 출판 연도가 2003년인 책의 모음
const filteredCartoons = cartoons.filter(isCreatedAt2003); 

📌 map

🔸 map() : 배열 내의 모든 요소 각각에 대하여 주어진 함수를 호출한 결과를 모아 새로운 배열을 반환.
🔸 하나의 데이터를 다른 데이터로 매핑(mapping)할 때 사용.
🔸 기존 배열을 변경하지 않음.

실제 map 활용 예시

  1. 배열의 각 요소가
  2. 특정 논리(함수)에 의해
  3. 다른 요소로 지정됨(map).
// 만화책 모음
const cartoons = [
  {
    id: 1,
    bookType: 'cartoon',
    title: '식객',
    subtitle: '어머니의 쌀',
    createdAt: '2003-09-09',
    genre: '요리',
    artist: '허영만',
    averageScore: 9.66,
  },
  {
    id: 2,
    // .. 이하 생략
  },
  // ... 이하 생략
]; 
// 만화책 한 권의 부제를 리턴하는 로직(함수)
const findSubtitle = function (cartoon) {
  return cartoon.subtitle;
}; 
// 각 책의 부제 모음 
const subtitles = cartoons.map(findSubtitle); // ['어머니의 쌀', ...]

📌 reduce (acc, init)

🔸 Array.reduce(callback[, initialValue]) : 배열의 각 요소에 대해 주어진 리듀서(reducer) 함수를 실행하고, 하나의 결과값을 반환.

  • initialValue : callback의 최초 호출에서 첫 번째 인수에 제공하는 값. 초기값을 제공하지 않으면 배열의 첫 번째 요소를 사용함.

🔸 여러 데이터를, 하나의 데이터로 응축(reduce)할 때 사용.
🔸 리듀서(reducer) 함수는 네 개의 인자를 가짐.

  • 누산기 (accumulator) : 콜백의 반환값 누적. 콜백의 이전 반환값 또는, 콜백의 첫 번째 호출이면서 initialValue를 제공한 경우에는 initialValue의 값.
  • 현재 값 (currentValue) : 처리할 현재 요소.
  • 현재 인덱스 (currentIndex) : 처리할 현재 요소의 인덱스. (initialValue를 제공한 경우 0, 아니면 1부터 시작)
  • 원본 배열 (src)
function (accumulator, currentValue, currentIndex, array) {
    return accumulator + currentValue;
  }

🔸 리듀서 함수의 반환 값은 누산기(acc)에 할당.

실제 reduce 활용 예시

  1. 배열의 각 요소를
  2. 특정 방법(함수)에 따라
  3. 원하는 하나의 형태로
  4. 응축합니다. (reduction)
// 단행본 모음
const cartoons = [
  {
    id: 1,
    bookType: 'cartoon',
    title: '식객',
    subtitle: '어머니의 쌀',
    createdAt: '2003-09-09',
    genre: '요리',
    artist: '허영만',
    averageScore: 9.66,
  },
  {
    id: 2,
    // .. 이하 생략
  },
  // ... 이하 생략
];
// 단행본 한 권의 평점을 누적값에 더한다.
const scoreReducer = function (sum, cartoon) {
  return sum + cartoon.averageScore;
}; 
// 초기값에 0을 주고, 숫자의 형태로 평점을 누적한다.
let initialValue = 0 
// 모든 책의 평점을 누적한 평균을 구한다.
const cartoonsAvgScore = cartoons.reduce(scoreReducer, initialValue) / cartoons.length;

reduce의 색다른 사용법

🔸 배열을 문자열로

  • 유저 정보 배열 안에 있는 요소의 이름을 하나로 응축
function joinName(resultStr, user) {
  resultStr = resultStr + user.name + ', ';
  return resultStr;
}
let users = [
  { name: 'Tim', age: 40 },
  { name: 'Satya', age: 30 },
  { name: 'Sundar', age: 50 }
];
users.reduce(joinName, ''); // 'Tim, Satya, Sundar'
호출 횟수resultStruser리턴 값
1번째 호출'''Tim''Tim'
2번째 호출'Tim''Satya''Tim, Satya'
n번째 호출'Tim, Satya''Sundar''Tim, Satya, Sundar'

🔸 배열을 객체로

  • 유저 정보 배열 안에 있는 요소로 주소록을 만듦.
function makeAddressBook(addressBook, user) {
  let firstLetter = user.name[0];
  if(firstLetter in addressBook) {
    addressBook[firstLetter].push(user);
  } else {
    addressBook[firstLetter] = [];
    addressBook[firstLetter].push(user);
  }
  return addressBook;
}
let users = [
  { name: 'Tim', age: 40 },
  { name: 'Satya', age: 30 },
  { name: 'Sundar', age: 50 }
];
users.reduce(makeAddressBook, {});
// {
	T: [{ name: 'Tim', age: 40}],
    S: [ { name: 'Satya', age: 30 },
        { name: 'Sundar', age: 50 }]
   }
호출 횟수addressBookuser리턴 값
1번째 호출{}{ name: 'Tim', age: 40 }{T: [{ name: 'Tim', age: 40 }]}
2번째 호출{T: [{ name: 'Tim', age: 40 }]}{ name: 'Satya', age: 30 }{T: [{ name: 'Tim', age: 40 }], S: [{ name: 'Satya', age: 30 }]}
n번째 호출{T: [{ name: 'Tim', age: 40 }], S: [{ name: 'Satya', age: 30 }]}{ name: 'Sundar', age: 50 }{T: [{ name: 'Tim', age: 40 }], S: [{ name: 'Satya', age: 30 }, { name: 'Sundar', age: 50 }]}

CH3. 고차 함수의 중요성

📌 고차 함수를 쓰는 이유

🔸 추상화(abstraction) : 복잡한 어떤 것을 압축해서 핵심만 추출한 상태로 만드는 것으로 추상화로 인해 생산성(producticity)이 향상됨.

예시)
브라우저 창에 주소를 입력했을 때, 입력한 내용을 전파하고, 어디 서버로 갔다가 다른 서버로 가는 등 그런 복잡한 내용을, 일상생활에서는 몰라도 됨. 우리는 그저 주소창에 올바른 주소를 입력하면, 브라우저가 해당 사이트를 보여 준다는 것만 알고 있음. 이런 것이 추상화.

🔸 자주 반복해서 사용하는 로직은 별도의 함수로 작성는 것은 추상화의 사례로 볼 수 있음. 추상화의 관점에서 함수를 바라보면, 함수는 사고(thought) 또는 논리(logic)의 묶음.
🔸 함수를 통해 얻은 추상화를 한 단계 더 높인 것이 고차함수.

  • 함수 = 값을 전달받아 값을 리턴 = 값에 대한 복잡한 로직은 감춰져 있음 = 값 수준에서의 추상화

🔸 고차 함수는 이 추상화의 수준을 사고의 추상화 수준으로 끌어올림.

  • 값 수준의 추상화: 단순히 값(value)을 전달받아 처리하는 수준.
  • 사고의 추상화: 함수(사고의 묶음)를 전달받아 처리하는 수준.

🔸 고차 함수를 통해, 보다 높은 수준(higher order)에서 생각할 수 있음.

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

📌 고차 함수와 추상화

사고 수준의 추상화의 예시

const data = [
  {
    gender: 'male',
    age: 24,
  },
  {
    gender: 'male',
    age: 25,
  },
  {
    gender: 'female',
    age: 27,
  },
  {
    gender: 'female',
    age: 22,
  },
  {
    gender: 'male',
    age: 29,
  },
];
  • 예를 들어, 위의 데이터에서 남성들의 평균 나이를 구한다고 했을 때, 다음과 같이 함수를 작성할 수 있음
function getAverageAgeOfMale(data) {
	const onlyMales = data.filter(function (d) {
    	return d.gender === "male"
    });
    const numOfMales = onlyMales.length
    const onlyMaleAges = onlyMales.map(function (d) {
    	return d.age;
    })
    const sumOfAges = onlyMales.reduce(function (acc, cur) {
    	return acc + cur
    }, 0)
    return sumOfAges / numOfMales;
}
  • getAverageAgeOfMale 함수는 배열 메서드(filter, map, reduce)를 적절하게 사용하여 순차적으로 원하는 작업을 수행.
  • 해당 코드는 '남성'의 '평균 나이'만 구하는 작업에만 사용할 수 있음.
  • 개선하자면, 'male'을 매개변수화 하여 조금 더 일반적인 함수로 변경할 수 있음.
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
profile
코린이👽

0개의 댓글