Groovy 클로저
Groovy 클로저
이 장에서는 Groovy의 클로저(Closure)에 대해 다뤄요. Groovy에서 클로저는 인자를 받을 수 있고, 값을 반환할 수 있으며, 변수에 담을 수도 있는 열린 익명 코드 블록이에요. 클로저는 자신을 둘러싼 스코프에서 선언된 변수를 참조할 수 있죠. 다만 함수형 프로그래밍에서 말하는 정식 클로저의 정의와는 달리, Groovy의 Closure는 자기 주변 스코프 밖에서 정의된 자유 변수도 담을 수 있어요. 엄밀한 의미에서 클로저의 개념을 벗어나긴 하지만, 그 덕분에 이 장에서 설명할 여러 장점들이 생깁니다.
출처: Groovy 공식 문서
본문
1. 문법
1.1. 클로저 정의하기
클로저 정의는 다음 문법을 따릅니다.
{ [closureParameters -> ] statements }
여기서 [closureParameters->]는 쉼표로 구분된 (선택적인) 매개변수 목록이고, statements는 0개 이상의 Groovy 문장이에요. 매개변수 형태는 메서드의 매개변수 목록과 비슷하며, 타입을 붙일 수도 있고 붙이지 않을 수도 있습니다.
매개변수 목록을 명시하면 ->(화살표)가 필수로 들어가야 해요. 이 화살표가 인자와 클로저 본문을 구분해 주죠. statements 부분은 0개, 1개 또는 여러 개의 Groovy 문장으로 이루어집니다.
유효한 클로저 정의의 예를 몇 가지 볼게요.
{ item++ } (1)
{ -> item++ } (2)
{ println it } (3)
{ it -> println it } (4)
{ name -> println name } (5)
{ String x, int y -> (6)
println "hey ${x} the value is ${y}"
}
{ reader -> (7)
def line = reader.readLine()
line.trim()
}
(1) — item이라는 변수를 참조하는 클로저
(2) — 화살표(->)를 붙여 클로저 매개변수와 코드를 명시적으로 구분할 수도 있어요
(3) — 암시적 매개변수(it)를 사용하는 클로저
(4) — it을 명시적 매개변수로 쓴 대안 버전
(5) — 그럴 때는 매개변수에 명확한 이름을 붙이는 편이 나아요
(6) — 타입이 지정된 두 매개변수를 받는 클로저
(7) — 클로저는 여러 문장을 담을 수 있어요
1.2. 객체로서의 클로저
클로저는 groovy.lang.Closure 클래스의 인스턴스예요. 그래서 코드 블록임에도 다른 변수와 똑같이 변수나 필드에 담을 수 있습니다.
def listener = { e -> println "Clicked on $e.source" } (1)
assert listener instanceof Closure
Closure callback = { println 'Done!' } (2)
Closure<Boolean> isTextFile = {
File it -> it.name.endsWith('.txt') (3)
}
(1) — 클로저를 변수에 담을 수 있고, 이 클로저는 groovy.lang.Closure의 인스턴스예요
(2) — def나 var를 쓰지 않는다면 타입으로 groovy.lang.Closure를 사용해요
(3) — 선택적으로 groovy.lang.Closure의 제네릭 타입을 이용해 반환 타입을 지정할 수도 있어요
1.3. 클로저 호출하기
익명 코드 블록인 클로저는 다른 메서드처럼 호출할 수 있어요. 다음과 같이 인자가 없는 클로저를 정의했다고 해볼게요.
def code = { 123 }
클로저 안의 코드는 여러분이 클로저를 호출할 때만 실행돼요. 일반 메서드처럼 변수를 호출하면 되죠.
assert code() == 123
아니면 명시적으로 call 메서드를 쓸 수도 있어요.
assert code.call() == 123
클로저가 인자를 받는 경우에도 원리는 똑같습니다.
def isOdd = { int i -> i%2 != 0 } (1)
assert isOdd(3) == true (2)
assert isOdd.call(2) == false (3)
def isEven = { it%2 == 0 } (4)
assert isEven(3) == false (5)
assert isEven.call(2) == true (6)
(1) — int 타입 인자를 받는 클로저를 정의해요
(2) — 직접 호출할 수 있죠
(3) — 또는 call 메서드로 호출해요
(4) — 암시적 인자(it)를 쓰는 클로저도 마찬가지예요
(5) — (arg)로 직접 호출할 수 있고
(6) — call로도 호출할 수 있어요
메서드와 달리 클로저는 호출할 때 항상 값을 반환합니다. 다음 절에서는 클로저 인자를 어떻게 선언하는지, 언제 쓰는지, 그리고 암시적 매개변수 it이 무엇인지 살펴볼게요.
2. 매개변수
2.1. 일반 매개변수
클로저의 매개변수는 일반 메서드의 매개변수와 같은 원리를 따릅니다.
- 선택적인 타입
- 이름
- 선택적인 기본값
매개변수는 쉼표로 구분해요.
def closureWithOneArg = { str -> str.toUpperCase() }
assert closureWithOneArg('groovy') == 'GROOVY'
def closureWithOneArgAndExplicitType = { String str -> str.toUpperCase() }
assert closureWithOneArgAndExplicitType('groovy') == 'GROOVY'
def closureWithTwoArgs = { a,b -> a+b }
assert closureWithTwoArgs(1,2) == 3
def closureWithTwoArgsAndExplicitTypes = { int a, int b -> a+b }
assert closureWithTwoArgsAndExplicitTypes(1,2) == 3
def closureWithTwoArgsAndOptionalTypes = { a, int b -> a+b }
assert closureWithTwoArgsAndOptionalTypes(1,2) == 3
def closureWithTwoArgAndDefaultValue = { int a, int b=2 -> a+b }
assert closureWithTwoArgAndDefaultValue(1) == 3
2.2. 암시적 매개변수
클로저가 ->를 이용해 매개변수 목록을 명시하지 않으면, 클로저는 항상 it이라는 이름의 암시적 매개변수를 하나 정의해요. 즉 다음 코드는
def greeting = { "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'
아래 코드와 완전히 동일합니다.
def greeting = { it -> "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'
인자를 받지 않는 클로저를 선언하고 싶고, 반드시 인자 없이만 호출되도록 제한하고 싶다면, 빈 매개변수 목록을 명시적으로 선언해야 해요.
def magicNumber = { -> 42 }
// 이 호출은 실패해요. 클로저가 인자를 받지 않기 때문이죠
magicNumber(11)
2.3. 가변 인자(Varargs)
클로저도 다른 메서드처럼 가변 인자를 선언할 수 있어요. 가변 인자 메서드는 마지막 매개변수가 가변 길이(또는 배열)일 때, 인자를 여러 개 받을 수 있는 메서드를 말해요. 다음 예시를 볼게요.
def concat1 = { String... args -> args.join('') } (1)
assert concat1('abc','def') == 'abcdef' (2)
def concat2 = { String[] args -> args.join('') } (3)
assert concat2('abc', 'def') == 'abcdef'
def multiConcat = { int n, String... args -> (4)
args.join('')*n
}
assert multiConcat(2, 'abc','def') == 'abcdefabcdef'
(1) — 첫 매개변수로 문자열을 여러 개 받는 클로저
(2) — 배열로 감싸지 않고도 인자를 몇 개든 넘겨서 호출할 수 있어요
(3) — args 매개변수를 배열로 선언해도 같은 동작을 바로 쓸 수 있죠
(4) — 마지막 매개변수가 배열이거나 명시적 가변 인자 타입이면 됩니다
3. 위임 전략(Delegation strategy)
3.1. Groovy 클로저 vs 람다 표현식
Groovy는 클로저를 Closure 클래스의 인스턴스로 정의해요. 그래서 Java 8의 람다 표현식과는 꽤 다릅니다. 위임(delegation)은 Groovy 클로저의 핵심 개념인데, 람다에는 이에 해당하는 개념이 없어요. 클로저의 delegate를 바꾸거나 위임 전략을 바꾸는 능력 덕분에 Groovy에서 멋진 도메인 특화 언어(DSL)를 설계할 수 있습니다.
3.2. Owner, delegate 그리고 this
delegate의 개념을 이해하려면 먼저 클로저 안에서 this가 무엇을 의미하는지 설명해야 해요. 클로저는 사실 세 가지 서로 다른 것을 정의합니다.
this는 클로저가 정의된 바깥 클래스를 가리켜요owner는 클로저가 정의된 바깥 객체를 가리키는데, 그 객체는 클래스일 수도 있고 클로저일 수도 있어요delegate는 메시지의 수신자가 명시되지 않았을 때 메서드 호출이나 프로퍼티 접근이 해석되는 제3의 객체를 가리켜요
3.2.1. this의 의미
클로저 안에서 getThisObject를 호출하면 클로저가 정의된 바깥 클래스를 돌려줘요. 명시적으로 this를 쓰는 것과 같죠.
class Enclosing {
void run() {
def whatIsThisObject = { getThisObject() } (1)
assert whatIsThisObject() == this (2)
def whatIsThis = { this } (3)
assert whatIsThis() == this (4)
}
}
class EnclosedInInnerClass {
class Inner {
Closure cl = { this } (5)
}
void run() {
def inner = new Inner()
assert inner.cl() == inner (6)
}
}
class NestedClosures {
void run() {
def nestedClosures = {
def cl = { this } (7)
cl()
}
assert nestedClosures() == this (8)
}
}
(1) — 클로저가 Enclosing 클래스 안에 정의되어 있고, getThisObject를 반환해요
(2) — 클로저를 호출하면 클로저가 정의된 Enclosing의 인스턴스를 돌려줘요
(3) — 보통은 this 축약 표기만 쓰면 되죠
(4) — 정확히 같은 객체를 반환합니다
(5) — 클로저가 내부 클래스에 정의되어 있다면
(6) — 클로저 안의 this는 최상위 클래스가 아니라 내부 클래스를 반환해요
(7) — 중첩 클로저의 경우, 여기처럼 cl이 nestedClosures의 스코프 안에 정의되어 있을 때
(8) — this는 둘러싼 클로저가 아니라 가장 가까운 바깥 클래스에 해당합니다!
물론 이렇게 바깥 클래스의 메서드를 호출하는 것도 가능해요.
class Person {
String name
int age
String toString() { "$name is $age years old" }
String dump() {
def cl = {
String msg = this.toString() (1)
println msg
msg
}
cl()
}
}
def p = new Person(name:'Janice', age:74)
assert p.dump() == 'Janice is 74 years old'
(1) — 클로저가 this에 대해 toString을 호출하는데, 실제로는 바깥 객체, 즉 Person 인스턴스의 toString 메서드를 호출하게 돼요
3.2.2. 클로저의 owner
클로저의 owner는 클로저 안의 this 정의와 아주 비슷하지만 한 가지 미묘한 차이가 있어요. owner는 클로저든 클래스든 바로 바깥의 객체를 반환합니다.
class Enclosing {
void run() {
def whatIsOwnerMethod = { getOwner() } (1)
assert whatIsOwnerMethod() == this (2)
def whatIsOwner = { owner } (3)
assert whatIsOwner() == this (4)
}
}
class EnclosedInInnerClass {
class Inner {
Closure cl = { owner } (5)
}
void run() {
def inner = new Inner()
assert inner.cl() == inner (6)
}
}
class NestedClosures {
void run() {
def nestedClosures = {
def cl = { owner } (7)
cl()
}
assert nestedClosures() == nestedClosures (8)
}
}
(1) — 클로저가 Enclosing 클래스 안에 정의되어 있고, getOwner를 반환해요
(2) — 클로저를 호출하면 클로저가 정의된 Enclosing의 인스턴스를 돌려줘요
(3) — 보통은 owner 축약 표기만 쓰면 되죠
(4) — 정확히 같은 객체를 반환합니다
(5) — 클로저가 내부 클래스에 정의되어 있다면
(6) — 클로저 안의 owner는 최상위 클래스가 아니라 내부 클래스를 반환해요
(7) — 하지만 중첩 클로저의 경우, 여기처럼 cl이 nestedClosures의 스코프 안에 정의되어 있을 때
(8) — owner는 둘러싼 클로저에 해당하므로 this와는 다른 객체예요!
3.2.3. 클로저의 delegate
클로저의 delegate는 delegate 프로퍼티로 접근하거나 getDelegate 메서드를 호출해서 얻을 수 있어요. Groovy에서 도메인 특화 언어를 만들 때 아주 강력한 개념이죠. this와 owner가 클로저의 어휘적 스코프를 가리키는 반면, delegate는 클로저가 사용할 사용자 정의 객체예요. 기본적으로 delegate는 owner로 설정됩니다.
class Enclosing {
void run() {
def cl = { getDelegate() } (1)
def cl2 = { delegate } (2)
assert cl() == cl2() (3)
assert cl() == this (4)
def enclosed = {
{ -> delegate }.call() (5)
}
assert enclosed() == enclosed (6)
}
}
(1) — getDelegate 메서드를 호출해서 delegate를 얻을 수 있어요
(2) — 또는 delegate 프로퍼티를 써요
(3) — 둘 다 같은 객체를 반환해요
(4) — 그 객체는 둘러싼 클래스나 클로저죠
(5) — 특히 중첩 클로저의 경우에 그렇습니다
(6) — delegate는 owner에 해당해요
클로저의 delegate는 어떤 객체로든 바꿀 수 있어요. 서로 서브클래스가 아니면서 둘 다 name이라는 프로퍼티를 정의하는 두 클래스를 만들어서 확인해 볼게요.
class Person {
String name
}
class Thing {
String name
}
def p = new Person(name: 'Norman')
def t = new Thing(name: 'Teapot')
그런 다음 delegate에서 name 프로퍼티를 가져오는 클로저를 정의해요.
def upperCasedName = { delegate.name.toUpperCase() }
클로저의 delegate를 바꾸면 대상 객체가 달라지는 걸 볼 수 있어요.
upperCasedName.delegate = p
assert upperCasedName() == 'NORMAN'
upperCasedName.delegate = t
assert upperCasedName() == 'TEAPOT'
이 시점까지만 보면, 클로저의 어휘적 스코프에 target 변수를 정의해 둔 것과 크게 다르지 않아 보일 수 있어요.
def target = p
def upperCasedNameUsingVar = { target.name.toUpperCase() }
assert upperCasedNameUsingVar() == 'NORMAN'
하지만 큰 차이가 몇 가지 있습니다.
- 마지막 예시에서
target은 클로저 안에서 참조되는 지역 변수예요 - delegate는 투명하게 사용할 수 있어요. 즉 다음 문단에서 설명하듯 메서드 호출 앞에
delegate.을 붙일 필요가 없죠
3.2.4. 위임 전략(Delegation strategy)
클로저 안에서 수신 객체를 명시하지 않고 프로퍼티에 접근할 때마다 위임 전략이 개입합니다.
class Person {
String name
}
def p = new Person(name:'Igor')
def cl = { name.toUpperCase() } (1)
cl.delegate = p (2)
assert cl() == 'IGOR' (3)
(1) — name은 클로저의 어휘적 스코프 안의 변수를 가리키지 않아요
(2) — 클로저의 delegate를 Person의 인스턴스로 바꿀 수 있어요
(3) — 그러면 메서드 호출이 성공해요
이 코드가 동작하는 이유는 name 프로퍼티가 delegate 객체에서 투명하게 해석되기 때문이에요! 이는 클로저 안에서 프로퍼티나 메서드 호출을 해석하는 아주 강력한 방법이죠. 명시적으로 delegate. 수신자를 붙일 필요가 없어요. 클로저의 기본 위임 전략이 그렇게 만들어 주기 때문이에요. 클로저에는 선택할 수 있는 여러 해석 전략이 실제로 정의되어 있습니다.
Closure.OWNER_FIRST는 기본 전략이에요. owner에 프로퍼티/메서드가 있으면 owner에서 호출하고, 없으면 delegate를 사용해요Closure.DELEGATE_FIRST는 그 반대예요. delegate를 먼저 쓰고, 그다음 owner를 써요Closure.OWNER_ONLY는 프로퍼티/메서드 탐색을 owner에서만 수행해요. delegate는 무시되죠Closure.DELEGATE_ONLY는 프로퍼티/메서드 탐색을 delegate에서만 수행해요. owner는 무시됩니다Closure.TO_SELF는 고급 메타프로그래밍 기법이 필요하거나 커스텀 해석 전략을 구현하고 싶은 개발자를 위한 거예요. owner나 delegate가 아니라 클로저 클래스 자체에서만 해석해요. 이걸 쓰는 게 의미가 있으려면 여러분이 직접Closure의 서브클래스를 구현해야 합니다
기본 "owner first" 전략을 다음 코드로 확인해 볼게요.
class Person {
String name
def pretty = { "My name is $name" } (1)
String toString() {
pretty()
}
}
class Thing {
String name (2)
}
def p = new Person(name: 'Sarah')
def t = new Thing(name: 'Teapot')
assert p.toString() == 'My name is Sarah' (3)
p.pretty.delegate = t (4)
assert p.toString() == 'My name is Sarah' (5)
(1) — 설명을 위해 "name"을 참조하는 클로저 멤버를 정의해요
(2) — Person과 Thing 클래스 모두 name 프로퍼티를 정의해요
(3) — 기본 전략을 쓰면 name 프로퍼티가 먼저 owner에서 해석돼요
(4) — 그래서 delegate를 Thing의 인스턴스인 t로 바꿔도
(5) — 결과가 바뀌지 않아요. name을 먼저 클로저의 owner에서 해석하기 때문이죠
물론 클로저의 해석 전략을 바꾸는 것도 가능해요.
p.pretty.resolveStrategy = Closure.DELEGATE_FIRST
assert p.toString() == 'My name is Teapot'
resolveStrategy를 바꾸면 Groovy가 "암시적 this" 참조를 해석하는 방식을 바꾸는 거예요. 이 경우 name을 먼저 delegate에서 찾고, 없으면 owner에서 찾아요. delegate가 Thing 인스턴스이므로 name이 delegate에 정의되어 있고, 그 값이 사용됩니다.
"delegate first"와 "delegate only"(또는 "owner first"와 "owner only")의 차이는 delegate(각각 owner)에 그런 메서드나 프로퍼티가 없을 때 드러나요.
class Person {
String name
int age
def fetchAge = { age }
}
class Thing {
String name
}
def p = new Person(name:'Jessica', age:42)
def t = new Thing(name:'Printer')
def cl = p.fetchAge
cl.delegate = p
assert cl() == 42 (1)
cl.delegate = t
assert cl() == 42 (1)
cl.resolveStrategy = Closure.DELEGATE_ONLY
cl.delegate = p
assert cl() == 42 (2)
cl.delegate = t
try {
cl() (3)
assert false
} catch (MissingPropertyException ex) {
// "age" is not defined on the delegate
}
(1) — "owner first"에서는 delegate가 무엇이든 상관없어요
(2) — "delegate only"에서 delegate를 p로 하면 성공해요
(3) — "delegate only"에서 delegate를 t로 하면 실패해요
이 예시에서는 둘 다 name 프로퍼티를 갖지만 age는 Person 클래스만 선언하는 두 클래스를 정의했어요. Person 클래스는 age를 참조하는 클로저도 선언하죠. 기본 해석 전략을 "owner first"에서 "delegate only"로 바꿀 수 있어요. 클로저의 owner가 Person 클래스이므로, delegate가 Person 인스턴스면 클로저 호출이 성공하지만 delegate가 Thing 인스턴스면 groovy.lang.MissingPropertyException과 함께 실패하는 걸 확인할 수 있어요. 클로저가 Person 클래스 안에 정의되어 있어도 owner는 사용되지 않는 거죠.
이 기능을 활용해 DSL을 개발하는 방법에 대한 종합적인 설명은 매뉴얼의 별도 섹션에서 확인할 수 있어요.
3.2.5. 메타프로그래밍이 있을 때의 위임 전략
"owner first" 위임 전략을 설명하며 owner의 프로퍼티/메서드가 "존재하면" 사용하고 아니면 delegate의 것을 사용한다고 말했었죠. "delegate first"도 반대 방향으로 같은 이야기예요. 여기서 "존재하면"이라는 표현보다는 "처리되면(handled)"이라는 표현이 더 정확해요. 즉 "owner first"의 경우, 프로퍼티/메서드가 owner에 있거나 owner가 propertyMissing/methodMissing 훅을 가지고 있으면 owner가 멤버 접근을 처리한다는 뜻이에요.
이전 예시를 살짝 바꾼 코드로 확인해 볼게요.
class Person {
String name
int age
def fetchAge = { age }
}
class Thing {
String name
def propertyMissing(String name) { -1 }
}
def p = new Person(name:'Jessica', age:42)
def t = new Thing(name:'Printer')
def cl = p.fetchAge
cl.resolveStrategy = Closure.DELEGATE_FIRST
cl.delegate = p
assert cl() == 42
cl.delegate = t
assert cl() == -1
이 예시에서 우리의 Thing 클래스 인스턴스(마지막 cl 사용에서의 delegate)에는 age 프로퍼티가 없지만, propertyMissing 훅으로 없는 프로퍼티를 처리하기 때문에 age가 -1로 나와요.
4. GString 안의 클로저
다음 코드를 봐볼게요.
def x = 1
def gs = "x = ${x}"
assert gs == 'x = 1'
이 코드는 예상대로 동작해요. 그런데 다음을 추가하면 어떻게 될까요?
x = 2
assert gs == 'x = 2'
이 assert는 실패해요! 여기에는 두 가지 이유가 있습니다.
- GString은 값의
toString표현을 느긋하게(lazily) 평가해요 - GString에서
$ {x}문법은 클로저가 아니라$x에 대한 표현식이며, GString이 생성될 때 평가돼요
이 예시에서 GString은 x를 참조하는 표현식과 함께 생성돼요. GString이 만들어질 때 x의 값은 1이므로, GString은 1이라는 값으로 생성되죠. assert가 실행될 때 GString이 평가되고 1이 toString으로 String으로 변환됩니다. x를 2로 바꾸면 x의 값은 바뀌지만, 이는 서로 다른 객체이고 GString은 여전히 예전 객체를 가리키고 있어요.
GString은 참조하는 값이 **변경(mutate)**될 때만
toString표현이 바뀌어요. 참조 자체가 바뀌면 아무 일도 일어나지 않아요.
GString 안에서 진짜 클로저가 필요하고, 예를 들어 변수의 느긋한 평가를 강제하고 싶다면, 다음처럼 ${→ x} 대체 문법을 써야 해요.
def x = 1
def gs = "x = ${-> x}"
assert gs == 'x = 1'
x = 2
assert gs == 'x = 2'
변경(mutation)과 어떻게 다른지 다음 코드로 확인해 볼게요.
class Person {
String name
String toString() { name } (1)
}
def sam = new Person(name:'Sam') (2)
def lucy = new Person(name:'Lucy') (3)
def p = sam (4)
def gs = "Name: ${p}" (5)
assert gs == 'Name: Sam' (6)
p = lucy (7)
assert gs == 'Name: Sam' (8)
sam.name = 'Lucy' (9)
assert gs == 'Name: Lucy' (10)
(1) — Person 클래스는 name 프로퍼티를 반환하는 toString 메서드를 가져요
(2) — Sam이라는 이름의 Person을 먼저 만들어요
(3) — Lucy라는 이름의 Person을 하나 더 만들어요
(4) — p 변수를 Sam으로 설정해요
(5) — p의 값, 즉 Sam을 참조하는 클로저가 생성돼요
(6) — 문자열을 평가하면 Sam이 반환됩니다
(7) — p를 Lucy로 바꾸면
(8) — 문자열은 여전히 Sam을 평가해요. GString 생성 당시 p의 값이었기 때문이죠
(9) — 그래서 Sam을 변경해 이름을 Lucy로 바꾸면
(10) — 이번에는 GString이 제대로 변경됩니다
객체를 변경하거나 감싸는 것에 의존하고 싶지 않다면, 빈 인자 목록을 명시적으로 선언해서 GString 안에서 클로저를 써야 해요.
class Person {
String name
String toString() { name }
}
def sam = new Person(name:'Sam')
def lucy = new Person(name:'Lucy')
def p = sam
// "p"를 느긋하게 평가하는 GString 만들기
def gs = "Name: ${-> p}"
assert gs == 'Name: Sam'
p = lucy
assert gs == 'Name: Lucy'
5. 클로저 강제 변환(Closure coercion)
클로저는 인터페이스나 단일 추상 메서드(SAM) 타입으로 변환될 수 있어요. 자세한 설명은 매뉴얼의 해당 섹션을 참고하세요.
6. 함수형 프로그래밍
클로저는 Java 8의 람다 표현식처럼 Groovy에서 함수형 프로그래밍 패러다임의 핵심이에요. 함수에 대한 일부 함수형 프로그래밍 연산은 Closure 클래스에서 바로 사용할 수 있어요. 이 섹션에서 확인해 볼게요.
6.1. 커링(Currying)
Groovy에서 커링은 부분 적용(partial application)의 개념을 가리켜요. Groovy가 클로저에 적용하는 스코프 규칙이 다르기 때문에, 함수형 프로그래밍에서 말하는 진짜 커링 개념과는 일치하지 않아요. Groovy의 커링은 클로저의 매개변수 하나에 값을 설정하고, 인자를 하나 덜 받는 새 클로저를 반환하게 해 줍니다.
6.1.1. 왼쪽 커링(Left currying)
왼쪽 커링은 클로저의 가장 왼쪽 매개변수에 값을 설정하는 거예요. 예를 들어 볼게요.
def nCopies = { int n, String str -> str*n } (1)
def twice = nCopies.curry(2) (2)
assert twice('bla') == 'blabla' (3)
assert twice('bla') == nCopies(2, 'bla') (4)
(1) — nCopies 클로저는 두 매개변수를 정의해요
(2) — curry가 첫 매개변수를 2로 설정하고, String 하나만 받는 새 클로저(함수)를 만들어요
(3) — 그래서 새 함수는 String만으로 호출할 수 있어요
(4) — 두 매개변수로 nCopies를 호출하는 것과 동일해요
6.1.2. 오른쪽 커링(Right currying)
왼쪽 커링과 비슷하게, 클로저의 가장 오른쪽 매개변수에도 값을 설정할 수 있어요.
def nCopies = { int n, String str -> str*n } (1)
def blah = nCopies.rcurry('bla') (2)
assert blah(2) == 'blabla' (3)
assert blah(2) == nCopies(2, 'bla') (4)
(1) — nCopies 클로저는 두 매개변수를 정의해요
(2) — rcurry가 마지막 매개변수를 bla로 설정하고, int 하나만 받는 새 클로저(함수)를 만들어요
(3) — 그래서 새 함수는 int만으로 호출할 수 있어요
(4) — 두 매개변수로 nCopies를 호출하는 것과 동일해요
6.1.3. 인덱스 기반 커링(Index based currying)
클로저가 2개보다 많은 매개변수를 받는 경우, ncurry로 임의의 매개변수에 값을 설정할 수 있어요.
def volume = { double l, double w, double h -> l*w*h } (1)
def fixedWidthVolume = volume.ncurry(1, 2d) (2)
assert volume(3d, 2d, 4d) == fixedWidthVolume(3d, 4d) (3)
def fixedWidthAndHeight = volume.ncurry(1, 2d, 4d) (4)
assert volume(3d, 2d, 4d) == fixedWidthAndHeight(3d) (5)
(1) — volume 함수는 3개의 매개변수를 정의해요
(2) — ncurry가 두 번째 매개변수(index = 1)를 2d로 설정하고, 길이와 높이를 받는 새 volume 함수를 만들어요
(3) — 그 함수는 volume에서 너비를 생략한 것과 동일해요
(4) — 지정된 인덱스부터 시작해 여러 매개변수를 설정하는 것도 가능해요
(5) — 결과 함수는 원래 함수의 매개변수 수에서 ncurry로 설정된 매개변수 수를 뺀 만큼의 인자를 받습니다
6.2. 메모이제이션(Memoization)
메모이제이션은 클로저 호출 결과를 캐시할 수 있게 해 줘요. 함수(클로저)가 하는 계산이 느린데, 같은 인자로 자주 호출된다는 걸 안다면 유용하죠. 대표적인 예가 피보나치 수열이에요. 순진한 구현은 다음과 같을 수 있어요.
def fib
fib = { long n -> n<2?n:fib(n-1)+fib(n-2) }
assert fib(15) == 610 // slow!
fib가 같은 인자로 재귀 호출되는 경우가 많아서 지수적 알고리즘이 되기 때문에 이건 순진한 구현이에요.
- fib(15)를 계산하려면 fib(14)와 fib(13)의 결과가 필요해요
- fib(14)를 계산하려면 fib(13)과 fib(12)의 결과가 필요해요
호출이 재귀적이므로 같은 값을 계속 다시 계산하게 됩니다. 물론 캐시할 수 있는 값인데도요. 이 순진한 구현은 memoize로 호출 결과를 캐시하면 "고칠" 수 있어요.
fib = { long n -> n<2?n:fib(n-1)+fib(n-2) }.memoize()
assert fib(25) == 75025 // fast!
캐시는 인자의 실제 값을 기준으로 동작해요. 따라서 기본형이나 박싱된 기본형이 아닌 다른 것으로 메모이제이션을 쓸 때는 아주 조심해야 해요.
캐시의 동작은 대체 메서드들로 조정할 수 있어요.
memoizeAtMost는 최대 n개의 값을 캐시하는 새 클로저를 생성해요memoizeAtLeast는 최소 n개의 값을 캐시하는 새 클로저를 생성해요memoizeBetween은 최소 n개에서 최대 n개의 값을 캐시하는 새 클로저를 생성해요
모든 memoize 변형에서 사용하는 캐시는 LRU 캐시예요.
6.3. 합성(Composition)
클로저 합성은 함수 합성의 개념에 해당해요. 즉 두 개 이상의 함수를 합성(호출을 연결)해서 새 함수를 만드는 거죠. 다음 예시를 볼게요.
def plus2 = { it + 2 }
def times3 = { it * 3 }
def times3plus2 = plus2 << times3
assert times3plus2(3) == 11
assert times3plus2(4) == plus2(times3(4))
def plus2times3 = times3 << plus2
assert plus2times3(3) == 15
assert plus2times3(5) == times3(plus2(5))
// reverse composition
assert times3plus2(3) == (times3 >> plus2)(3)
6.4. 트램펄린(Trampoline)
재귀 알고리즘은 종종 물리적 한계, 즉 최대 스택 깊이에 제약을 받아요. 예를 들어 자기 자신을 너무 깊이 재귀 호출하는 메서드를 호출하면 결국 StackOverflowException을 받게 됩니다.
그런 상황을 돕는 접근법 중 하나가 Closure와 그 트램펄린(trampoline) 기능을 사용하는 거예요.
클로저는 TrampolineClosure로 감싸집니다. 호출되면 트램펄린된 Closure는 원래 Closure를 호출해 결과를 기다려요. 호출 결과가 또 다른 TrampolineClosure 인스턴스라면(아마도 trampoline() 메서드 호출 결과로 생성됐겠죠), Closure가 다시 호출돼요. 이렇게 반환된 트램펄린 클로저 인스턴스의 반복 호출은 트램펄린 클로저가 아닌 다른 값이 반환될 때까지 계속됩니다. 그 값이 트램펄린의 최종 결과가 되죠. 이렇게 하면 스택을 채우는 대신 호출이 직렬로 이루어집니다.
팩토리얼 함수를 구현할 때 trampoline()을 쓰는 예시를 볼게요.
def factorial
factorial = { int n, def accu = 1G ->
if (n < 2) return accu
factorial.trampoline(n - 1, n * accu)
}
factorial = factorial.trampoline()
assert factorial(1) == 1
assert factorial(3) == 1 * 2 * 3
assert factorial(1000) // == 402387260.. plus another 2560 digits
6.5. 메서드 포인터(Method pointers)
일반 메서드를 클로저로 사용할 수 있으면 편리할 때가 많아요. 예를 들어 클로저의 커링 기능을 쓰고 싶은데 일반 메서드에는 그런 기능이 없을 수 있죠. Groovy에서는 메서드 포인터 연산자로 어떤 메서드에서든 클로저를 얻을 수 있습니다.