Functional Thinking Chapter 2. 전환
Chapter 2 전환
새로운 프로그래밍 언어를 배우기는 쉽다. 이미 친숙한 개념들의 새로운 문법을 배우기만 하면된다. 일례로, 자바스크립트를 배우기로 마음먹었다면, 자바스크립트의 if 문의 사용법을 설명해주는 문서를 읽어보는 것이 제일 처음으로 할 일이다. 일반적으로 개발자들은 이미 다른 언어에 대해, 이미 알고 있는 지식을 적용하여 새 언어를 배운다. 하지만 새 패러다임을 익히기는 어렵다. 이미 친숙한 문제에 대해 다른 해답을 떠올릴 능력을 배워야 하기 때문이다. 함수형 코드를 작성하기 위해서는, 함수형 언어인 스칼라나 클로저로의 전환이 필요한 것이 아니라 문제에 접근하는 방식의 전환이 필요하다.
2.1 일반적인 예제
가비지 컬렉션이 주류로 자리 잡은 이후 디버그하기 어려운 일련의 문제들은 사라져버렸고, 개발자가 처리하기엔 복잡하고 오류가 잦은 프로세스를 런타임이 알아서 처리할 수 있게 되었다. 함수형 프로그래밍은, 복잡한 최적화는 런타임에게 맡기고 개발자가 좀 더 추상화된 수준에서 코드를 작성할 수 있게 함으로써, 알고리즘 측면에서 가비지 컬렉션과 동일한 역할을 수행할 것이다. 개발자들은 가비지 컬렉션에서 얻었던 것과 같이 복잡성이 낮아지고 성능이 높아지는 혜택을 받게 될 것이다. 특히 이번에는 개발자가 좀 더 친밀하게 느낄 수 있는 문제의 해법을 고안하는 과정에서 혜택을 받게 된다.
2.1.1 명령형 처리
명령형 프로그래밍이란 상태를 변형하는 일련의 명령들로 구성된 프로그래밍 방식을 말한다. 전형적인 for 루프가 명령형 프로그래밍의 훌륭한 예이다. 초기 상태를 설정하고, 되풀이할 때마다 일련의 명령을 실행한다.
명령형 프로그래밍과 함수형 프로그래밍의 차이를 살펴보기 위해서, 통상적인 문제와 그에 대한 명령형의 해답을 살펴보자. 어떤 이름 목록에서, 한 글자로 된 이름을 제외한 모든 이름을 대문자화 해서 쉼표로 연결한 문자열을 구하려 한다고 해보자.
public class TheCompanyProcess {
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(",");
}
}
return result.substring(0, result.length() - 1).toString();
}
private String capitalizeString(String s) {
return s.substring(0, 1).toUpperCase() + s.substring(1, s.length());
}
}
목록 전체를 처리해야 하기 때문에, 위의 예제에서 가장 쉽게 이 문제를 푸는 방법은 명령형 루프를 사용하는 것이다. 각 이름마다 이름이 한 글자보다 긴가를 확인하고, 대문자화된 이름을 결과에 추가한다. 마지막 이름에는 쉼표를 포함하면 안 되므로, 마지막 결과에서 끝 글자를 잘라낸다.
명령형 프로그래밍은 개발자로 하여금 루프 내에서 연산하기를 권장한다. 나는 이 예제에서 세 가지를 실행했다. 한 글자짜리 이름을 필터했고, 목록에 남아있는 이름들을 대문자로 변형하고, 이 목록을 하나의 문자열로 변환했다. 우선 이 세 가지 작업을 목록에 적용할 ‘유용한 작업들’ 이라고 정의하자. 명령형 언어에서는 세 가지 작업에 모두 저수준의 메커니즘을(목록내에서 반복해서) 사용해야 한다. 함수형 언어들은 이런 작업을 위한 몇몇 도우미(helper) 들을 제공한다.
2.1.2 함수형 처리
함수형 프로그래밍은 프로그램을 수학 공식을 모델링 하는 표현과 변형으로 기술하며, 가변상태를 지양한다. 함수형 프로그래밍 언어는 명령형 언어와는 다르게 문제를 분류한다. 앞에서 언급한 필터, 변형, 변환 등의 논리적 분류도 저수준의 변형을 구현하는 함수들이었다. 개발자는 고차함수에 매개변수로 주어지는 함수를 이용하여 저수준의 작업을 커스터마이즈 할 수 있다. 따라서 아래 예제처럼 의사코드를 사용하여 이 문제를 개념화할 수 있다.
listOfEmps
-> filter(x.length > 1)
-> transform(x.capitalize)
-> convert(x + "," + y)
함수형 언어는 이런 개념화된 해법을 세부사항에 구애받지 않고 모델링 할 수 있게 해준다.
아래 예제는 위의 예제 ‘CompanyProcess’ 를 스칼라로 구현한 것이다.
val employees = List("neal", "s", "stu", "j", "rich", "bob", "aiden", "j", "ethan",
"liam", "mason", "noah", "lucas", "jacob", "jayden", "jack")
val result = employees
.filter(_.length > 1)
.map(_.capitalize)
.reduce(_ + "," + _)
스칼라로 짠 예제는 구현 세부사항만 추가되었을뿐 마치 위의 의사코드처럼 읽힌다. 먼저 이름 리스트에서 한 글자 이름을 필터해서 제거한다. 다음으로, 이 연산의 결과를 주어진 코드 블록을 컬렉션의 각 요소에 실행하여 그 결과를 리턴하는 map 함수에 넘겨준다. 마지막으로 map 의 출력 컬렉션이 reduce 함수로 흘러 들어간다. reduce 함수는 각 요소를 주어진 코드 블록의 규칙에 따라 결합하는 역할을 하는 함수다. 이 경우 첫 두 요소를 쉼표로 결합하여 연결한다. 세 함수 모두에서 매개변수를 어떻게 이름 지어도 상관이 없기 때문에, 스칼라에서는 이름을 생략하고 언더바(_) 를 대신 사용했다.
스칼라를 첫 번째 구현 언어로 선택한 이유는, 문법이 어느 정도 친숙하고, 또 스칼라가 이런 개념들에 대해 업계에서 일관되게 사용하는 이름을 사용하기 때문이다. 사실 자바8 에서도 동일한 기능이 있다. 이는 아래와 같이 스칼라 버전과 매우 유사하다.
public class Process {
public String cleanNames(List<String> names) {
if (names == null)
return "";
return names
.stream()
.filter(name -> name.length() > 1)
.map(name -> capitalize(name))
.collect(Collectors.joining(","));
}
private String capitalize(String e) {
return e.substring(0, 1).toUpperCase() + e.substring(1, e.length());
}
}
위 예제에서는 reduce 함수 대신 collect 함수를 사용했는데, 이것이 자바 String 클래스에 대해서는 좀 더 효율적이기 때문이다. collect 함수는 자바8 에서 reduce 함수의 특별한 경우에 해당한다. 그 밖에는 위의 스칼라 예제와 매우 흡사하다.
목록 내에 null 이 있을 경우에 대비해서 스트림에 조건 하나를 넣는 것도 쉽게 할 수 있다.
return names
.stream()
.filter(name -> name != null)
.filter(name -> name.length() > 1)
.map(name -> capitalize(name))
.collect(Collectors.joining(","));
자바 런타임은 똑똑해서 null 체크와 길이 필터를 하나의 연산으로 묶어주기 때문에, 개발자는 아이디어를 간단명료하게 표현하면서도 성능이 좋은 코드를 작성할 수 있다.
그루비도 이런 기능을 다 가지고 있지만 이름은 루비 같은 스크립팅 언어보다 좀 더 일관적이다.
class TheCompanyProcess {
public static String cleanUpNames(listOfNames) {
listOfNames
.findAll { it.length() > 1 }
.collect { it.capitalize() }
.join(",")
}
}
위의 그루비로 작성한 예제는 구조적으로 스칼라의 예제코드와 흡사하지만, 메서드 이름과 치환식별자는 다르다. 그루비의 findAll 함수를 컬렉션에 적용하면 코드블록을 실행한 다음 결과가 true 인 요소만 유지한다. 그루비도 스칼라처럼 개발자가 코드 블록을 매개변수 하나만으로 간단하게 적을 수 있게 해준다. 그루비의 치환방법은 하나의 매개변수를 표현하는데 it 이라는 키워드를 사용한다. map 의 그루비 버전인 collect 메서드는 주어진 코드블록을 컬렉션의 각 요소에 대해 실행한다. 그루비에는 문자열 컬렉션을 받아서 모든 요소들을 주어진 구분 문자로 연결하여서 하나의 문자열을 만드는 join 함수가 있다. 바로 여기에서 필요한 함수다.
이제까지 본 모든 언어들은 함수형 프로그래밍의 주요 개념을 포함하고 있다. 함수형 사고로의 전환은, 어떤 경우에 세부적인 구현에 뛰어들지 않고 이런 고수준 추상 개념을 적용할지를 배우는 것이다.
그렇다면 고수준의 추상적 사고로 얻는 이점은 무엇일까? 첫째로, 문제의 공통점을 고려하여 다른 방식으로 분류하기를 권장한다는 것이다. 둘째로, 런타임이 최적화를 잘할 수 있도록 해준다는 것이다. 어떤 경우에서는, 결과가 변하지 않는 한, 작업 순서를 바꾸면 더 능률적이 된다.(예를 들어 더 적은 아이템을 처리함으로써). 셋째로, 개발자가 엔진 세부사항에 깊이 파묻힐 경우 불가능한 해답을 가능하게 한다. 일례로 TheCompanyProcess 의 자바코드 예제를 여러 스레드에 나눠서 처리하게끔 할 때 해야 할 일을 상상해보라. 개발자가 저수준의 반복과정을 제어해야 하기 때문에, 스레드 관련 코드가 문제 해결 코드에 섞여 들어가게 된다. 스칼라 버전에서는 아래처럼 스트림에 par 만 붙이면 된다.
val parallelResult = employees
.par
.filter(_.length() > 1)
.map(_.capitalize)
.reduce(_ + "," + _)
자바8 버전도 똑같이 바꿔주면 같은 효과를 얻을 수 있다.
public String cleanNamesP(List<String> names) {
if (names == null)
return "";
return names
.parallelStream()
.filter(n -> n.length() > 1)
.map(e -> capitalizeString(e))
.collect(Collectors.joining(","));
}
높은 추상수준에서 코딩작업을 하고, 저수준의 세부적인 최적화는 런타임이 담당하게 하면 된다. 가비지 컬렉션을 탑재한 탄탄한 가상 머신을 짜는 것은 엄청나게 어려운 일이기 때문에 개발자들은 기꺼이 그 책임을 양도한다. JVM 엔지니어들이 가비지 컬렉션을 거의 신경 쓸 필요가 없을 정도로 추상화해준 덕분에 개발자들의 삶의 질은 향상되었다.
map, reduce, filter 와 같은 함수형 연산도 이와 같은 이중적인 혜택을 준다. 반복, 변형, 리덕션 같은 저수준 작업의 세부사항에 대해 생각하지 말고, 유사한 형태의 문제들이 얼마나 많은지부터 인식해보라.
2.3 공통된 빌딩블록
2.3.1 필터
목록에 할 수 있는 흔한 작업은 필터하는 것이다. 사용자가 정한 조건으로 목록에 있는 요소들을 필터하여 더 작은 목록을 만드는 작업이다. 필터작업을 할 때에는 필터 조건에 따라서 원래 목록보다 작은 목록(또는 컬렉션)을 생성한다.
public static IntStream factorsOf(int number) {
return IntStream.range(1, number + 1)
.filter(potential -> number % potential == 0);
}
위의 코드는 1부터 대상 숫자까지의 목록을 만들고, filter() 메서드를 적용하여 약수가 아닌 숫자들을 제거한다. 자바의 나머지 연산자(%)는 정수 나눗셈의 나머지를 리턴하기 때문에 나머지가 0 이면 약수임을 알 수 있다. 람다 블록을 사용하지 않고도 같은 결과를 얻을수는 있지만, 람다 블록이 있는 언어에서는 더 간결하게 표현할 수 있다. 그루비 버전은 아래와 같다.
static def factors(number) {
(1..number).findAll { number % it == 0 } // 그루비에서는 필터를 findAll() 이라 부른다.
}
//[1, 2, 5, 10]
위에서는 매개변수 하나를 넘기는 대신, 단일 매개변수 치환 키워드인 it을 플레이스홀더로 사용했다. 메서드의 마지막 줄은 리턴값인 약수들의 목록이다.
2.3.2 맵
맵 연산은 컬렉션의 각각의 요소에 같은 함수를 적용하여 새로운 컬렉션으로 만든다.
2.3.3 폴드 / 리듀스
자주 사용하는 함수는 많이 사용하는 언어들 사이에서도 이름이 다양하고 약간씩 의미도 다르다. foldLeft 나 reduce 는 캐터모피즘(catamorphism) 이라는 목록 조작 개념의 특별한 변형이다.
reduce 와 fold 연산은 기능이 중복되기도 하지만 언어마다 약간씩 다른 점도 있다. 둘 다 누산기(accumulator)를 사용해 값을 모은다. reduce 함수는 주로 초기값을 주어야 할 때 사용하고, fold 는 acc 에 아무것도 없는채로 시작한다. 컬렉션에 대한 연산 순서는 메서드 이름(foldLeft, 또는 foldRight)으로 지정한다. 둘 다 컬렉션을 변형하지 않는다.
우리는 함수형 자바에서 foldLeft() 를 살펴봤다. 여기서 ‘왼쪽폴드’ 란 다음과 같은 의미이다.
- 이항 함수나 연산으로 목록의 첫째 요소와 누산기의 초기값을 결합한다. 초기값이 없는 경우도 있다.
- 앞의 단계를 목록이 끝날때 까지 계속하면 누산기가 폴드 연산의 결과를 갖게된다.
숫자 목록의 합을 구할때 바로 이런 방법을 사용한다. 0 부터 시작해서 첫째 요소를 더하고, 그 결과에 둘째 요소를 더하고, 목록이 끝날 때 까지 같은 연산을 계속한다.
덧셈은 교환법칙이 성립하므로 foldLeft() 든 foldRight() 든 상관없다. 하지만 뺄셈이나 나눗셈 같은 경우는 순서가 중요하므로 foldRight() 메서드가 필요하다. 순수함수형 언어에서 왼쪽폴드와 오른쪽 폴드는 다르게 구현된다. 예를 들어 오른쪽 폴드는 무한수령에 적용할 수 있지만, 왼쪽폴드는 그렇지 못하다.
컬렉션의 요소들을 처리하여 길이가 다른 결과(더 작은 컬렉션이나 단일 값)를 원할때는 fold 난 reduce 를 주로 사용한다.
tip : 컬렉션의 요소를 하나씩 다른 함수로 처리할때는 reduce 나 fold 를 사용하라. (역주 : fold 나 reduce 의 경우 컬렉션의 각 요소에 매번 새로운 함수(같은 이항함수이지만 첫 매개변수가 달라진다)를 적용하게 된다. 모든 요소에 같은 일항 함수를 적용하는 map과는 구별된다.)
함수형 프로그래밍과 같이 다른 패러다임을 익힐때 더려운 점은 새로운 빌딩블록을 배우고, 풀고자 하는 문제에서 그것이 해법이 될 수 있다는 점을 인지하는 것이다. 함수형 프로그래밍에서는 추상 개념이 많지 않는 대신, 그 각 개념이 범용성을 띈다(구체성은 고차함수에 매개변수로 주어지는 함수를 통해 덧붙여진다). 함수형 프로그래밍은 매개변수와 합성에 크게 의존하므로 ‘움직이는 부분’ 사이의 상호작용에 대한 규칙이 많지 않고, 따라서 개발자의 작업을 쉽게 해준다.
2.4 골치 아프게 비슷비슷한 이름들
함수형 언어들은 공통된 부류의 몇 가지 함수들을 가지고 있다. 친숙한 함수지만 낯설은 이름때문에 개발자들은 언어를 바꿀때 가끔 어려움을 느낀다. 함수형 언어는 주로 함수형 패러다임에 준해서 이런 한수들의 이름을 정한다. 스크립트 언어에서 파생된 언어들은 서술적인 이름을 사용하는 경향이 있다. 어떤 경우는 여러가지 다른 이름이 모두 같은 함수의 별칭으로 사용되기도 한다.
2.4.1 필터
필터 함수로 컬렉션에 불리언 조건을 명시할 수 있다. 이 함수는 조건을 만족시키는 요소로 이루어진 컬렉션의 부분집합을 리턴한다. 필터 연산은 컬렉션에서 조건을 만족시키는 첫 요소를 리턴하는 찾기 기능과 깊은 연관이 있다.
스칼라
스칼라에는 필터의 형태가 여러가지이다. 가장 간단한 형태는 입력조건에 따라 목록을 필터하는 것이다. 첫 번째 예제에서는 우선 수의 목록을 준비한다. 그리고 각 숫자가 3으로 나뉘어야 한다는 조건을 가진 코드블록을 넘겨 filter() 함수를 적용한다.
val numbers = List.range(1, 11)
numbers filter (_ % 3 == 0)
//List(3, 6, 9)
필터연산의 많은 예제가 숫자를 사용하긴 하지만, filter() 는 어떤 컬렉션에도 적용할 수 있다. 다음 예제는 filter() 를 단어 목록에 적용하여 세 글자 단어들을 얻어낸다.
val words = List("the", "quick", "brown", "fox", "jumped", "over", "the", "lazy", "dog")
words.filter(_.length == 3)
//List(the, fox, the, dog)
스칼라의 필터 연산 중 다른 한 가지는 컬렉션을 여러 조각으로 분리한 결과를 리턴하는 partition() 함수이다. 분리조건을 정하는 함수를 전달하여 어떻게 분리할지를 정한다. 다음 partition() 함수는 3으로 나뉘는가 여부에 따라 분리된 두 목록을 리턴한다.
numbers partition(_ % 3 == 0)
//(List(3, 6, 9),List(1, 2, 4, 5, 7, 8, 10))
filter() 함수는 조건에 맞는 요소들의 컬렉션을 리턴하는 반면, find() 함수는 조건을 만족시키는 첫 번째 요소만 리턴한다.
numbers find(_ % 3 == 0)
//Some(3)
하지만 find() 의 리턴값은 조건을 만족시킨 값 자체가 아니고, Option 클래스로 래핑된 값이다. Option 은 Some 또는 None 두 가지 값을 가질 수 있다. 다른 함수형 언어들과 마찬가지로 스칼라는 null 을 리턴하는 것을 피하기 위해 관례적으로 Option 을 사용한다. Some() 인스턴스는 실제 리턴값을 래핑하며, 위 예제처럼 numbers find(_ % 3 == 0) 의 경우에는 실제 리턴값은 3이다. 리턴할 값이 업는 경우에는 대신 None 을 리턴한다.
numbers find(_ < 0)
//None
스칼라는 컬렉션에서 주어진 술어 함수에 만족시키는 요소를 간직하거나 또는 버리는 함수들도 가지고 있다. takeWhile() 함수는 컬렉션의 앞에서부터 술어 함수를 만족시키는 값들의 최대집합을 리턴한다.
List(1,2,3,-4,5,6,7,8,9,10) takeWhile( _ > 0)
//List(1, 2, 3)
dropWhile() 함수는 컬렉션의 앞에서 파라미터로 전달된 함수에 만족하는 원소들을 없앤다. 파라미터로 전달된 함수가 최초로 false 를 리턴하면 그 뒤의 원소들은 살아남는다.
words dropWhile(_ startsWith "t")
//List(quick, brown, fox, jumped, over, the, lazy, dog)
그루비
그루비는 함수형 언어라고 할 수는 없지만 스크립팅 언어에서 파생된 이름을 가진 함수형 패러다임을 다수 포함하고 있다. 일례로 그루비의 findAll() 메서드는 함수형 언어에서 전통적으로 filter() 불리는 함수이다.
(1..10).findAll { it % 3 == 0 }
//[3, 6, 9]
스칼라의 filter 함수처럼, 그루비의 findAll() 은 문자열을 포함해 모든 자료형에 적용된다.
words.findAll { it.length() == 3}
//[the, fox, the, dog]
그루비에는 split() 이라는, partition() 과 유사한 함수도 있다.
(1..10).split { it % 3 }
//[[1, 2, 4, 5, 7, 8, 10], [3, 6, 9]]
스칼라의 partition() 이 중첩된 목록을 리턴하는 것처럼, 그루비 split() 메서드는 중첩된 배열을 리턴한다.
그루비의 find() 메서드는 컬렉션에서 조건을 만족시키는 첫 요소를 리턴한다.
(1..10).find { it % 3 == 0 }
//3
스칼라와는 달리, 그루비는 자바의 관례를 따라서 find() 의 결과가 없을 때는 null을 리턴한다.
(1..10).find { it < 0 }
//null
그루비에는 스칼라 버전과 유사한 takeWhile() 과 dropWhile() 메서드도 있다.
[1, 2, 3, -4, 5, 6, 7, 8, 9].takeWhile { it > 0 }
//[1, 2, 3]
스칼라 예제와 마찬가지로 dropWhile() 은 특화된 필터처럼 작용한다. 목록의 앞부분만 필터하여 술어 조건을 만족시키는 최다수의 요소를 건너뛴다.
def moreWords = ["the", "two", "ton"] + words
moreWords.dropWhile { it.startsWith("t") }
//[quick, brown, fox, jumped, over, the, lazy, dog]
2.4.2 맵
모든 함수형 언어에서 볼 수 있는 두 번째 주요 변형 함수는 맵이다. 맵 함수는 함수와 컬렉션을 받아서 이 함수를 각 요소에 적용한 후 컬렉션을 리턴한다. 리턴된 컬렉션의 각각의 값은 변했지만 필터의 경우와는 달리 원래 컬렉션과 크기는 같다.
스칼라
스칼라의 map 함수는 코드 블록을 받아서 변형된 컬렉션을 리턴한다.
List(1,2,3,4,5) map (_ + 1)
//List(2, 3, 4, 5, 6)
map 함수는 가능한 모든 자료형에 적용할 수 있다. 하지만 항상 컬렉션의 각 요소가 변형된 값을 리턴하는 것만은 아니다. 다음 경우에는 문자열의 각 요소의 길이를 목록으로 리턴한다.
words map (_.length)
//List(3, 5, 5, 3, 6, 4, 3, 4, 3)
중첩 리스트는 함수형 프로그래밍 언어에 자주 등장하기 때문에, 보통 중첩을 펼치는 연산을 라이브러리에서 지원한다. 이러한 연산을 흔히 플래트닝(flattening, 평탄화) 이라고 한다. 중첩된 목록을 플래트닝하는 예를 살펴보자.
List(List(1,2,3), List(4,5,6), List(7,8,9)) flatMap (_.toList)
//List(1, 2, 3, 4, 5, 6, 7, 8, 9)
여기선 리턴되는 목록은 내부 구조 없이 각 요소들만 포함되어 있다. flatMap() 함수는 전통적인 의미에서 중첩되지 않은 것처럼 보이는 자료구조에도 적용된다. 일례로 문자열의 목록은 중첩된 문자들의 배열로 볼 수 있다.
words flatMap (_.toList)
//List(t, h, e, q, u, i, c, k, b, r, o, w, n, f, o, x, j, u, m, p, e, d, o, v, e, r, t, h, e, l, a, z, y, d, o, g)
그루비
그루비도 맵의 변형인 collect() 를 가지고 있다. 기본형은 코드블록을 받아서 컬렉션의 각 요소에 적용한다.
(1..5).collect { it += 1 }
//[2, 3, 4, 5, 6]
다른 언어들과 마찬가지로 그루비도 it 이란 키워드를 매개변수 치환에 사용함으로써 익명함수를 위한 간단한 문법을 지원한다.
collect() 함수는 어떤 컬렉션에도 사용할 수 있기 때문에 문자열의 배열에도 사용이 가능하다.
words.collect { it.length() }
//[3, 5, 5, 3, 6, 4, 3, 4, 3]
그루비도 내부 구조를 없애버리는 flatMap() 과 유사한 flatten() 함수가 있다.
words.collect { it.toList() }.flatten()
//[t, h, e, q, u, i, c, k, b, r, o, w, n, f, o, x, j, u, m, p, e, d, o, v, e, r, t, h, e, l, a, z, y, d, o, g]
2.4.3 폴드 / 리듀스
세 번째의 공통 함수는 이름이 가장 다양하고, 언어에 따라 미묘한 차이가 많이 있다.
스칼라
스칼라는 동적 타이핑 언어인 그루비나 클로저에서는 다루지 않는 다양한 자료형 시나리오들을 해결해야 하기 때문에 폴드 연산 종류가 가장 다양하다. 합계를 내는 데에는 리듀스를 주로 사용한다.
List.range(1, 10) reduceLeft ( (a,b) => a + b )
/*
1 + 2 = 3
3 + 3 = 6
6 + 4 = 10
10 + 5 = 15
15 + 6 = 21
21 + 7 = 28
28 + 8 = 36
36 + 9 = 45
*/
reduce() 에 주어지는 함수 또는 연산자는 주로 두 개의 매개변수를 받아서 하나의 결과를 리턴하여 리스트를 처리한다. 스칼라의 편리한 구문을 사용하면 함수를 간결하게 정의할 수 있다.
List.range(1, 10) reduceLeft(_ + _)
reduceLeft() 함수는 첫번째 요소가 연산의 좌항이라고 간주한다. 덧셈은 피연산자의 위치에 상관이 없지만, 나눗셈과 같은 경우에는 순서가 중요하다. 연산자가 적용되는 순서를 뒤바꾸려면 reduceRight() 를 사용하라.
println(List.range(1, 10) reduceRight( _ - _ ))
/*
8 - 9 = -1
7 - (-1) = 8
6 - 8 = -2
5 - (-2) = 7
4 - 7 = -3
3 - (-3) = 6
2 - 6 = -4
1 - (-4) = 5
*/
위에서 순서를 뒤바꾼다고 했을때, 그 사용법을 직관적으로 떠올리지는 못했을 것이다. reduceRight() 함수는 피연산자의 순서를 바꾸는 것이 아니라, 연산의 방향을 뒤바꾼다. 다시말해 8-9 를 먼저 연산하고, 그 결과를 다음 연산의 두 번째 매개변수로 사용한다.
리듀스와 같은 고수준의 추상 개념을 어떤 경우에 사용하는가를 터득하는 것이 함수형 프로그래밍을 마스터하는 방법 중의 하나이다. 이 예제는 reduceLeft() 를 사용하여 컬렉션에 들어있는 가장 긴 단어를 찾아낸다.
words reduceLeft( (a, b) => if (a.length > b.length) a else b)
//jumped
reduce 와 fold 는 서로 중복되는 기능을 가지고 있지만, 앞에서 언급했듯이 조금씩 차이가 있다. 한 가지 다른 점을 보면 어떻게 사용하는지를 알 수 있다. 스칼라에서 reduceLeft[B >: A](op: (B, A) => B): B 의 시그니처를 보녀 각 요소를 결합시키는 함수가 유일한 매개변수이다. 초기값은 컬렉션의 첫 번째 요소로 간주한다. 반면에 foldLeftB(op: (B, A)) => B): B의 시그니처는 초기 시드 값을 포함하기 때문에 목록의 요소와 다른 자료형의 리턴값을 가능하게 해준다.
foldLeft() 를 사용해서 컬렉션을 합하는 예제이다.
println(List.range(1, 10).foldLeft(0)(_ + _))
//45
그루비
그루비는 리듀스 계열의 inject() 함수에 오버로딩을 사용하여 스칼라의 reduce() 와 foldLeft() 의 기능을 제공한다. 함수의 한 버전은 초기값을 받기도 한다. 다음 예제는 inject() 를 사용하여 컬렉션의 합을 구한다.
(1..10).inject {a, b -> a + b}
//55
초기값을 받는 다른 형태도 있다.
(1..10).inject(0, { a, b -> a + b })
//55
그루비는 스칼라나 클로저에 비해 함수형 라이브러리가 작다. 그루비는 함수형 프로그래밍만 강조하는게 아닌 멀티패러다임 언어라는 점을 생각하면 당연한 일이다.