Functional thinking의 첫 번째 및 두 번째 기사에서 필자는 일부 함수형 프로그래밍 주제와 이러한 주제들이 Java™ 및 관련 언어에 어떻게 관련되는지 조사했다. 이번 기사에서는 이 탐색을 계속하여, 이전 기사에 나온 숫자 규정자의 Scala 버전을 알려주고 커링, 부분적 애플리케이션 및 재귀의 일부 학문적인 주제에 대해 논의한다.
필자는 숫자 규정자의 Scala 버전이 적어도 Java 개발자들에게는 구문적인 수수께끼가 가장 적기 때문에 마지막으로 남겨두었다. (규정자의 요구사항을 요약하면, 1보다 큰 어느 양의 정수나 주어지면, 이를 완전수(perfect), 과잉수(abundant) 또는 부족수(deficient)로 분류해야 한다. 완전수는 그 수 자체는 인수로 제외하고, 그 인수들을 합하면 최대 그 수까지 되는 수이다. 과잉수의 인수의 합계는 그 수보다 크고, 부족수의 인수의 합계는 그 수보다 적다.) 목록 1은 다음 Scala 버전을 보여준다.
목록 1. Scala로 된 숫자 규정자
package com.nealford.conf.ft.numberclassifier
object NumberClassifier {
def isFactor(number: Int, potentialFactor: Int) =
number % potentialFactor == 0
def factors(number: Int) =
(1 to number) filter (number % _ == 0)
def sum(factors: Seq[Int]) =
factors.foldLeft(0)(_ + _)
def isPerfect(number: Int) =
sum(factors(number)) - number == number
def isAbundant(number: Int) =
sum(factors(number)) - number > number
def isDeficient(number: Int) =
sum(factors(number)) - number < number
}
|
독자가 지금까지 Scala를 본 적이 없다고 해도 이 코드는 매우 가독성이 높을 것이다. 이전과 같이,
두 가지 관심 있는 메소드는 factors()와 sum()이다. factors() 메소드는
1에서부터 대상 숫자까지 숫자 목록을 취하여, 필터링 범주로서 오른쪽의 코드 블록을 사용하여(그렇지 않으면,
predicate라고 함) Scala의 내장 filter() 메소드를
적용한다. 코드 블록은 Scala의 내재된 매개변수를 활용하며, 이는 이름 지정된 변수가 필요하지
않을 때 이름이 없는 플레이스홀더(_ 문자)를 허용한다. Scala의
구문적 유연성 덕분에 filter() 메소드를 연산자를 호출하는 것과 동일한
방법으로 호출할 수 있다. 독자가 원한다면,
(1 to number).filter((number % _ == 0))도 작동한다.
sum() 메소드는 지금까지 익숙한 fold left 연산(Scala에서
foldLeft()로 구현됨)을 사용한다. 이 경우에 변수의 이름을 지정할 필요가 없기
때문에, 필자는 _를 플레이스홀더로 사용하며, 이는 코드 블록을 정의하기 위한
간단한 깔끔한 구문을 활용한다. foldLeft() 메소드는 Functional Java 라이브러리에서
이름이 유사한 메소드와 동일한 태스크를 수행한다(참고자료 참조).
이는 첫 번째 기사에
다음과 같이 나타난다.
- 초기 값을 취하여 목록의 첫 번째 요소에 대한 연산을 통해 이를 결합한다.
- 결과를 취하여 동일한 연산을 다음 요소에 적용한다.
- 목록이 소진될 때까지 이를 계속 수행한다.
이는 숫자 목록에 합하는 등의 연산을 적용하는 방법의 일반화된 버전이다. 0부터 시작하고 첫 번째 요소를 더하고 결과를 취하여 이를 두 번째 요소로 더하고 목록이 소모될 때까지 계속한다.
필자가 이전 버전에 유닛 테스트를 알려준 적이 없다고 하더라도, 모든 예제에 테스트가 있다. ScalaTest라는
효율적인 유닛 테스팅 라이브러리는 Scala에 사용할 수 있다(참고자료 참조). 다음 목록 2는
필자가 목록 1의 isPerfect() 메소드를 확인하기 위해
작성한 첫 번째 유닛 테스트를 보여준다.
목록 2. Scala 숫자 규정자에 대한 유닛 테스트
@Test def negative_perfection() {
for (i <- 1 until 10000)
if (Set(6, 28, 496, 8128).contains(i))
assertTrue(NumberClassifier.isPerfect(i))
else
assertFalse(NumberClassifier.isPerfect(i))
}
|
하지만 독자와 같이 필자는 더 함수적으로 사고하기 위해 노력하고 있으므로,
목록 2의 코드는 두 가지 측면에서 골치거리이다. 첫 번째, 이는 무엇을 수행하기 위해 반복하며,
이는 명령적 사고를 시연한다. 두 번째, 필자는 바이너리 범용 if 명령문을 신경 쓰지 않는다. 필자가 해결 중인 문제가 무엇인가? 필자는 숫자 규정자가
완전수로 불완전수를 인식하지 않도록 해야 한다. 목록 3은 다음과 같이 약간 다르게 서술하여 이 문제에 대한 해결책을 보여준다.
목록 3. 완전수 분류에 대한 대안 테스트
@Test def alternate_perfection() {
assertEquals(List(6, 28, 496, 8128),
(1 until 10000) filter (NumberClassifier.isPerfect(_)))
}
|
목록 3은 1부터 100,000까지 완전한 숫자만 알려진 완전수 목록에 속하는 것임을 주장한다. 함수적으로 사고하는 것은 코드로 확장될 뿐만 아니라 이를 테스팅하는 것에 대한 사고방식으로도 확장한다.
목록 필터링으로 보여준 함수형 접근방식은 함수형 프로그래밍 언어 및 라이브러리 전반에서 일반적이다. 코드를
매개변수로 전달시키는 기능을 사용하는 것은(목록 3에서
filter() 메소드와 같이) 코드 재사용을 다른 방식으로 생각하는 것을
시연한다. 독자가 기존의 설계 패턴 구동형 객체 지향의 영역에 익숙하다면, 이 접근방식을
Gang of Four Design Patterns 책의 Template Method 설계 패턴에 대한 이러한 접근방식과
비교한다(참고자료 참조). Template Method 패턴은 추상 메소드를 사용하고
개별 세부사항을 하위 클래스로 지연하기 위해 대체하여 기반 클래스에서 알고리즘의 기본 폼을
정의한다. 컴포지션을 사용하는 함수형 접근방식을 통해 기능을 적절하게 적용하는 메소드로 기능을
전달할 수 있다.
코드 재사용을 달성하는 또 다른 방법은 커링을 사용하는 것이다. 수학자인 Haskell Curry(Haskell 프로그래밍 언어도 그의 이름에서 나옴)의 이름에서 따온 커링은 다중 인수 함수를 변환하므로 단일 인수 함수의 체인으로 불릴 수도 있다. 긴밀하게 연관된 부분적 애플리케이션은 고정 값을 하나 이상의 인수로 함수에 지정하기 위한 기술이며, 그렇게 함으로써 arity(함수로 매개변수의 숫자)가 더 적은 또 다른 함수를 제작한다. 차이점을 이해하기 위해 다음 목록 4에서 커링을 시연하는 Groovy 코드를 살펴보도록 하자.
목록 4. Groovy의 커링
def product = { x, y -> return x * y }
def quadrate = product.curry(4)
def octate = product.curry(8)
println "4x4: ${quadrate.call(4)}"
println "5x8: ${octate(5)}"
|
목록 4에서 필자는 두 개의 매개변수를 승인하는 코드 블록으로
product를 정의한다. Groovy의 내장 curry() 메소드를
사용하여 product를 두 가지 새로운 코드 블록에 대한 빌딩 블록으로
사용한다. 즉, 이는 quadrate와 octate이다. Groovy를 사용하면
코드 블록이 간편하게 호출된다. 즉, call() 메소드를 명백히 실행하거나
코드 블록 이름 다음에 어느 매개변수나 포함하는 소괄호 세트를 지정하는 제공된 언어 레벨 간편 표기법(Syntactic sugar)을
사용할 수 있다(예를 들어, octate(5)로 표시됨).
부분적 애플리케이션은 커링과 유사하지만 하나의 인수로 결과 함수를 제한하지 않는 폭넓은 기술이다. Groovy는
curry() 메소드를 사용하여 다음 목록 5와 같이
커링과 부분적 애플리케이션 둘 다 처리한다.
목록 5. 부분적 애플리케이션 대 커링, 둘 다 Groovy의
curry() 메소드 사용
def volume = { h, w, l -> return h * w * l }
def area = volume.curry(1)
def lengthPA = volume.curry(1, 1) //partial application
def lengthC = volume.curry(1).curry(1) // currying
println "The volume of the 2x3x4 rectangular solid is ${volume(2, 3, 4)}"
println "The area of the 3x4 rectangle is ${area(3, 4)}"
println "The length of the 6 line is ${lengthPA(6)}"
println "The length of the 6 line via curried function is ${lengthC(6)}"
|
목록 5에서 volume 코드 블록은 잘 알려진 공식을 사용하여
고체 사각형의 정육면체 볼륨을 계산한다. 그러면 필자가 volume의 첫 번째
차원(높이의 h)을 1로 고정하여
area 코드 블록(사각형 영역을 계산함)을 만든다.
행 세그먼트의 길이를 리턴하는 코드 블록에 대한 빌딩 블록으로서 volume을
사용하기 위해 필자는 부분적 애플리케이션이나 커링 중 하나를 수행할 수 있다. lengthPA는
1에서 처음 두 개의 매개변수 각각을 고정하여 부분적 애플리케이션을
사용한다. lengthC는 커링을 두 번 적용하여 동일한 결과를 낳는다. 차이점은
미묘하고 최종 결과는 동일하지만, 독자가 커링과 부분적 애플리케이션이라는 용어를 함수형 프로그래머 내에서
교환하여 사용한다면, 올바르게 될 것이다.
함수형 프로그래밍은 새롭고 다른 빌딩 블록을 제공하여 명령형 언어가 다른 메커니즘으로 완수하는 동일한 목표를 달성한다. 이러한 빌딩 블록 사이의 관계는 세심하게 고려되었다. 이전에 필자는 코드 재사용 메커니즘으로 컴포지션을 보여주었다. 커링과 컴포지션을 결합할 수 있다는 것은 놀라운 일이 아니다. 다음 목록 6의 Groovy 코드를 살펴보자.
목록 6. 부분적 애플리케이션 컴포지션
def composite = { f, g, x -> return f(g(x)) }
def thirtyTwoer = composite.curry(quadrate, octate)
println "composition of curried functions yields ${thirtyTwoer(2)}"
|
목록 6에서 필자는 두 가지 함수를 작성하는
composite 코드 블록을 작성한다. 해당 코드 블록을 사용하여 필자는 두 가지 메소드를 함께 작성하기 위해
부분적 애플리케이션을 사용하여 thirtyTwoer 코드 블록을 작성한다.
부분적 애플리케이션과 커링을 사용하면 독자는 Template Method 설계 패턴과 같은 메커니즘과 유사한 목표를
달성한다. 예를 들어, 독자는 다음 목록 7과 같이 adder 코드 블록 위에
incrementer 코드 블록을 빌드하여 이를 작성할 수 있다.
목록 7. 다른 빌딩 블록
def adder = { x, y -> return x + y }
def incrementer = adder.curry(1)
println "increment 7: ${incrementer(7)}"
|
물론, Scala는 다음 목록 8과 같이 Scala 문서에서 나온 스니펫으로 시연한 대로 커링을 지원한다.
목록 8. Scala로 된 커링
object CurryTest extends Application {
def filter(xs: List[Int], p: Int => Boolean): List[Int] =
if (xs.isEmpty) xs
else if (p(xs.head)) xs.head :: filter(xs.tail, p)
else filter(xs.tail, p)
def dividesBy(n: Int)(x: Int) = ((x % n) == 0)
val nums = List(1, 2, 3, 4, 5, 6, 7, 8)
println(filter(nums, dividesBy(2)))
println(filter(nums, dividesBy(3)))
}
|
목록 8의 코드는 filter() 메소드로 사용된
dividesBy() 메소드를 구현하는 방법을 보여준다. 필자는
dividesBy() 메소드의 첫 번째 매개변수를 코드 블록을 작성하기 위해 사용된
값에 고정시키도록 커링을 사용하여
익명 함수를 filter() 메소드로 전달한다. 필자가 매개변수로 대상 숫자를
전달하여 작성된 코드 블록을 전달할 때, Scala는 새 함수를 커링한다.
함수형 프로그래밍에 긴밀하게 연관된 또 다른 주제가 재귀이며, 이는(Wikipedia에 따르면) "항목을 자체적으로 유사한 방식으로 반복하는 프로세스"이다. 실제로는 스스로 동일한 메소드를 호출하여 내용에 대해 반복하는 전산학적 방식이다(종료 조건을 보유하도록 항상 신중하게 보장함). 문제의 핵심이 감소되는 목록으로 동일한 사항을 계속 반복해야 하는 필요성이기 때문에, 많은 경우에 재귀로 인해 이해하기에 간편한 코드가 나타난다.
목록을 필터링하는 것을 고려하자. 반복적인 접근방식을 사용하여 필자는 필터링 범주를 승인하고 해당 컨텐츠에 대해 루프하여, 원하지 않는 요소를 필터링해낸다. 다음 목록 9는 Groovy로 필터링의 간단한 구현 방식을 보여준다.
목록 9. Groovy로 된 필터링
def filter(list, criteria) {
def new_list = []
list.each { i ->
if (criteria(i))
new_list << i
}
return new_list
}
modBy2 = { n -> n % 2 == 0 }
l = filter(1..20, modBy2)
println l
|
목록 9의 filter() 메소드는
list 및 criteria(목록을 필터링하는 방법을 지정하는
코드 블록)를 승인하고 목록에 대해 반복하며, 각 항목이 예측과 일치하는 경우 새 목록에 추가한다.
이제 목록 8을 돌아보자. 이는 Scala로 된 필터링 기능의 재귀적 구현 방식이다. 이는
목록을 처리하기 위한 함수적 언어로 된 일반적인 패턴을 따른다. 목록의 하나의 보기는 두 부분으로 되어있다.
즉, 이는 목록에서 앞의 항목(헤드)과 모든 다른 항목들이다. 많은 함수형 언어는 이러한 관용구를 사용하여
목록을 계속 반복하기 위한 특정 메소드를 보유한다. filter() 메소드는 먼저
목록이 비어 있는지 확인한다 — 이 메소드에 종료 조건이 매우 중요함. 목록이 비어 있으면, 간단히 리턴한다. 그렇지 않으면,
매개변수로 전달된 조건부 조건(p)을 사용한다. 이 조건이 true이면(필자가
목록에서 이 항목을 원하는 것을 의미함) 필자는 현재 헤드와 필터링된 나머지 목록을 취하여
구성된 새 목록을 리턴한다. 조건부 조건이 실패하면, 필자는 필터링된 나머지로만 구성된 새 목록을
리턴한다(첫 번째 요소 삭제). Scala로 된 목록 구성 연산자는 두 경우에 대한 리턴 조건을 매우 가독성이 높고
이해하기 간편하게 만든다.
필자가 추측하건대, 독자가 현재는 재귀를 전혀 사용하지 않는다 — 이는 도구 상자의 일부도 아니다. 하지만, 이러한 일부 원인은 대부분의 명령형 언어가 이에 대한 지원이 활성화되지 않아 실제보다 사용하기에 훨씬 더 어렵게 된다는 사실에 달려있다. 깔끔한 구문과 지원을 추가하여 함수형 언어를 사용하면 재귀는 간단한 코드 재사용의 후보가 된다.
이번 기사는 함수적 사고 영역에서 기능에 대한 필자의 조사를 완성한다. 우연히도 이 기사의 대부분은 필터링에 대한 내용이라서 이를 사용하고 구현하는 방법을 많이 알려주었다. 하지만 이는 그렇게 놀라운 일도 아니다. 많은 함수형 패러다임은 목록과 관련하여 제작되었다. 왜냐하면 대부분의 프로그래밍 핵심은 목록을 처리하는 것이기 때문이다. 목록과 관련하여 더 강력한 기능을 가진 언어와 프레임워크 작성이 가능하다.
다음 기사에서 필자는 함수형 프로그래밍의 빌딩 블록 중 하나인 불변성(immutability)에 대해 깊이 논의할 것이다.
교육
- The Productive Programmer(Neal Ford저, O'Reilly Media, 2008년):
코딩 효율성을 개선하는 데 도움을 주는 도구와 연습에 대해 논의하는 Neal Ford의 가장 최신 저서이다.
- Scala: Scala는 JVM에서 현대적인 함수형 언어이다.
- Functional Java:
Functional Java는 Java로 많은 함수형 언어 구성을 추가하는 프레임워크이다.
-
The busy Java developer's guide to Scala:
Ted Neward의 이 developerWorks 시리즈에서 Scala에 대해 더 깊이 알아보자.
-
"Practically Groovy: Functional programming with curried closures"(Ken Barclay외 공저, developerWorks, 2005년 8월):
Groovy로 함수형 프로그래밍에 대해 자세히 읽어보자.
- Design Patterns: Elements of Reusable Object-Oriented Software(Erich Gamma 등, Addison-Wesley, 1994년): 설계 패턴에 대한 Gang of Four의 고전 작품이다.
-
기술 서점에서
다양한 기술 주제와 관련된 서적을 살펴보자.
-
developerWorks Java 기술 영역: Java 프로그래밍과 관련된 모든 주제를 다루는 여러 편의 기사를 찾아보자.
제품 및 기술
- ScalaTest:
ScalaTest는 Scala와 Java 코드에 대한 유닛 테스팅 라이브러리이다.
- IBM 제품 평가판을
다운로드하거나 IBM SOA Sandbox의
온라인 시험판을 살펴보고 DB2®,
Lotus®, Rational®, Tivoli® 및
WebSphere®의 애플리케이션 개발 도구 및 미들웨어 제품을 사용해 볼 수 있다.
토론
-
developerWorks 포럼 &블로그를 통해 developerWorks 커뮤니티에 참여하자.

Neal Ford는 글로벌 IT 컨설팅 업체인 ThoughtWorks의 소프트웨어 아키텍트이자 Meme Wrangler이다. 애플리케이션, 교육용 자료, 매거진 기사 및 비디오/DVD 프리젠테이션을 설계 및 개발하며 다양한 기술과 관련된 서적의 저자 또는 편집자이기도 하다. 최근에 출판된 책으로는 The Productive Programmer가 있다. 대규모 엔터프라이즈 애플리케이션의 설계 및 빌드에 많은 관심을 가지고 있는 그는 전세계의 개발자 컨퍼런스에서 국제적으로 인정 받고 있는 연사로도 활동하고 있다. 그의 웹 사이트를 살펴보자.