First-class citizen
JavaScript에는 특별한 대우를 받는 일급 객체(first-class citizen)가 있다.
대표적인 일급 객체 중 하나가 함수이다.
함수는 다음과 같은 부분에서 특별하게 취급된다.
- 변수에 할당(assignment)할 수 있다.
- 다른 함수의 인자(argument)로 전달될 수 있다.
- 다른 함수의 결과로서 반환될 수 있다.
변수에 함수를 할당할 경우 주의해야 할 점은 호이스팅이 되지 않는다는 것이다.
하지만 호이스팅의 경우를 제외하면, 변수에 함수를 할당하는 함수 표현식이나 기존에 알고 있던 함수 선언식이나 크게 다르지 않다.
다만 함수 표현식의 경우 함수가 변수에 저장될 수 있다는 사실을 분명하게 보여준다.
What is higher order function?
고차함수는 함수를 인자(argument)로 받을 수 있고, 함수의 형태로 반환할 수 있는 함수이다.
마찬가지로, 함수 내부에서 변수에 함수를 할당할 수 있다.
그리고 함수는 함수 자체를 반환할 수도 있다.
이 때 다른 함수(caller)의 인자(argument)로 전달되는 함수를 callback function이라고 한다.
콜백 함수의 이름은, 어떤 작업이 완료되었을 때 호출하는 경우가 많아서, 답신 전화를 뜻하는 콜백이라는 이름이 붙여졌다.
이러한 콜백 함수를 전달받은 고차함수는 함수 내부에서 이 콜백 함수를 호출(invoke)할 수 있다.
caller는 조건에 따라 콜백 함수의 실행 여부를 결정할 수 있다.
호출하지 않을 수도 있고 여러 번 실행할 수도 있다.
'함수를 반환하는 함수'는 모양새가 특이한 만큼, 부르는 용어가 따로 있다.
'함수를 반환하는 함수'를 고안해 낸 논리학자 Haskell Curry의 이름을 따서 커리함수라고 한다.
그러나 정확하게 구분하자면, 고차함수가 커리함수를 포함한다.
- 다른 함수를 인자로 받는 경우
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
- 함수를 반환하는 경우
function adder(added) {
return function (num) {
return num + added;
};
}
let output = adder(5)(3); // -> 8
console.log(output); // -> 8
// 함수는 일급 객체이기 때문에 adder가 반환하는 함수를 변수에 저장할 수 있다.
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(5, double)(3); // -> 13
// doubleAdder가 리턴하는 함수를 변수에 저장할 수 있다. (일급 객체)
const addTwice3 = doubleAdder(3, double);
addTwice3(2); // --> 8
Built-in higher order functions (내장 고차함수)
JavaScript에는 기본적으로 내장된 고차함수가 여럿 있다.
그중에서도 배열 메소드 중 일부가 대표적인 고차함수에 해당한다.
filter
배열의 filter 메소드는 모든 배열의 요소 중에서 특정 조건을 만족하는 요소를 걸러내는 메소드이다.
예를 들어 배열에서 짝수만을 걸러내거나, 길이가 10 이하인 문자열을 걸러낼 수 있다.
이 때 명심해야 할 것이, 걸러내는 특정 조건이 함수의 형태 라는 점이다.
filter 메소드는 걸러내기 위한 조건을 명시한 함수를 인자로 받기 때문에 고차함수라고 볼 수 있다.
filter 메소드는 배열의 요소를 인자로 전달되는 콜백 함수에 다시 전달한다.
콜백 함수는 배열의 요소를 받아서 함수를 실행하고, 콜백 함수 내부의 조건에 따라 true 또는 false를 반환한다.
이후 true인 값들을 다시 배열에 담아서 반환한다.
map과 마찬가지로 기존 배열을 수정하지 않는다.
const isEven = function (num) {
return num % 2 === 0;
};
let arr = [1, 2, 3, 4];
let output = arr.filter(isEven);
console.log(output); // ->> [2, 4]
const isLteFive = function (str) {
return str.length <= 5;
};
let arr = ["hello", "code", "states", "happy", "hacking"];
let output = arr.filter(isLteFive);
console.log(output); // ->> ['hello', 'code', 'happy']
const cartoons = [
{
id: 1,
bookType: "cartoon",
title: "식객",
subtitle: "어머니의 쌀",
createdAt: "2003-09-09",
genre: "요리",
artist: "허영만",
averageScore: 9.66,
},
{
id: 2,
// .. 이하 생략
},
// ... 이하 생략
]; // 단행본의 모음
const isCreatedAt2003 = function (cartoon) {
const fullYear = new Date(cartoon.createdAt).getFullYear();
return fullYear === 2003;
}; // 단행본 한 권의 출판판년도가 2003인지 확인하는 함수
const filteredCartoons = cartoons.filter(isCreatedAt2003); // 출판년도가 2003년인 책의 모음
map
배열의 모든 요소에 동일한 함수를 실행한 뒤 그 값을 반환한다.
이 때 기존 배열은 수정되지 않는다.
let arr = [1, 2, 3];
let result = arr.map(function (el) {
return el * 2;
});
console.log(result); // ->> [2, 4, 6]
map은 하나의 데이터를 다른 데이터로 mapping 할 때 사용된다.
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
배열의 하나의 누적값으로 만든다.
reduce는 초기값을 설정할 수 있는데, 만약 설정하지 않는다면 배열의 0번째 값이 초기값이 된다.
이후 배열의 끝까지 반복하면서 초기값과 현재값을 가지고 지정한 함수를 실행한다.
let arr = [1, 2, 3];
let result = arr.reduce(function (acc, cur, idx) {
acc += cur;
return acc;
});
//result = 6
초기값을 지정해 줄 경우, 초기값부터 시작하고 배열의 0번째 값이 첫번째 현재값이 된다.
let arr = [1, 2, 3];
let result = arr.reduce(function (acc, cur, idx) {
acc += cur;
return acc;
}, 1);
//result = 7
이외에도 가장 작은 숫자를 구하거나, 배열을 객체로 만들거나 하는 작업이 가능하다.
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;
}; // 단행본 한 권의 평점을 누적값에 더한다.
let initialValue = 0; // 숫자의 형태로 평점을 누적한다.
const cartoonsAvgScore =
cartoons.reduce(scoreReducer, initialValue) / cartoons.length;
// 모든 책의 평점을 누적한 평균을 구한다.
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 }
];
let newAddressBook = users.reduce(makeAddressBook, {});
newAddressBook의 값 :
{
T: [
{ name: 'Tim', age: 40 }
],
S: [
{ name: 'Satya', age: 30 },
{ name: 'Sundar', age: 50 }
]
}
Why higher order function?
왜 고차함수를 써야하는지에 대해 생각해보려면 우선 추상화(abstraction)에 대해 이해해야 한다.
추상화는 복잡한 어떤 것을 압축해서 핵심만 추출한 상태로 만드는 작업 이다.
우리가 살아가는 세상은 추상화로 가득 차 있다.
'-1'을 표현하는 현실의 방법은 존재하지 않지만 우리는 '-1'이라는 문자를 보고 -1은 0보다 1만큼 작은 수라고 설명할 수 있다.
이렇듯 추상화를 이용하면 효율적이고 편하게 생각할 수 있다.
브라우저 창에 주소를 입력했을 때 어떤 일이 일어나는지 정확하게 알 수 있는가?
입력한 내용을 전파하고, 어디 서버로 갔다가 다른 서버로 가는 등 복잡한 내용을 일상생활에서는 몰라도 된다.
우리는 그저 주소창에 올바른 주소를 입력하면 브라우저가 해당 사이트를 보여 준다는 것만 알아도 된다.
스마트폰으로 카카오톡이나 페이스북 메신저를 통해 친구에게 'ㅇㅇ'이라는 메세지를 보내면 그 순간 스마트폰은 기지국과 약 20개의 메세지를 주고 받는다.
하지만 우리들은 이런 것들을 전부 알지 못하고 알 필요도 없다.
그저 입력창에 메세지를 입력하고 전송 버튼을 누르면 내 친구가 메세지를 받는다는 사실만 알고 있으면 된다.
자동차의 시동 버튼, 자료를 정리하는 엑셀, 교통비를 지불하는 교통 카드도 추상화의 결과이다.
일상생활에서 추상화가 아닌 것을 찾아보기 힘들 정도이다.
JavaScript를 비롯한 많은 프로그래밍 언어 역시 추상화의 결과이다.
컴퓨터를 구성하는 장치는 0과 1만 이해한다.
하지만 JavaScript의 syntax를 올바르게 사용하는 것만으로, 다양한 프로그램을 쉽게 작성할 수 있다.
따라서 추상화는 생산성(productivity)의 향상 이라고 볼 수 있다.
한편 프로그램을 작성할 때 자주 반복해서 사용하는 로직은 별도의 함수로 작성하기도 한다.
이 역시 추상화의 좋은 사례이다.
추상화의 관점에서 함수를 바라보면 함수는 사고(thought) 또는 논리(logic)의 묶음이다.
우리는 JavaScript의 함수를 간단하게 구현해내면서도 복잡한 로직은 신경 쓸 필요가 없다.
일반함수
- 함수 = 값을 전달받아 값을 반환 = 값에 대한 로직은 감추어져 있음 = 값 수준에서의 추상화
- 값 수준의 추상화 : 단순히 값(value)을 전달받아 처리하는 수준
고차함수
- 고차함수 = 함수를 전달받거나 함수를 반환 = 사고(함수)에 대한 복잡한 로직은 감추어져 있음 = 사고 수준에서의 추상화
- 사고의 추상화 : 함수(사고의 묶음)를 전달받아 처리하는 수준
추상화의 수준이 높아지는 만큼, 생산성도 비약적으로 상승한다.
사고 수준의 추상화 예시
const data = [
{
gender: "male",
age: 24,
},
{
gender: "male",
age: 25,
},
{
gender: "female",
age: 27,
},
{
gender: "female",
age: 22,
},
{
gender: "male",
age: 29,
},
];
위와 같이 주어진 데이터를 순차적으로 처리하려고 할 때, 모든 작업을 하나의 함수로 작성할 수 있다.
다음은 남성들의 평균 나이를 구하는 예시이다.
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;
}
위 코드는 꽤 괜찮은 코드이지만 남성의 평균 나이를 구하는 작업에만 사용할 수 있다.
'male'을 매개변수화(parameterization)하여 조금 더 일반적인(generic) 함수로 변경할 수 있다.
하지만 이렇게 해도 어디까지나 남성 또는 여성의 평균 나이를 구하는 작업만 수행할 수 있다.
한편, filter, map, reduce 등의 배열 메소드를 사용하면 남성 중 최연소 나이를 구하거나 여성 중 최연소 나이와 최연장 나이의 차이를 구하는 로직을 만들어 볼 수도 있다.
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
이처럼 고차함수를 통해 사고 수준에서의 추상화를 달성할 수 있다.
각각의 작업은 다른 목적을 위해 재사용될 수도 있다.
'JavaScript > Vanilla' 카테고리의 다른 글
class, 그리고 JS의 객체 지향 (0) | 2021.07.27 |
---|---|
DOM (Document Object Model) (0) | 2021.07.24 |
객체의 얕은 복사와 깊은 복사 (0) | 2021.07.24 |
Hoisting (0) | 2021.07.24 |
Spread와 Rest 문법 (0) | 2021.07.24 |