Function
정의
함수 란?
()
연산자를 적용하여 평가할 수 있는 모든 호출 가능 표현식- 호출시 사용 가능한 결과값을 반환
자, void 를 반환하는 함수를 우리는 앞으로 함수로 쳐주지 않기로 한다.
그럼 뭐라고 부르지?
일급 시민
Javascript에서는 함수가 객체이므로 값으로 다룰 수 있어서 일급 또는 일급 시민 이라고 부른다.
보통 프로그래밍에서 일급이라고 함은, 적용 가능한 모든 연산을 수행할 수 있는 것들을 일급 이라고 부른다.
(어렵다...?)
그럼 그러한 연산은 뭐가 있을까?
- 값을 할당하거나
- 함수의 인자로 전달하거나
- 함수의 반환값으로 사용하거나
보통 위와 같은 케이스를 말한다.
그럼 Javascript에서 이러한 연산을 어떻게 수행하나 한번 보자.
먼저 값을 할당하거나
const add = function(a, b) {
return a + b;
};
// Or
// const add = (a, b) => a + b;
add(1, 2); // 3
함수의 인자로 전달하거나
function calculator(fn, a, b) {
return fn(a, b);
}
calculator(add, 1, 2); // 3
함수의 반환값으로 사용하거나
function highOrderFn(fn) {
return function(a, b) {
return fn(a, b);
};
}
const join = highOrderFn(add);
join('hello', 'world'); // helloworld
음, 일급 칭호를 받을 만 하군.
고차함수
바로 위 예제 코드 중 calculator
와 highOrderFn
함수를 정의했는데 바로 이게 고차함수(High-order function) 이다.
즉, 함수를 인수로 전달받거나 반환받을 수 있다면 그게 바로 고차함수다.
고차함수, 고계함수 둘 다 같은 의미다.
이 고차함수를 사용하면 과연 이득이 뭘까?
바로 함수를 전달하고, 반환받는 방식을 통해 굉장히 유연한 모듈성을 갖춘 코드를 작성할 수 있게 된다.
역시 예제로 보는게 제일 좋겠다. 명령형 방식과 한번 비교해보자.
고객이 우리에게 요구사항을 전달했다.
미국 거주자 명단을 출력해주세요.
우리가 가지고 있는 데이터는 아래와 같다.
class Person {
constructor(name, country) {
this.name = name;
this.country = country;
}
}
const people = [
new Person('huna', 'Korea'),
new Person('Martin Fowler', 'UK'),
new Person('James Arthur Gosling', 'Canada'),
new Person('Douglas Crockford', 'USA'),
new Person('Yukihiro Matsumoto', 'Japan'),
new Person('Ryan Dahl', 'USA')
];
명령형 방식을 먼저 보면
function printPeopleInTheUs(people) {
for (let i = 0; i < people.length; i++) {
const thisPerson = people[i];
if (thisPerson.country === 'USA') {
console.log(thisPerson);
}
}
}
printPeopleInTheUs(people);
// Person { name: 'Douglas Crockford', country: 'USA' }
// Person { name: 'Ryan Dahl', country: 'USA' }
루프를 돌면서 값들을 하나씩 뽑아 USA
데이터를 출력하면 끝~
이것도 나쁘지 않은데?
그렇다. 나쁘지 않다. 이대로 고객에게 전달해주자.
음, 생각이 바꼇어요. 다른 나라 거주자도 볼 수 있으면 좋겠어요.
자, 이제 고차함수를 통해서 좀 더 유연하게 바꿔보자.
고차함수를 사용할 수 있게 요구사항을 바꾼 고객에게 박수를 🤬
function printPeople(people, action) {
for (let i = 0; i < people.length; i++) {
action(people[i]);
}
}
function action(person) {
if (person.country === 'USA') {
console.log(person);
}
}
printPeople(people, action);
// Person { name: 'Douglas Crockford', country: 'USA' }
// Person { name: 'Ryan Dahl', country: 'USA' }
고객의 요구사항에 맞게 action
함수를 인자로 변경해서 호출하면 된다.
근데 이정도로 바꾸려고 고차함수를 쓴다면 좀 허접하지 않는가?
일단 고객에게 보내보자.
잠시 후...
저희는 콘솔 말고 파일에도 저장이 가능하면 좋겠어요
좀 더... 야근을 해보자...
function printPeople(people, selector, printer) {
people.forEach(person => selector(person) && printer(person));
}
const inUs = person => person.country === 'USA';
printPeople(people, inUs, console.log);
// Person { name: 'Douglas Crockford', country: 'USA' }
// Person { name: 'Ryan Dahl', country: 'USA' }
이제 printPeople
함수에 데이터, 조건, 출력
순서로 인자를 전달받아 처리할 수 있도록 개선이 되었다.
고차함수를 통해 대충 함수 호출부분만 봐도 어떤 일을 하는지 짐작이 가지 않는가?
다행히 고객도 만족스럽다고 연락이 왔다.
하지만 우리는 끝을 모르는 개발자.
리팩토링을 할 시간이다.
이번 리팩토링에는 커링(currying) 이라는 FP의 꽃 기능을 소개하려고 한다.
자, 놀라지 마시라.
const curry = (fn, arity = fn.length, ...args) =>
arity <= args.length ? fn(...args) : curry.bind(null, fn, arity, ...args);
const inCountry = curry((country, person) => person.country === country);
people.filter(inCountry('USA')).map(console.log)
결과는 처음과 좀 다르지만 (Array.prototype.map() 함수는 (value, index, array) 를 인자로 받는다)
마지막 함수를 보면 체이닝을 통해 함수들을 아주 왕성하게 주고받는 코드를 볼 수 있을 것이다.
또한 go
라는 함수를 통해 함수 합성으로 구현도 가능하다.
이 함수는 유인동 님의 강의에서 감명깊게 본 코드이다.
단, go
를 사용하기 위해선 Ramda와 같은 currying이 된 함수형 라이브러리가 필요하다.
const go = (v, ...fn) => fn.reduce((a, f) => f(a), v);
go(
people,
R.filter(inCountry('USA')),
R.map(console.log)
);
curry
, go
함수에 대해서는 나중에 따로 포스팅을 한번 할 예정이니 그때까지 천천히 코드를 음미해보길 바란다.
출처: 함수형 자바스크립트