고양이와 코딩
[쏙쏙 들어오는 함수형 코딩] CH12 ~ CH13 함수형 반복, 함수형 도구 체이닝 본문
저번 주 파트부터 익명함수를 인자로 받는 코드가 많이 나오는데, 너무너무 헷갈려서 @_@~~
무작정 실습하기보다는 제대로 이해하고 넘어가고자 합니다!
코드의 냄새: 함수 이름에 있는 암묵적 인자
- 거의 똑같이 구현된 함수가 있다.
- 함수 이름이 구현에 있는 다른 부분을 가리킨다.
리팩터링: 암묵적 인자를 드러내기
- 함수 이름에 있는 암묵적 인자를 확인한다.
- 명시적인 인자를 추가한다.
- 함수 본문에 하드 코딩된 값을 새로운 인자로 바꾼다.
- 함수를 호출하는 곳을 고친다.
→ 이 부분은 예를들어 장바구니 안의 넥타이를 가리키는 이름을, 좀 더 일반적인 이름(예를들면 item)으로 변경하는 식으로 이해했습니다.
리팩터링: 함수 본문을 콜백으로 바꾸기
- 함수 본문에서 바꿀 부분의 앞부분과 뒷부분을 확인합니다.
- 리팩터링 할 코드를 함수로 빼냅니다.
- 빼낸 함수의 인자로 넘길 부분을 또 다른 함수로 빼냅니다.
function emailsForCustomers(customers, goods, bests) {
var emails = [];
forEach(customers, function (customer) {
var email = emailsForCustomers(customer, goods, bests);
emails.push(email);
});
return emails;
}
위 코드에서 forEach를 사용한 부분은 map()으로 빼낼 수 있습니다.
function emailsForCustomers(customers, goods, bests) {
return map(customers, function (customer) {
return emailForCustomer(customer, goods, bests);
});
}
function map(array, f) {
var newArray = [];
return forEach(array, function (element) {
newArray.push(f(element));
});
return newArray;
}
여기서 가장 이해가 안되는 부분은 익명 함수를 인자로 전달하는 부분인데, 위 코드에서 map 함수를 호출할 때 두 번째 인자로 익명 함수를 전달받습니다. 이 익명 함수가 하는 역할은 각 고객(customer)에 대해 emailForCustomer함수를 호출하여 해당 고객의 이메일을 생성한다고 이해했습니다!
map 함수는 forEach 함수를 호출합니다. forEach함수는 배열의 각 요소 (element)에 대해 주어진 함수를 실행하고,
newArray 배열에 값을 추가합니다!
함수형 도구 : reduce()
`callback` : 배열의 각 요소에 대해 실행될 함수, 네 개의 인자를 가진다.
- accumulator : 누산기 역할, callback의 반환값이 누적된다.
- currentValue : 현재 배열 요소의 값
- index ? : 현재 배열 요소의 인덱스
- array ? : reduce를 호출한 배열 자체
`initialValue` ? : 초기값으로 사용될 값. 이 값이 제공되지 않으면 첫 번째 배열 요소가 초기값으로 사용된다. 빈 배열에서는 초기값이 없을 경우 TypeError가 발생한다
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
reduce 함수의 일반적인 사용 예제
중첩된 콜백 개선하기
function biggestPurchasesBestCustomers(customers) {
var bestCustomers = filter(customers, function (customer) {
return customer.purchases.length >= 3;
});
var biggestPurchases = map(bestCustomers, function (customer) {
return reduce(
customer.purchases,
{ total: 0 },
function (biggestSoFar, purchase) {
if (biggestSoFar.total > purchase.total) {
return biggestSoFar;
} else return purchase;
}
);
});
return biggestPurchases;
}
// 중첩 콜백을 없애보자
function findMaxPurchases(biggestSoFar, purchase) {
if (biggestSoFar.total > purchase.total) {
return biggestSoFar;
} else return purchase;
}
function biggestPurchasesBestCustomers(customers) {
var bestCustomers = filter(customers, function (customer) {
return customer.purchases.length >= 3;
});
var biggestPurchases = map(bestCustomers, function (customer) {
return reduce(customer.purchases, { total: 0 }, findMaxPurchases);
});
return biggestPurchases;
}
// 위 코드에서 findMaxPurchases 함수를 더 일반적인 함수로 만들어보자
function findMaxValue(biggestSoFar, current, property) {
if (biggestSoFar[property] > current[property]) {
return biggestSoFar;
} else return current;
}
function biggestPurchasesBestCustomers(customers) {
var bestCustomers = filter(customers, function (customer) {
return customer.purchases.length >= 3;
});
var biggestPurchases = map(bestCustomers, function (customer) {
return reduce(
customer.purchases,
{ total: 0 },
function (biggestSoFar, purchase) {
return findMaxValue(biggestSoFar, purchase, "total");
}
);
});
return biggestPurchases;
}
- 제가 작성한 코드에서는 속성 이름을 함수가 아닌 속성 자체를 직접 전달받아 사용합니다.
- 이 부분을 함수를 전달받는 코드로 바꾸면 maxKey 함수가 더 일반적인 상황에서 사용될 수 있습니다.
function maxKey(array, init, f) {
return reduce(array, init, function (biggestSoFar, element) {
if (f(biggestSoFar) > f(element)) {
return biggestSoFar;
} else {
return element;
}
});
}
function biggestPurchasesBestCustomers(customers) {
var bestCustomers = filter(customers, function (customer) {
return customer.purchases.length >= 3;
});
var biggestPurchases = map(bestCustomers, function (customer) {
return maxKey(customer.purchases, { total: 0 }, function (purchase) {
return purchase.total;
});
});
return biggestPurchases;
}
체인을 명확하게 만들기
- 단계에 이름 붙이기
- 콜백에 이름 붙이기
- 두 방법을 비교하기
연습문제 >>
마케팅팀은 구매 금액이 최소 100달러를 넘고(AND) 두 번 이상 구매한 고객을 찾으려고 합니다. 두 가지 조건을 만족하는 고객을 큰 손 이라고 합니다. 함수형 도구를 체이닝해서 bigSpenders() 함수를 만들어 보세요.
- filter로 조건 필터링
- 구매 금액이 최소 100달러를 넘는 고객을 찾는 함수
- 두 번 이상 구매한 고객을 찾는 함수를 만들기
먼저 짜본 코드
function bigSpender(customers) {
return filter(customers, function (customer) {
return isBigpurchase(customer) && isOverTwoPurchases(customer);
});
}
// 구매 금액이 최소 100달러를 넘는지 확인하는 함수
function isBigpurchase(purchase) {
return purchase.total >= 100;
}
// 두 번 이상 구매한 고객인지 확인하는 함수
function isOverTwoPurchases(customer) {
return customer.purchases.length >= 2;
}
정답
function bigSpender(customers) {
var withBigPurchases = filter(customers, hasBigPurchases);
var with20rMorePurchases = filter(withBigPurchases, has20rMorePurchases);
return with20rMorePurchases;
}
function hasBigPurchases(customer) {
return filter(customer.purchases, isBigPurchase).length > 0;
}
function isBigPurchase(purchase) {
return purchase.total >= 100;
}
function has20rMorePurchases(customer) {
return customer.purchases.length >= 2;
}
첫 번째 코드에서는 bigSpender 함수에서 filter함수를 한 번만 사용하여 모든 조건을 동시에 검사합니다.
두 번째 코드에서는 filter 함수를 두 번 호출하여 각 조건을 별도로 검사합니다.
재사용성 측면에서는 두 번째 접근 방식이 조금 더 유용할 수 있습니다
- 두 번째 코드는 각각의 조건을 별도의 함수로 분리하여 관리하므로, 각 조건이 변경되거나 추가되어도 해당 함수만 수정하면 됩니다.
조건을 변경, 추가할 때 기존 함수를 수정하면 다른 부분에 영향을 미치지 않습니다.
그렇다면 첫 번째 코드에서 조건을 별도의 함수로 분리 해서 이렇게도 작성할 수 있지 않을까? 라고 생각했습니다
function bigSpender(customers) {
var withBigPurchases = filter(customers, hasBigPurchases);
var with2OrMorePurchases = filter(withBigPurchases, has2OrMorePurchases);
return with20rMorePurchases;
}
function hasBigPurchases(customer) {
return isBigPurchase(customer) && isOverTwoPurchases(customer);
}
function isBigPurchase(customer) {
return customer.purchases.some(function (purchase) {
return purchase.total >= 100;
});
}
function has20rMorePurchases(customer) {
return customer.purchases.length >= 2;
}
isBigPurchase 함수 내에서 some 메서드를 사용해 고객이 100달러 이상 구매를 한번 이상 했는지를 확인합니다
만약 조건에 만족하는 객체가 있다면 isBigPurchase함수는 true를 반환합니다.
has20rMorePurchases 함수도 마찬가지로 조건을 만족한다면 true를 반환합니다.
체이닝 팁 요약
- 데이터 만들기
- 배열 전체를 다루기
- 작은 단계로 나누기
- 조건문을 filter()로 바꾸기
- 유용한 함수로 추출하기
- 개선을 위해 실험하기
function shoesAndSocksInventory(products) {
var inventory = 0;
for (var p = 0; p < products.length; p++) {
var product = products[p];
if (product.type === "shoes" || product.type === "socks") {
inventory += product.numberInInventory;
}
}
return inventory;
}
위 코드를 함수형 체인으로 변경하기
- 반복문은 map으로 사용할 수 있다
- if문은 filter로 변경할 수 있다
function shoesAndSocksInventory(products) {
var shoesAndSocks = filter(products, function (product) {
return product.type === "shoes" || product.type === "socks";
});
var inventory = map(shoesAndSocks, function (product) {
return product.numberInInventory;
});
return reduce(inventory, 0, plus);
}
배열에서 shoes와 socks를 필터링 하는 부분을 조건문에서 빼내 filter 함수로 필터링 한 후 반환된 값을 shoesAndSocks 변수에 할당합니다.
inventory는 필터링 된 shoesAndSocks 배열의 각 요소를 가져와서 그 요소의 numberInInventory 속성을 추출합니다.
이렇게 추출된 값은 새로운 배열로 변환되고 , inventory 변수에는 shoesAndSocks 배열의 각 요소들의 numberInInventory라는 속성 값으로 이루어진 배열이 할당됩니다.
마지막으로 reduce 메서드를 사용하여 초깃값 0부터 plus함수를 호출해 배열의 각 요소를 더한 값을 반환합니다.
(최종 재고량?)
]
함수형 반복, 함수형 도구 체이닝을 사용하는 이유?
1. 순수 함수와 불변성 유지: 앞 장에서 배웠듯 어디에서 사용해도 다른 변수를 변경시키지 않는 순수 함수를
중심으로 하는것이 함수형 프로그래밍이기 때문에, 함수형 반복과 함수형 도구 체이닝을 사용한다
2. 가독성과 유지보수성: 반복문과 조건문을 고차 함수로 변경하면 코드의 의도를 명확하게 전달할 수 있다.
각 단계가 명확하게 표현되므로(단계에 이름을 붙인다) 코드의 가독성이 향상되고 유지보수가 용이해진다.
3. 추상화와 재사용성: 함수형 도구(이 장에서는 map, filter, reduce...)를 사용하면 반복되는 작업을
고차 함수로 추상화 할 수 있다. 이러한 추상화는 코드의 재사용성을 높이고 중복을 줄여주며, 다양한 상황에 재사용할 수 있다.
4. 테스트의 용이성? : 지금까지 구구절절 코드를 작은 함수들로 모듈화 하는 방법을 배웠다. 각 함수를
단위 테스트 하기에 적합하다고 생각한다.
'함수형 코딩 스터디' 카테고리의 다른 글
[쏙쏙 들어오는 함수형 코딩] CH14 ~ CH15 (0) | 2024.03.19 |
---|---|
[쏙쏙 들어오는 함수형 코딩] - CH8 ~ CH9 계층형 설계 (0) | 2024.02.19 |
[쏙쏙 들어오는 함수형 코딩] CH6 ~ CH7 (얕은 복사, 깊은 복사) (0) | 2024.02.11 |
[쏙쏙 들어오는 함수형 코딩] - CH4 ~ CH5 (1) | 2024.02.04 |
[쏙쏙 들어오는 함수형 코딩] - CH1 ~ CH3 (1) | 2024.01.28 |