김동형수 개발기

Grokking Functional Programming - 1부 2장 본문

책 스터디/[완료] FP - Grokking Funtional Programming

Grokking Functional Programming - 1부 2장

김동형수 2022. 11. 16. 23:29

2장 순수함수

 

이 장에서 배울 내용

  • 순수 함수가 필요한 이유는 무엇입니까?
  • 데이터 사본을 전달하는 방법
  • 저장하는 대신 다시 계산하는 방법
  • 상태를 전달하는 방법
  • 순수 함수를 테스트하는 방법

2.1 순수 함수가 필요한 이유는 무엇입니까?

거짓말 하지 않는 함수, 시그니처만으로 모든 동작이 예상되는 신뢰할 수 있는 함수, 그것을 순수함수라고 한다.
순수함수는 코딩할 때 버그가 날 확률이 적어진다.

 

장바구니 할인

요구사항

요구 사항: 장바구니

1) 모든 항목( 문자열 로 모델링됨 )을 카트에 추가할 수 있습니다.

2) 장바구니에 책이 추가된 경우 5% 할인됩니다.

3) 추가된 도서가 없을 경우 할인율은 0%입니다.

4) 카트에 있는 항목은 언제든지 액세스할 수 있습니다.

 

장바구니 다이어그램

 

2.2 명령형 코딩

public class ShoppingCart {
 private List<String> items = new ArrayList<>();
 private boolean bookAdded = false;

public void addItem(String item) {
 items.add(item);
 if(item.equals("Book")) {
  bookAdded = true;
 }
}

public int getDiscountPercentage() {
 if(bookAdded) {
  return 5;
 } else {
  return 0;
 }
}

public List<String> getItems() {
  return items;
 }
}

 

합리적으로 보이지만, 책이 추가되면 할인율이 5가 반환되지만 items, bookAdded는 모두 상태이므로 이후에 변할 수 있다. ( mutable )

만약 책을 넣었다가 빼버린다면.. 위 코드에서는 할인율이 항상 5로 될 것 같다.

 

2.3 코드 깨기

내가 앞서 지적했던 문제점이 바로 나온다. 책에서는 이를 상태가 부적절하게 처리된다고 한다.

 

2.4 데이터 사본 전달

getItems 에서 반환하는 걸 items 필드가 아니라 값을 복사해서 반환한 items를 수정해도 실제 인스턴스에 있는 items에 영향이 가지 않도록 처리한다. ( immutable )

데이터 복사본을 전달하는 것은 함수형 프로그래밍에서 기본적인 작업이라고 책에선 말한다.

책에선 장바구니의 요구사항이 하나 추가된다. 그 내용은 '항목 제거'

 

public void removeItem(String item) {
 items.remove(item);
 if(item.equals("Book")) {
  bookAdded = false;
 }
}

내용을 봤을 때 책이 유니크하다면 모를까 List에선 유일함이 보장되지 않으니 2개 이상의 책이 들어있는 리스트에서 삭제한다면 여전히 bookAdded의 상태값은 true로 유지가 되어야 할 것 같아보인다.

 

2.5 코드 깨기... 다시

역시 위에서 예상했던 문제가 나온다. 헤헤.. 좀 감각있는듯 아직까진 처음이니까 예상이 잘 된다

2.6 저장 대신 재계산

책에서는 bookAdded 필드를 제거하고 items에 책이 들어있는지 List의 멤버함수인 contains를 사용해서 할인율 분기를 처리하고 있다.

다들 아시다시피 리스트에서 찾게 되면 선형탐색이라 매우 큰 크기의 리스트에서는 성능이 낮다.

 

그렇지만 많은 문제가 있었고 앞으로 일으킬 수 있는 bookAdded 필드를 제거했다. 성능적인 측면은 손해를 봤지만 가독성은 얻었다. 이를 두고 책에서는 코너케이스 성능교환을 했다 라고 한다.

 

2.7 상태를 전달하여 논리에 집중

해당 클래스에서 요구사항 관점으로 볼때 가장 중요한 기능을 하는 메서드는 getDiscountPercentage이다.

인스턴스 생성하는 비용을 절감하기 위해서 인스턴스 메서드에서 public static 메서드로 전환하고 할인율 판단에 필요한 items는 매개변수로 전달받는다.

 

2.8 상태는 어디로 갔습니까?

모든 상태(필드)를 제거했고 public static 함수만 덩그라니 남겼다. 현재의 getDiscountPercentage는 아래의 3가지 항목을 만족하지 않는다.

  • 모든 항목( String 으로 모델링됨 )을 카트에 추가할 수 있습니다.
  • 카트의 항목은 언제든지 액세스할 수 있습니다.
  • 카트에 이전에 추가된 모든 항목을 제거할 수 있습니다.

그러나 위 항목은 자바 표준라이브러리인 List에서도 충분히 소화가 가능하다.

 

적은량의 코드로 클래스에서 관리하는 상태가 없고 장바구니 항목 목록을 매개변수로 전달받기만 하면 getDiscountPercentage 함수는 이전과 같은 힘을 발휘하다.

 

