고양이와 코딩

[쏙쏙 들어오는 함수형 코딩] CH12 ~ CH13 함수형 반복, 함수형 도구 체이닝 본문

함수형 코딩 스터디

[쏙쏙 들어오는 함수형 코딩] CH12 ~ CH13 함수형 반복, 함수형 도구 체이닝

ovovvvvv 2024. 3. 12. 15:13
728x90

저번 주 파트부터 익명함수를 인자로 받는 코드가 많이 나오는데, 너무너무 헷갈려서 @_@~~ 
무작정 실습하기보다는 제대로 이해하고 넘어가고자 합니다!

 

 

코드의 냄새: 함수 이름에 있는 암묵적 인자

  • 거의 똑같이 구현된 함수가 있다.
  • 함수 이름이 구현에 있는 다른 부분을 가리킨다.

리팩터링: 암묵적 인자를 드러내기

  • 함수 이름에 있는 암묵적 인자를 확인한다.
  • 명시적인 인자를 추가한다.
  • 함수 본문에 하드 코딩된 값을 새로운 인자로 바꾼다.
  • 함수를 호출하는 곳을 고친다.

→ 이 부분은 예를들어 장바구니 안의 넥타이를 가리키는 이름을, 좀 더 일반적인 이름(예를들면 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` : 배열의 각 요소에 대해 실행될 함수, 네 개의 인자를 가진다.

  1. accumulator : 누산기 역할, callback의 반환값이 누적된다.
  2. currentValue : 현재 배열 요소의 값
  3. index ? : 현재 배열 요소의 인덱스
  4. 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;
}

 

 

체인을 명확하게 만들기

  1. 단계에 이름 붙이기 
  2. 콜백에 이름 붙이기
  3. 두 방법을 비교하기

연습문제 >>

마케팅팀은 구매 금액이 최소 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;
}

위 코드를 함수형 체인으로 변경하기

  1. 반복문은 map으로 사용할 수 있다
  2. 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. 테스트의 용이성? : 지금까지 구구절절 코드를 작은 함수들로 모듈화 하는 방법을 배웠다. 각 함수를
단위 테스트 하기에 적합하다고 생각한다.