※ 함수형 자바스크립트 라는 책을 한 장씩 리뷰해보도록 하겠습니다.
Chapter 3. 자료구조는 적게, 일은 더 많이
"계산 프로세스는 컴퓨터에 내재하는 추상적인 존재다. 이들이 점점 진화하면서 프로세스는 데이터라는 또 다른 추상적인 존재에 영향을 끼친다."
3장에서는, 이전 장에서 함수를 순수하게 만드는 법에 대해 알아보았으니 이것들을 서로 연결하는 방법을 알아보겠습니다. 자료구조를 순차적으로 탐색/변환하는 데 쓰이는 실용적인 연산들 (map, reduce, filter 등등) 과 로대시JS 에 대해 볼 것입니다. 이런 도구들을 통해 메인 로직에서 제어 흐름을 명확하게 분리하여 간결하고 확장성 좋은 선언적 프로그램을 작성할 수 있습니다.
3.1 어플리케이션의 제어 흐름
프로그램이 정답에 이르기까지 거치는 경로를 제어흐름이라고 합니다. 명령형 프로그램은 작업 수행에 필요한 전 단계를 노출하여 흐름이나 경로를 서술하며, 분기 또는 루프에 따라 움직이는 일련의 연산들로 구성됩니다.
반면 선언적 프로그램 (함수형 프로그램) 에선 서로 연결된 블랙박스 연산을 제어합니다. 데이터는 한 연산에서 다른 연산으로 (각각 개별적 순수함수) 독립적으로 흘러가며, 분기와 반복은 상당 부분 없애고 고수준의 추상화로 대체합니다.
3.2 메서드 체이닝
메서드 체이닝은 여러 메서드를 단일 구문으로 호출하는 OOP 패턴입니다. 메서드가 모두 동일한 객체에 속해 있으면 메서드 흘리기라고도 합니다. OOP 패턴에서 문자열을 다루는 예시를 보겠습니다.
'Functional Programing'.substring(0, 10).toLowerCase() + ' is fun';
substring, toLowerCase 메서드 각각 자신을 소유한 문자열 객체에 접근해 새로운 문자열로 반환합니다. 기존의 문자열을 건드리지않으니 함수형 프로그래밍과도 잘 맞는 패턴입니다. 이를 함수형으로 리팩토링 해보겠습니다.
concat(toLowerCase(substring('Functional Programing', 1, 10))), 'is Fun');
매개변수는 모두 함수 선언부에 명시해서 부수효과를 없애고 원본 객체를 바꾸지 않아야 한다는 함수형 패러다임을 잘 반영했지만, 위의 메서드 체이닝 방식보다는 매끄럽지 못합니다.
3.3 함수 체이닝
함수형 프로그래밍에선, 자료구조를 새로 만들어 어떤 요건을 충족시키는게 아닌 흔한 자료구조 (배열 등) 를 이용해 다수의 굵게 나뉜 고계 연산을 적용합니다. 이러한 고계 연산으로 아래와 같은 일을 합니다.
- 작업을 수행하기 위해 무슨 일을 해야 하는지 기술된 함수를 인수로 받습니다.
- 임시 변수의 값을 계속 바꾸면서 부수효과를 일으키는 기존 수동 루프를 대체합니다.
3.3.1 람다 표현식
함수형 프로그래밍에서 탄생한 람다 표현식 (자바스크립트의 두 줄 화살표 함수) 은 한 줄짜리 익명함수를 일반함수 선언보다 단축구문으로 나타냅니다. 아래 예시)
const name = p => p.name;
함수형 프로그래밍은 람다 표현식과 잘 어울리는 주요 고계함수 (map, reduce, filter) 의 사용을 적극 권장합니다.
함수형 자바스크립트는 대부분 자료 리스트를 처리하는 코드이기에 이를 사용할 일이 많으며, 추가로 여러 연산을 포함하는 로대시JS 사용을 추천합니다. 아래에서 로대시JS 함수들을 살펴보겠습니다.
3.3.2 _.map : 데이터를 변환
map 은 배열 각 원소에 이터레이터 함수를 적용하여 크기가 같은 새 배열을 반환하는 고계함수입니다. 아래 예시)
_.map(persons,
s => (s !== null && s !== undefined) ? s.fullname : ''
); // persons 배열의 각 원소를 돌며 null, undefined 가 아닐시 fullname 반환한 리스트를 생성
map 함수는 컬렉션의 원소를 전부 파싱할 경우 유용하며, 함수 f 와 n 개의 원소가 담긴 컬렉션을 받아 왼쪽 --> 오른쪽 방향으로 각 원소에 f 를 적용한 계산 결과를 크기가 n 인 새 배열에 담아 반환하므로 불변성도 간직됩니다.
// map 구현부
function map(arr, fn) {
const len = arr.length,
result = new Array(len);
for (let idx = 0 ; idx < len ; ++idx) {
result[idx] = fn(arr[idx], idx, arr);
}
return result;
}
// _.map 안에서도 일반 루프를 사용
// _.map 이 반복을 대행하므로 개발자는 이터레이터 함수에 구현한 비즈니스 로직만 신경쓰면 됨
3.3.3 _.reduce : 결과를 수집
reduce 는 원소 배열을 하나의 값으로 짜내는 고계함수로, 원소마다 함수를 실행한 결과값의 누적치를 계산합니다.
아래 예시)
_(persons).reduce((stat, person) => {
const country = person.address.country;
stat[country] = _.isUndefined(stat[country]) ? 1 : stat[country] + 1;
return stat;
}, {}); // persons 배열을 순회하며 {} 빈 객체에 국가별 거주사람 수 누적치 반환
// reduce 구현부
function reduce(acc, fn, accumulator) {
let idx = -1,
len = arr.length;
if (!accumulator && len > 0) {
accumulator = arr[++idx];
}
while (++idx < len) {
accumulator = fn(accumulator, arr[idx], idx, arr);
}
return accumulator;
}
reduce 는 다음 매개변수를 받습니다.
- fn : 배열 각 원소마다 실행할 이터레이터 함수로, 매개변수는 누산치, 현재 값, 인덱스, 배열입니다.
- accumulator : 계산할 초기값으로 넘겨받는 인자이며, 매 함수 호출 시 계산된 결과값을 저장하는데 쓰입니다.
또한 reduce 는 일괄 적용 연산이라 배열을 순회하는 도중 그만두고 나머지 원소를 생략할 수 없습니다.
3.3.4 _.filter : 원하지 않는 원소를 제거
filter 는 배열 원소를 반복하면서 술어 함수 p 가 true 를 반환하는 원소만 추려내고 그 결과를 새 배열에 담아 반환하는 고계함수 입니다. 아래 예시)
const bornIn1903 = person => person.birthYear === 1903;
_(persons).filter(bornIn1903).map(fullname).join(' and ');
// person 컬렉션에서 1903년 출생자들만 추려서 반환
// filter 구현부
function filter(arr, predicate) {
let idx = -1,
len = arr.length,
result = [];
while (++idx < len) {
let value = arr[idx];
if (predicate(value, idx, this)) {
result.push(value);
}
}
return result;
}
3.4 코드 헤아리기
'코드를 헤아린다' 는 뜻은, 프로그램의 일부만 들여다봐도 무슨일을 하는 코드인지 멘털 모델 (전체 변수의 상태와 함수 출력 같은 동적인 부분 뿐만 아니라 설계 가독성 및 표현성 같은 정적인 측면도 포함하는 개념) 을 쉽게 구축할 수 있다는 것 입니다.
3.4.1 선언적 코드와 느슨한 함수 체인
함수형 프로그래밍에선 map, reduce, filter 와 같은 구성요소를 바탕으로 순수함수를 쌓아가면 자연스레 한눈에 들어오는 코드가 완성됩니다. 이정도의 추상화가 되면 기반 자료구조에 영향을 끼치지않는 방향으로 연산을 바라볼 수 있으며, 따라서 함수형 프로그래밍은 자료구조보다 연산에 더 중점을 둡니다. 로대시JS 를 사용해 연산에 초점을 둔 코드를 보겠습니다.
_.chain(name)
.filter(isValid)
.map(s => s.replace(/_/, ' '))
.uniq()
.map(_.startCase)
.sort()
.value();
_.chain 함수는 주어진 입력을 원하는 출력으로 변환하는 연산들을 연결함으로써 입력 객체의 상태를 확장합니다. 또한 _.chain 을 사용하면 복잡한 프로그램을 느긋하게 작동시키는 장점도 있습니다. value() 함수를 호출 전까지는 아무것도 실행하지 않기 때문입니다. 이처럼 프로그램 파이프라인을 느긋하게 정의하면 가독성을 비롯해 자료구조를 재사용하거나 메서드를 융합하여 최적화가 가능합니다.
3.4.2 유사 SQL 데이터 : 데이터로서의 함수
로대시JS 의 믹스인 기능을 응용해 핵심 라이브러리에 함수를 추가하여 확장한 후, 마치 원래 있떤 함수처럼 체이닝해 사용해보겠습니다. 이는 SQL 구문과 유사할 것입니다.
_.mixin({'select': _.map,
'from': _.chain,
'where': _.filter,
'sortBy': _.sortByOrder}); // 믹스인 정의
_.from(persons)
.where(p => p.birthYear > 1900 && p.address.country !== 'US')
.sortBy(['firstname'])
.select(p => p.firstname)
.value();
믹스인은 잘 활용하면 강력한 수단이니 이 링크 를 읽어보면 좋을 것입니다. 이처럼 자바스크립트 코드도 SQL 처럼 데이터를 함수 형태로 모형화할 수 있는데, 이를 데이터로서의 함수라고 합니다.
3.5 재귀적 사고방식
하스켈, 스킴, 얼랭 등 순수 함수형 프로그래밍 언어는 처음부터 루프 구조가 없기 때문에 배열 등을 탐색할 때 재귀는 필수입니다. 자바스크립트에서도 XML 파일, HTML 문서, 그래프 등을 파싱할 때 재귀를 다양하게 활용할 수 있습니다.
3.5.1 재귀란?
재귀는 주어진 문제를 자기 반복적인 문제들로 잘게 분해한 다음, 이들을 다시 조합해 원래 문제의 정답을 찾는 기법입니다. 주된 구성요소는 다음과 같습니다.
- 기저 케이스 : 종료 조건이라고도 하며, 재귀 함수가 구체적인 결과값을 바로 계산할 수 있는 입력 집합
- 재귀 케이스 : 함수가 자기 자신을 호출할 때 전달한 입력 집합을 처리
3.5.2 재귀적으로 생각하기
숫자 배열의 원소를 모두 더하는 예시로 살펴보겠습니다. 먼저 일반 루프문입니다.
let acc = 0;
for (let i = 0 ; i < nums.length ; i++) {
acc += nums[i];
}
하지만 이전에 reduce 라는 함수를 알고있으므로 이를 활용해보도록 하겠습니다.
_(nums).reduce((acc, current) => acc + current, 0);
_.reduce 함수를 써서 첫번째 원소를 나머지 원소들과 순차적으로 더해가며 결과값을 계산하는 재귀적 사고방식을 적용한 것입니다. 이 사고방식을 확장하면 다음과 같이 수평사고가 가능합니다.
sum[1,2,3,4,5,6,7,8,9] = 1 + sum[2,3,4,5,6,7,8,9]
= 1 + 2 + sum[3,4,5,6,7,8,9]
= 1 + 2 + 3 + sum[4,5,6,7,8,9]
// 재귀적 덧셈
function sum(arr) {
if (_.isEmpty(arr)) {
return 0;
}
return _.first(arr) + sum(_.rest(arr));
}
sum([]); // -> 0
sum([1,2,3,4,5,6,7,8,9]); // -> 45
내부적으로 재귀 호출 스택이 겹겹이 쌓이며, 알고리즘이 종료조건에 이르면 쌓인 스택이 런타임에 의해 즉시 풀리면서 반환문이 모두 실행되고 실제 덧셈이 이뤄집니다.
재귀의 성능에 대해 얘기해보자면, 현대의 컴파일러는 아주 영리하게 루프를 최적화할 수 있도록 진화했기때문에 루프와 재귀의 성능 차이는 미미하다고 볼 수 있습니다.
3.6 Summary
- 고계함수 map, reduce, filter 를 써서 코드 확장 가능
- 로대시JS 는 데이터 흐름과 변환 과정이 명확히 구획된 제어 체인을 통해 데이터 처리 및 프로그램 작성 도모
- 함수형 프로그래밍의 선언적 스타일로 개발하면 코드 헤아리기 쉬움
- 고수준의 추상화를 SQL 어휘로 매핑하면 더 심도있게 데이터 이해 가능
- 재귀는 자기 반복적 문제를 해결하는데 쓰이며, 정의된 자료구조를 재귀적으로 파싱해야 함
출처 : 함수형 자바스크립트(책)
'책 리뷰 > 함수형 자바스크립트' 카테고리의 다른 글
함수형 자바스크립트 [5장] (0) | 2020.06.27 |
---|---|
함수형 자바스크립트 [4장 - (2)] (0) | 2020.06.22 |
함수형 자바스크립트 [4장 - (1)] (1) | 2020.06.02 |
함수형 자바스크립트 [2장] (0) | 2020.05.23 |
함수형 자바스크립트 [1장] (0) | 2020.05.17 |