13장 함수형 도구 체이닝
이번 장에서 살펴볼 내용
- 함수형 도구를 조합하는 법을 배운다.
- 복잡한 반복문을 함수형 도구 체인으로 바꾸는 법을 배운다.
- 데이터 변환 파이프라인을 만들어 작업을 수행하는 법을 배운다.
함수형 도구 체이닝
우수고객의 가장 비싼 주문을 찾는 함수
const biggestPurchasesBestCustomers = (customers: Customer[]) => {
// 1 단계
const bestCustomers: Customer[] = filter(
customers,
(customer) => customer.purchases > 3
);
// 2 단계
const biggestPurchases: Purchases[] = map(bestCustomers, (customer) => {
return reduce(
customer.purchases,
(biggest, purchase) => {
if (biggest.total > purchase.total) {
return biggest;
}
return purchase;
},
{ total: 0 }
);
});
return biggestPurchases;
};
콜백 분리
maxKey(customer.purchases, (purchase) => purchase.total, { total: 0 });
const maxKey = <T, K>(
array: T[],
keySelector: (item: T) => K,
defaultValue: K
): K => {
return reduce(
array,
(biggest, item) => {
if (keySelector(biggest) > keySelector(item)) {
return biggest;
}
return item;
},
defaultValue
);
};
const biggestPurchasesBestCustomers = (customers: Customer[]) => {
// 1 단계
const bestCustomers: Customer[] = filter(
customers,
(customer) => customer.purchases > 3
);
// 2 단계
const biggestPurchases: Purchases[] = map(bestCustomers, (customer) => {
return maxKey(customer.purchases, (purchase) => purchase.total, {
total: 0,
});
});
return biggestPurchases;
};
단계에 이름 붙이기 (방법 1)
const biggestPurchasesBestCustomers = (customers: Customer[]) => {
const bestCustomers: Customer[] = selectBestCustomers(customers);
const biggestPurchases: Purchases[] = getBiggestPurchases(bestCustomers);
return biggestPurchases;
};
const selectBestCustomers = (customers: Customer[]) => {
return filter(customers, (customer) => customer.purchases.length >= 3);
};
const getBiggerPurchases = (customers: Customer[]) => {
return map(customers, getBiggestPurchase);
};
const getBiggestPurchase = (customer: Customer) => {
return maxKey(customer.purchases, (purchase) => purchase.total, {
total: 0,
});
};
콜백에 이름 붙이기 (방법 2)
const biggestPurchasesBestCustomers = (customers: Customer[]) => {
const bestCustomers: Customer[] = filter(customers, isGoodCustomer);
const biggestPurchases: Purchases[] = map(bestCustomers, getBiggestPurchase);
return biggestPurchases;
};
const isGoodCustomer = (customer: Customer) => customer.purchases.length >= 3;
const getBiggestPurchase = (customer: Customer) => {
return maxKey(customer.purchases, getPurchaseTotal, { total: 0 });
};
const getPurchaseTotal = (purchase: Purchase) => purchase.total;
두 방법을 비교
- 두번째 방법이 더 명확하다. (재사용성, 단계 중첩 방지)
효율성
- filter(), map()은 매번 새 배열을 만든다
- 비효율적이라고 생각 할 수 있으나 GC가 빠르게 처리하게 때문에 걱정 No!
- 체인을 최적화 하는 것을 스트림 결합이라고 한다.
// 하나의 값에 map() 두번 사용
const names = map(customers, getFullName);
const nameLengths = map(names, StringLength);
// map() 한번 사용
const nameLengths = map(customers, (customer) =>
StringLength(getFullName(customer))
);
한 번 사용하는 곳은 GC가 필요 없다.
반복문 함수형도구로 리팩토링
- 이해하고 다시 만들기
- 단서찾기
// 결과의 배열
const answer = [];
const window = 5;
// 배열 개수만큼 반복
for (let i = 0; i < array.length; i++) {
const sum = 0;
const count = 0;
// 0~5작은 구간 반복
for (let w = 0; w < window; w++) {
const idx = i + w;
if (idx < array.length) {
sum += array[idx];
// 값의 누적
count += 1;
}
}
// 배열의 추가
answer.push(sum / count);
}
팁 1 데이터 만들기
const answer = [];
const window = 5;
for (let i = 0; i < array.length; i++) {
const sum = 0;
const count = 0;
// 데이터를 배열에 넣어 함수형 도구 쓸수 있게 만들기
const subArray = array.slice(i, i + window);
for (let w = 0; w < subArray.length; w++) {
sum += subArray[w];
count += 1;
}
answer.push(sum / count);
}
팁 2 전체 배열 한번에 조작하기
const answer = [];
const window = 5;
for (let i = 0; i < array.length; i++) {
// 하위 배열을 만들기 위해 반복문의 인덱스를 사용
const subArray = array.slice(i, i + window);
answer.push(average(subArray));
}
팁 3 작은 단계로 나누기
const indices = [];
for (let i = 0; i < array.length; i++) {
indices.push(i);
}
const window = 5;
const answer = map(indices, (i) => {
const subArray = array.slice(i, i + window);
return average(subArray);
});
const indices = [];
for (let i = 0; i < array.length; i++) {
indices.push(i);
}
const window = 5;
const windows = map(indices, (i) => array.slice(i, i + window));
const answer = map(windows, average);
const range = (start: number, end: number) => {
const result = [];
for (let i = start; i < end; i++) {
result.push(i);
}
return result;
};
const window = 5;
const indices = range(0, array.length);
const windows = map(indices, (i) => array.slice(i, i + window));
const answer = map(windows, average);
절차적 코드와 함수형 코드 비교
const answer = [];
const window = 5;
for (let i = 0; i < array.length; i++) {
const sum = 0;
const count = 0;
for (let w = 0; w < window; w++) {
const idx = i + w;
if (idx < array.length) {
sum += array[idx];
count += 1;
}
}
answer.push(sum / count);
}
const window = 5;
const indices = range(0, array.length);
const windows = map(indices, (i) => array.slice(i, i + window));
const answer = map(windows, average);
// 유틸 함수
const range = (start: number, end: number) => {
const result = [];
for (let i = start; i < end; i++) {
result.push(i);
}
return result;
};
체이닝 팁 요약
- 데이터 만들기
- 배열 전체를 다루기
- 작은 단계로 나누기
- 조건문을 filter 로 바꾸기
- 유용한 함수 추출하기
- 개선을 위해 실험하기(노력)
체이닝 디버깅을 위한 팁
- 구체적인 것을 유지
- 출력해보기
- 타입 따라가기
다양한 함수형 도구
pluck()
const pluck = (array, field) => {
return map(array, (obj) => obj[field]);
};
const prices = pluck(products, 'price');
concat()
const concat = (arrays) => {
const ret = [];
forEach(arrays, (array) => {
forEach(array, (item) => {
ret.push(item);
});
});
return ret;
};
const purchaseArrays = pluck(customers, 'purchases');
const allPurchases = concat(purchaseArrays);
frequenciesBy(), groupBy()
const frequenciesBy = (array, f) => {
const ret = {};
forEach(array, (item) => {
const key = f(item);
if (ret[key]) ret[key] += 1;
else ret[key] = 1;
});
return ret;
};
const groupBy = (array, f) => {
const ret = {};
forEach(array, (item) => {
const key = f(item);
if (ret[key]) ret[key].push(item);
else ret[key] = [item];
});
return ret;
};
값을 만들기 위한 reduce()
const itemsAdded = ['shirt', 'shoes', 'socks', 'hat'];
const shippingCart = reduce(
itemsAdded,
(cart, item) => {
if (!cart[item]) {
return addItem(cart, {
name: item,
quantity: 1,
price: priceLookup(item),
});
} else {
const quantity = cart[item].quantity;
return setFieldByName(cart, item, 'quantity', quantity + 1);
}
},
{}
);
const shippingCart = reduce(itemsAdded, addOne, {});
const addOne = (cart, item) => {
if (!cart[item]) {
return addItem(cart, {
name: item,
quantity: 1,
price: priceLookup(item),
});
} else {
const quantity = cart[item].quantity;
return setFieldByName(cart, item, 'quantity', quantity + 1);
}
};
데이터를 사용해 창의적으로 만들기
const itemOps = [
['add', 'shirt'],
['add', 'shoes'],
['remove', 'socks'],
['remove', 'hat'],
];
const shippingCart = reduce(
itemOps,
(cart, itemOp) => {
const op = itemOp[0];
const item = itemOp[1];
if (op === 'add') return addOne(cart, item);
if (op === 'remove') return removeOne(cart, item);
},
{}
);