함수형 프로그래밍(functional programming)은 순수 함수들을 조합하고, 공유 상태, 변경 가능한 데이터, 사이드 이펙트를 피하여 소프트웨어를 만드는 선언형(declarative) 프로그래밍이다.
어떤 함수가 '순수 함수'를 만족하려면 두가지 조건이 필요하다.
함수의 결과값은 입력값에 의해서만 결정되어야 한다. 그 외의 외부 상태에 의해 결과값이 결정되게 된다면, 이 함수를 쓸 때마다 해당 외부 상태의 변경 이력을 신경써야 한다.
다음의 예제를 보자.
let name = 'youjin';
function func(age) {
return `hi, ${name}! I'm ${age} years old`;
}
func(13); // 'hi, youjin! I'm 13 years old'
name = 'nittre';
func(13); // 'hi, moojin I'm 13 years old'
func(13)
을 두 번 호출했지만 각각의 호출이 다른 값을 리턴했다. 공유 상태(shared state) name
에 의해 함수 func()
의 결과값이 바뀌었기 때문이다. 따라서 우리가 func()
를 호출할 때마다 name
변수가 어떻게 변하고 있는지 함께 예측해야 한다. 이는 극도로 어려운 일이며, func()
가 어떻게 동작하는지 정확히 예측할 수 없게 만든다. 결과값을 예측할 수 없는 함수를 사용하는 것은 도박이나 다름없다.
위의 예제를 개선해보자.
let name = 'youjin';
function func2(nickname, age) {
return `hi, ${nickname}! I'm ${age} years old`;
}
func(name, 13); // 'hi, youjin! I'm 13 years old'
func('nittre', 13); // 'hi, nittre! I'm 13 years old'
func2()
에서는 인자로 받은 nickname
과 age
에 따라 결과값이 결정된다. 따라서 우리가 func2()
를 호출할 때는 결과값이 어떻게 나올지 예측할 수 있다.
순수함수에 대한 더 자세한 포스팅은 여기([JavaScript] Pure Function(순수 함수))를 참고
함수형 프로그래밍에서는 순수 함수들을 조합하여 고차함수를 만든다. 이 과정에서 불필요한 구문을 줄일 수 있으며, 기존에 선언한 함수들을 다시 사용하게 되어 함수의 재사용성이 증대된다.
함수를 조합하는 것이 가능한 이유는 자바스크립트의 일급 객체가 함수이기 때문이다. 다시 말해, 함수를 인자로 넘기거나 리턴할 수 있다.
함수 합성에 대한 더 자세한 포스팅은 여기([JavaScript] 함수 합성(Function Composition)를 참고
일급 객체에 대한 더 자세한 포스팅은 여기([JavaScript] 일급 객체, 고차 함수, 배열 메서드(내장 고차 함수))를 참고
명령형 프로그래밍
명령형 프로그래밍은 문제를 해결하기 위한 구체적인 단계들을 기술하는 방식으로 코드를 전개한다. 즉, 흐름 제어(flow control) 방식으로, 문제를 '어떻게 해결하는지' 위주로 작성한다.
따라서 명령형 코드에는 구문(statement)을 자주 사용한다. 구문은 어떤 동작을 수행하는 코드로, for
, if
, switch
, throw
등이 있다.
선언형 프로그래밍
선언형 프로그래밍은 데이터 흐름(data flow)를 기술하는 방식으로 코드를 전개한다. 데이터 흐름을 기술한다는 것은 흐름 제어를 추상화하는 것이다.
선언적 코드는 표현에 좀 더 중점을 둔다. 표현식(expression)은 어떤 값을 평가하는 코드 조각이다. 표현식은 함수 호출, 값, 결과값을 생성하기 위해 평가*되는 연산자 세가지의 조합이다. (함수는 추상화된 구문이기 때문에, 함수 호출이라는 표현식 위주로 코드를 전개하는 선언형 프로그래밍은 흐름 제어를 추상화하는 것과 같다.)
- '값을 평가한다'는 것은 쉽게 말해 '어떤 연산을 수행한다'는 것이다.
// 표현식 예제
2 * 2
doubleMap([2, 3, 4])
Math.max(4, 3, 2)
예제를 보자. 다음의 코드는 숫자 배열의 각 요소에 2를 곱한 새 배열을 반환하는 매핑을 명령형으로 전개한다.
// 명령형
const doubleMap = numbers => {
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
doubled.push(numbers[i] * 2);
}
return doubled;
};
다음의 코드는 명령형 매핑과 동일한 작업을 수행하지만, Array.prototype.map()
을 사용해 흐름 제어를 추상화하였다.
// 선언형
const doubleMap = numbers => numbers.map(n => n * 2);
결론적으로, 명령형의 흐름 제어 보다는 선언형 코드를 사용하자. '어떻게 하는지' 보다는 '무엇을 하는지'에 집중하자. 선언형 코드를 짜면 합성 함수를 적극적으로 활용하고 코드를 재활용할 수 있다. 또한 로직에 집중할 수 있게 된다.