이를 관심사 분리라고 하고 뒷 챕터(8, 11)에 나온다고 한다.

 

2.9 비순수 함수와 순수 함수의 차이점

 

2.10 쉬는 시간: 순수 함수로 리팩토링

이제 명령형 코드를 순수 함수로 리팩토링할 차례이지만 완전히 다른 코드 조각입니다. 관련된 사람들의 양에 따라 팁을 계산하기 위해 친구 그룹이 사용할 수 있는 TipCalculator 클래스 를 리팩터링 할 것입니다. 계산하는 사람의 수가 1명에서 5명 사이일 경우 팁은 10%입니다. 그룹이 5명보다 크면 팁은 20%입니다. 우리는 또한 "dine & dash"의 코너 케이스를 다룹니다. 사람이 없을 때 팁은 분명히 0%입니다.

class TipCalculator {
 private List<String> names = new ArrayList<>();
 private int tipPercentage = 0;

 public void addPerson(String name) {
  names.add(name);
  if(names.size() > 5) {
   tipPercentage = 20;
  } else if(names.size() > 0) {
   tipPercentage = 10;
  }
 }

 public List<String> getNames() {
  return names;
 }

 public int getTipPercentage() {
  return tipPercentage;
 }
}

우선 상태로 관리되는 내용을 제거하고 매개변수로 넘겨받게 한 뒤 getTipPercentage에서 계산하도록 변경하고 해당 함수를 public static으로 변경하면 될 것 같다.

 

class TipCalculator {
 public static int getTipPercentage(List<String> names) {
  if(names.size() > 5) {
   return 20;
  } else if(names.size() > 0) {
   return 10;
  }
  return 0;
 }
}

 

2.11 쉬는 시간 설명: 순수 함수로 리팩토링

하나를 놓쳤다. addPerson을 데이터 사본 규칙을 이용해야하는 내용이다.

책에 나온 정답코드는 아래와 같다

 

class TipCalculator {
  public List<String> addPerson(List<String> names,
                   String name) {
   List<String> updated = new ArrayList<>(names);
   updated.add(name);
   return updated;
 }

 public static int getTipPercentage(List<String> names) {
  if(names.size() > 5) {
   return 20;
  } else if(names.size() > 0) {
   return 10;
  } else return 0;
 }
}

addPerson 메서드는 왜 static으로 지정하지 않았을까?

 

2.12 우리가 신뢰하는 순수 함수

클래스 및 메서드를 프로그래머가 직접 인코딩할때보다 순수함수로 작성을 시도하는 것이 버그가 더 적게 발생한다.

 

예시로 나오는 스칼라 계산 함수(f(x) = x * 95 / 100)의 특성은 아래와 같다.

  • 항상 단일 값을 반환합니다.
  • 인수에 따라서만 반환 값을 계산합니다.
  • 기존 값을 변경하지 않습니다.

2.13 프로그래밍 언어의 순수 함수

이 순수함수는 세 가지의 특성을 갖는다.

  • 항상 단일 값을 반환합니다.
    • 함수는 하나의 값만 반환한다.
  • 인수에 따라서만 반환 값을 계산합니다.
    • 결과값에 인수가 영향을 끼친다.
  • 기존 값을 변경하지 않습니다.
    • 입력한 값을 변경하지 않으므로 함수를 동일한 인자로 여러번 호출하더라도 결과값은 항상 동일하다.

2.14 순수함의 어려움...

좀 더 수학처럼 순수 함수가 되자.

순수 함수의 세 가지 특성에 집중하고 어디를 가든지 따라가려고 노력해야 합니다 .

 

2.15 순수한 함수와 깔끔한 코드

단일책임

함수가 단일 값만 반환할 수 있고 변수를 변경할 수 없는 경우 한 가지 작업만 수행할 수 있습니다.

CS에서 단일책임의 법칙 ( OOP 였던 거 같음 )

 

부작용 없음

함수의 결과가 단일 값만 반환한다면, 그 함수의 부작용은 없다고 말한다.

 

참조의 투명성

같은 매개변수의 값이면 결과가 항상 동일하다.

 

2.16 쉬는 시간: 순수한가 불순한가?

static int increment(int x) {
  return x + 1;
 }

 static double randomPart(double x) {
  return x * Math.random();
 }

 static int add(int a, int b) {
  return a + b;
 }

 class ShoppingCart {
  private List<String> items = new ArrayList<>();

 public int addItem(String item) {
  items.add(item);
  return items.size() + 5;
 }
}

 static char getFirstCharacter(String s) {
  return s.charAt(0);
}

1. 순수

2. 불순 - random에 의해서 매개변수가 같더라도 결과가 달라질 수 있음

3. 순수

4. 불순 - items가 상태를 저장하고 있으므로, 같은 매개변수를 넣어도 결과가 달라질 수 있음

5. 불순 - s가 null일경우 예외가발생

 

..실습..

Comments