2장, 전환

2022.01.08

  • 함수형 코드를 작성하기 위해서는 함수형 언어로의 전환이 필요한 것이 아니라 문제에 접근하는 방식의 전환이 필요하다.

2.1 일반적인 예제

  • 함수형 프로그래밍은 복잡한 최적화는 런타임에게 맡기고 개발자가 좀 더 추상화된 수준에서 코드를 작성할 수 있게 함.
  • 알고리즘 측면에서 가비지 컬렉션과 동일한 역할을 수행할 것이다.

2.1.1 명령형 처리

public String cleanNames(List<string> listofNames) {
  StringBuilder result = new StringBuilder();
  for(int i=0; i< listOfNames.size(); i++) {
    if (listOfNames.get(i).length() > 1){
      result.append(capitalizeString(listofNames.get(i))).append(",");
    }
  }
}

public String capitalizeString(String s) {
  return s.substring(0, 1).toUpperCase() + s.substring(1, s.length());
}
  • 명령형 프로그래밍은 개발자로 하여금 루프 내에서 연산하기를 권장한다.
    • 한 글자짜리 이름을 필터
    • 목록에 남아 있는 이름들을 대문자로 변형
    • 목록을 하나의 문자열로 변환
  • 명령형 언어에서는 세 가지 작업에 모두 저수준의 메커니즘을 (목록 내에서 반복해서) 사용해야 한다.

2.1.2 함수형 처리

  • 함수형 프로그래밍은 프로그램을 수학 공식을 모델링하는 표현과 변형으로 기술하며, 가변 상태를 지양한다.
var employees = List("neal", "s", "stu", "j", "rich")

var result = employees
  .filter(_.length() > 1)
  .map(_.capitalize)
  .reduce(_ + "," + _)
  • 고수준의 추상적 사고로 얻는 이점을 무엇일까?
  1. 문제의 공통점을 고려하여 다른 방식으로 분류하기를 권장한다.
  2. 런타임이 최적화를 잘할 수 있도록 해준다.

    결과가 변하지 않는 한, 작업 순서를 바꾸면 더 능률적이 된다. ( 처리할 아이템이 적어질 수 있으므로. )

  3. 개발자가 엔진 세부사항에 깊이 파묻힐 경우 불가능한 해답을 가능하게 한다.
public String cleanNamesP(List<String> names) {
    if (names == null) return "";
    return names
      .parallelStream()
      .filter(n -> n.length() > 1)
      .map(e -> capitalize(e))
      .collect(Collectors.joining(","))
  }

위와 같이 컬렉션 변형을 간단히 병렬처리로 바꿔주는 대체 함수들이 있다. 높은 추상 수준에서 코딩 작업을 하고, 저수준의 세부적인 최적화는 런타임이 담당하게 하면 된다.

  • 반복, 변형, 리덕션 같은 저수준 작업의 세부사항에 대해 생각하지 말고, 유사한 형태의 문제들이 얼마나 많은지부터 인식해보라.

2.2 사례 연구: 자연수의 분류

2.2.1 명령형 자연수 분류

  1. 대상이 되는 수를 보유한 내부 상태
  2. 합을 반복해서 계싼하는 것을 피하기 위한 내부 캐시
  3. 자신을 제외한 모든 약수의 합 계산

OOP 언어는 캡슐화를 이점으로 사용하기 때문에 객체지향적인 세계에서는 내부 상태의 사용이 보편적이며 권장된다. 상태를 분리해놓으면 값을 삽입할 수가 있기 때문에 단위 테스팅 같은 엔지니이링이 쉬워진다.

2.2.2 조금 더 함수적인 자연수 분류기

  1. 모든 메서드는 number를 매개변수로 받아야 한다. 그 값을 유지할 내부 상태는 없다.
  2. 모든 메서드는 순수함수이기 때문에 public static이다. 그렇기 때문에 자연수 분류 문제라는 범위 밖에서도 유용하다.
  3. 일반적이고 합리적인 변수의 사용으로 함수 수준에서의 재사용이 쉬워졌다.
  4. 이 코드는 캐시가 없기 때문에 반복적으로 사용하기에 비능률적이다.

