1. 개요
객체지향 프로그래밍은 필수적인 패러다임으로서 다루는 책과 글은 많다.
반면, 함수형 프로그래밍은 필수적이지도 않으며,
어떤 부분이 좋다고 콕 집어 말하기 어렵다.
이는 함수형 프로그래밍이 범용 패러다임이기 때문이다.
결국, 어디에서나 잘 어울릴 수 있다는 뜻이기도 하다
이 글은 아래 책을 읽고 난 후 작성되었습니다.
- 쏙쏙 들어오는 함수형 코딩 / 에릭 노먼드
2. 목표
함수형 프로그래밍의 주요 개념과 관점을 이해하고, 함수지향 설계와 아키텍쳐를 학습한다.
3. 여정
3.1. 자주 쓰이는 콜백 함수 만들어보기
지난 시간에 작성했던 콜백을 다시 한번 보자.
<javascript />
function repeatAction(array, action) {
for (let i = 0; i < array.length; i++) {
action(array[i]);
}
}
repeatAction(foods, cookAndEat);
<javascript />
function withLogging(func) {
try {
func();
} catch (e) {
console.log(e);
}
}
withLogging(() => { saveUserData(user); }
우리는 위와 같이 함수를 주입받아 계산으로 만드는 동작을 작성했었다.
이러한 행위를 체이닝(Chaining)이라고 한다.
이러한 체이닝 콜백 함수가 자주 쓰이는 예시에는 foreach, map, filter, reduce가 있다.
<javascript />
// Array의 각 요소에 특정 행동을 적용
function foreach(array, action) {
for (let i = 0; i < array.length; i++) {
action(array[i]);
}
}
// Array의 각 요소에 특정 행동을 적용한 결과를 새로운 배열로 반환
function map(array, action) {
let copy = [];
foreach(array, item => copy.push(action(item)));
return copy;
}
// Array의 각 요소 중 특정 조건을 만족하는 요소만을 새로운 배열로 반환
function filter(array, test) {
let copy = [];
for (let i = 0; i < array.length; i++) {
if (test(array[i])) {
copy.push(array[i]);
}
}
return copy;
}
// Array의 각 요소를 특정 행동을 적용한 결과를 하나의 값으로 축약
function reduce(array, init, action) {
let accumulator = init;
for (let i = 0; i < array.length; i++) {
accumulator = action(accumulator, array[i]);
}
return accumulator;
}
stream을 자주 사용하는 개발자분들이라면 한번쯤은 사용해보셨을지도 모른다.
자주 사용하는 함수형 코드이니만큼 실제 코드도 복잡하지 않다.
익숙하지 않은 분들을 위해 예시를 들면 아래와 같다.
<javascript />
let customers = [
{ name: '홍길동', age: 29, email: 'email1@example.com' },
{ name: '이순신', age: 28, email: 'email2@example.com' },
];
map(customers, c => c.email); // ['email1@example.com', 'email2@example.com']
filter(customers, c => c.age > 28); // [{ name: '홍길동', age: 29, email: 'email1@example.com' }]
reduce(customers, 0, (total, c) => total + c.age); // 57
function averageAge(customers) {
return reduce(customers, 0, (total, c) => total + c.age) / customers.length;
}
averageAge(customers); // 28.5
코드가 아닌 영어로 읽어도 이해하는데 크게 어려움이 없다.
단 한줄로 쉽게 이해할 수 있게 작성할 수 있다는 것이 함수형 코드의 장점이다.
이제 이러한 콜백 함수를 조합해서 더욱 복잡한 예시를 살펴보자
3.2. 좀 더 다양한 예시
아래는 다양한 상황에서 사용할 수 있는 함수들이다.
<javascript />
// Array에서 특정 속성값을 추출하는 함수
function pluck(array, key) {
return map(array, item => item[key]);
}
// 중첩된 배열을 한 단계 추출(평탄화)하는 함수
function flatMap(arrays) {
let result = [];
foreach(arrays, array => {
foreach(array, item => {
result.push(item);
});
});
return result;
}
// 고객의 장바구니 목록을 추출해서 평탄화
let allCarts = pluck(customers, 'carts'); // [['A', 'B'], ['C', 'D']]
flatMap(allCarts); // ['A', 'B', 'C', 'D']
<javascript />
function frequenciesBy(array, action) {
let counts = {};
foreach(array, item => {
let key = action(item);
if (counts[key]) {
counts[key]++;
} else {
counts[key] = 1;
}
});
return counts;
}
function groupBy(array, action) {
let groups = {};
foreach(array, item => {
let key = action(item);
if (groups[key]) {
groups[key].push(item);
} else {
groups[key] = [item];
}
});
return groups;
}
let products = [
{ name: '사과', price: 2000, type: '과일' },
{ name: '배', price: 3000, type: '과일' },
{ name: '고구마', price: 700, type: '채소' },
{ name: '감자', price: 600, type: '채소' },
{ name: '수박', price: 5000, type: '과일' },
];
let howMany = frequenciesBy(products, product => product.type);
console.log(howMany); // { 과일: 3, 채소: 2 }
let grouped = groupBy(products, product => product.type);
console.log(grouped); // { 과일: [ { name: '사과', price: 2000, type: '과일' }, ... ], 채소: [ ... ] }
<javascript />
// Array를 정렬하는 함수
function sortBy(array, evaluator) {
let result = [];
foreach(array, item => {
result.push(item);
});
result.sort((a, b) => evaluator(a, b));
return result;
}
let sorted = sortBy(products, (a, b) => a.price - b.price);
console.log(sorted); // [ { name: '고구마', price: 700, type: '채소' }, ... ]
3.3. 좀 더 복잡한 예시
이번에는 중첩된 데이터 구조에 체이닝을 사용하는 예시이다.
<javascript />
// 객체를 복사하는 함수(이전에 작성했던 함수)
function objectSet(obj, key, value) {
let copy = obj.assign({}, obj); // 객체 복사
obj[key] = value;
return copy;
}
// 객체를 수정하는 함수
function update(obj, key, modify) {
let oldValue = obj[key];
let newValue = modify(oldValue);
return objectSet(obj, key, newValue); // 객체 복사
}
// 3겹으로 중첩된 데이터 구조(cart -> shirt -> options)
let cart = {
shirt: {
name: 'shirt',
price: 10000,
quantity: 2,
options: {
color: 'white',
size: 'L'
}
},
}
// 옵션에서 사이즈를 수정하는 함수
function updateSize(item, newSize) {
return update(item, 'options', (options) => {
return update(options, 'size', () => {
return newSize;
});
});
}
// 카트에서 셔츠의 사이즈를 수정하는 함수
function updateShirtSizeInCart(cart, newSize) {
return update(cart, 'shirt', (shirt) => {
return updateSize(shirt, newSize);
});
}
console.log(updateShirtSizeInCart(cart, 'XL'));
이게 항상 올바른 예시라고는 할 수 없지만,
이런 식으로 중첩된 데이터 구조를 분해하여 함수형 코드를 적용할 수 있다.
3.4. 체이닝 사용 시 주의할 점
체이닝을 사용하는 것은 간단해보이지만,
막상 에러가 발생했을 때 디버깅을 하는 것을 어렵게 만들기도 한다.
따라서, 체이닝을 이용한 고차함수 사용 시에는 아래와 같은 방법이 권장된다.
- 체이닝을 위한 inline 변수이름을 의미있게 짓기
- 복잡한 체인인 경우, 중간중간 출력해보기
- 각 단계의 타입을 명확하게 하기
4. 마무리
이번 시간으로 함수형 프로그래밍 시리즈를 마무리한다.
아직 못 다룬 주제들이 많은데, 관심이 생기신 분들은
아래 키워드를 검색하여 추가로 공부해보아도 좋을 것 같다.
- 타임라인 원칙
- 암묵적 시간 모델 vs 명시적 시간 모델
- 반응형 아키텍쳐와 어니언 아키텍쳐
바쁜 시간 내주어 부족한 글을 읽어주셔서 감사합니다!
'Do you know?' 카테고리의 다른 글
아시나요? SSH (+ Oracle 무료 인스턴스 만들기) (1) | 2023.11.26 |
---|---|
아시나요? 마이크로 서비스와 도메인 주도 설계 (1) (0) | 2023.08.13 |
아시나요? 함수형 프로그래밍 - (4) 고차 함수 (0) | 2023.07.25 |
아시나요? 함수형 프로그래밍 - (3) 추상화 레벨 (0) | 2023.07.23 |
아시나요? 함수형 프로그래밍 - (2) 분류하기 (0) | 2023.07.18 |