모든 메서드가 public static 스코프를 가지며 순수함수로 이루어져 있기 때문에 메서드를 숨길 이유가 없다.

2.2.3 자바 8을 사용한 자연수 분류기

자바 8에 더해진 최고의 기능은 람다 블록과 고계함수다.
함수형 언어에서 스트림은 나중에 사용하기 위해 저장해두는 위치에너지와 같다. 스트림은 개발자가 종료 작업을 통해 값을 요구할 때까지는 위치에너지를 운동에너지로 변환하지 않는다.
스트림은 매개변수로 주고받을 수 있으며, 종료가 되기 전까지는 다른 조건을 덧붙일 수도 있다. 이것이 바로 게으른 평가이다.

2.2.4 함수형 자바를 사용한 자연수 분류기

함수형 자바는 자바 1.5 이후 버전에 무리 없이 함수형 표현을 추가하려는 목적으로 만들어진 오픈소스 프레임워크다.

  1. 함수형 자바의 범위는 경계를 포함하지 않는다.
  2. 반복하기 대신 필터하기
  3. 반복하기 대신 폴드하기

함수형 자바와 이전 버전의 차이가 단순히 문법 설탕이라고 생각할 수 있지만, 문법적 편리함은 중요하다.
한 언어로 아이디어를 표현하는 방식이 곧 문법이기 때문이다.

2.3 공통된 빌딩블록

  • 필터, 변형, 변환은 함수형 언어 및 프레임워크 어디에나 존재한다.

2.3.1 필터

  • 주어진 조건에 맞는 컬렉션의 부분집합을 구하려면 filter를 사용하라.

2.3.2 맵

  • 컬렉션을 그 자리에서 변형하려면 map 함수를 사용하라.

2.3.3 폴드/리듀스

  • foldLeft나 reduce는 캐터모피즘(catamorphism)이라는 목록 조작 개념의 특별한 변형이다.
  • reduce와 fold 연산은 둘 다 누산기(accumulator)를 사용해 값을 모은다.
  • reduce 함수는 주로 초기 값을 주어야 할 때 사용하고, fold는 누산기에 아무것도 없는 채로 시작한다.
  • 컬렉션 요소를 하나씩 다른 함수로 처리할 때는 reduce나 fold를 사용하라.
  • 함수형 프로그래밍에서는 추상 개념이 많지 않은 대신, 그 각 개념이 범용성을 띤다.

    구체성은 고계함수에 매개변수로 주어지는 함수 통해 덧붙여진다.

  • 함수형 프로그래밍은 매개변수와 합성에 크게 의존하므로 움직이는 부분 사이의 상호작용에 대한 규칙이 많지 않고, 따라서 개발자의 작업을 쉽게 해준다.

2.4 골치 아프게 비슷비슷한 이름들

  • 함수형 언어들은 공통된 부류의 몇 가지 함수들을 가지고 있다.
  • 함수형 언어는 주로 함수형 패러다임에 준해서 이런 함수들의 이름을 정한다.

2.4.1 필터

필터 함수로 컬렉션에 불리언 조건을 명시할 수 있다. 이 함수는 조건을 만족시키는 요소로 이루어진 컬렉션의 부분집합을 리턴한다.

2.4.2 맵

모든 함수형 언어에서 볼 수 있는 주요 변형 함수는 이다. 맵 함수는 함수와 컬렉션을 받아서 이 함수를 각 요소에 적용한 후 컬렉션을 리턴한다.

2.4.3 폴드/리듀스

  • 리듀스와 같은 고수준의 추상 개념을 어떤 경우에 사용하는가를 터득하는 것이 함수형 프로그래밍을 마스터하는 방법 중의 하나이다.
  • 여러 커뮤니티에서 다른 어휘를 사용하는 것이 함수형 프로그래밍 같은 새로운 패러다임을 배우는 과정에서의 어려움 중 하나이다.
  • 하지만 일단 그것들의 유사성을 터득하면 문법적으로 놀랍게도 함수형 언어들이 중복되는 기능을 지원한다는 것을 깨달을 것이다.