4.8. 트레일링 클로저
함수나 메서드 호출의 마지막 인자가 클로저일 때, 그 클로저를 괄호 밖으로 빼서 더 자연스러운 형태로 적을 수 있습니다. 이렇게 빼낸 형태를 트레일링 클로저라고 부릅니다.
4.8.1. 형태
트레일링 클로저의 문법은 다음과 같이 정의됩니다.
trailing_call ::= primary "(" arg_list? ")" trailing_closure
| primary trailing_closure // 0-arg call
trailing_closure ::= ("|" closure_params? "|" ("->" type)?)? block
함수(인자들) |매개변수| { 본체 }
함수(인자들) { 본체 } // 0-매개변수 단축
클로저는 호출 괄호 직후에 위치하며, 본체는 블록 표현식 의 블록입니다.
0-매개변수 단축: { body }
매개변수가 없는 클로저의 경우 || { body } 대신 { body } 형태로 단축해서 적을 수 있습니다.
let h = thread::spawn { heavy_work() };
// == thread::spawn(|| { heavy_work() })
with_lock(mutex) { critical_section() };
이 단축은 함수 호출 직후의 { ... } 에서만 의미가 있습니다. 다른 자리에 등장하는 { ... } 는 블록 표현식 의 일반 블록 표현식으로 해석됩니다.
1+ 매개변수
매개변수가 하나 이상인 경우에는 다음과 같이 적습니다.
let doubled = xs.map |x| { x * 2 };
let pos = xs.filter |x| { x > 0 };
let opened = with_file(path) |f| {
let data = f.read();
process(data)
};
pairs.for_each |k, v| { print(k, v) };
파이프 (|...|) 는 괄호 밖, 본체 직전에 위치합니다. 본체는 항상 블록이어서 |x| body 같은 한 줄 형태가 아니라 언제나 |x| { body } 형태로 적습니다.
트레일링 클로저는 가독성과 일관성을 위해 블록 본체를 기본으로 삼고 있습니다.
괄호 없는 형태의 규칙
xs.map |x| { .. } 처럼 호출 괄호 없이 클로저를 붙이는 형태는 다음 조건에서만 트레일링 클로저입니다.
- 바로 앞이 경로 (
thread::spawn,f) 이거나 멤버 이름 (.map) 입니다. - 뒤에
|매개변수|, 선택적-> R, 그리고{가 이어집니다. 이 모양이 아니면|는 비트 OR 입니다. ||는 언제나 논리 OR 입니다. 매개변수 없는 클로저는{ body }단축으로 씁니다.if/while/match의 머리 (조건과 scrutinee) 안에서는 괄호 없는 트레일링 클로저와{ body }단축이 꺼집니다. 그 자리의{는 본체 블록을 엽니다. 머리에서 클로저가 필요하면 괄호로 씁니다 (if xs.any(|x| { x > 0 }) { .. }).
let ys = xs.map |x| -> i64 { x as i64 * 2 }; // 반환 타입 명시
let z = a | b; // 비트 OR - 뒤에 `|params| {` 모양이 아님
if ok || retry { .. } // 논리 OR
4.8.2. 명시 괄호와의 자유 선택
트레일링 클로저는 허용일 뿐 강제가 아닙니다.
xs.map(|x| x * 2); // 명시 괄호 - OK
xs.map |x| { x * 2 }; // 트레일링 - OK
따라서 팀이나 사용자 스타일에 따라 자유롭게 골라 쓸 수 있습니다.
4.8.3. 함수 호출의 마지막 인자가 아닌 경우
트레일링 클로저는 마지막 인자 자리만 채울 수 있습니다. 클로저가 중간 인자라면 명시 괄호를 사용해야 합니다.
fn process(input: str, transform: fn(str) -> str, output: str) -> str { ... }
process("in", |s| { s.to_upper() }, "out"); // 명시 괄호 (중간 인자)
process("in", "out") |s| { s.to_upper() }; // ERROR - 트레일링은 마지막 자리만
4.8.4. 인자 0 개 호출
호출에 인자가 없을 때는 빈 괄호도 생략할 수 있습니다.
thread::spawn { heavy() }; // thread::spawn() { heavy() } 와 동일
// == thread::spawn(|| { heavy() })
thread::spawn 처럼 매개변수가 클로저 한 개라면 위 형태가 가장 자연스러운 호출 방식이 됩니다.
4.8.5. 중첩
트레일링 클로저는 자유롭게 중첩할 수 있습니다.
par::scope |s| {
s.spawn { f() };
s.spawn { g() };
};
내부 클로저에도 같은 단축 규칙 ({ body } ↔ || { body }) 을 그대로 적용할 수 있습니다.
4.8.6. 파서 모호성 처리
트레일링 클로저를 도입하면 일부 코드가 블록 표현식 또는 트레일링 클로저로 해석될 수 있는 모호함이 생깁니다. 다음 규칙으로 이 모호함을 해소합니다.
| 위치 | 의미 |
|---|---|
제어 키워드 (if, else, for, while, loop, match) 뒤 { ... } | 제어 본체 |
함수 호출 직후 { ... } | 트레일링 클로저 (0-arg) |
함수 호출 직후 |params| { ... } | 트레일링 클로저 (1+ arg) |
x = { ... }; 같은 일반 표현식 자리 { ... } | 블록 표현식 (즉시 평가) - 0-arg 클로저는 명시적 || { ... } |
식 안 |...| { ... } | 일반 클로저 식 |
핵심 규칙을 요약하면, 함수 호출 직후 ((args)) 에 공백이 있든 없든 { ... } 또는 |...| { ... } 가 따라오면 트레일링 클로저로 해석됩니다.
4.8.7. {} 단독의 일반 블록과 0-arg 클로저
let x = { 42 }; // 블록 표현식 (값 42)
let f = { 42 }; // ??
위 두 줄은 문맥상 같은 형태인데, 어떤 의미가 우선이 될까요.
규칙은 이렇습니다. { body } 가 식 자리에 단독으로 등장하면 블록 표현식으로 해석합니다.
만약 0-arg 클로저를 의도한다면 명시적으로 || { body } 를 적어야 합니다.
let f = || { 42 }; // 명시 0-arg 클로저
let r = f(); // 호출
즉, 함수 호출 직후의 { body } 만 트레일링 클로저로 인식됩니다.
4.8.8. par for 같은 문 자리의 클로저
병렬 루프, 트랜잭션, 컨텍스트 매니저 같은 자리에서 트레일링 클로저는 키워드처럼 읽히는 DSL 친화적인 형태를 만들어 줍니다.
par::for_each(xs) |x| { ... } // par for 처럼 보임
with_lock(mutex) { ... } // with-statement 처럼 보임
retry(3) { ... } // retry 키워드처럼 보임
이는 키워드 의 예약 키워드 최소화 원칙과도 일관됩니다. 라이브러리 함수와 트레일링 클로저를 결합하면 제어 흐름처럼 읽히는 패턴을 만들 수 있습니다.
4.8.9. 이름 있는 인자와의 결합
fn run(name: str = "task", body: fn() -> ()) { ... }
run("compute") { ... }; // 위치 + 트레일링 OK
run(name: "compute") { ... }; // named + 트레일링 OK
위치 인자와 named 인자, 그리고 트레일링 클로저는 자유롭게 결합할 수 있습니다.
4.8.10. 정리
트레일링 클로저는 사양 수준에서 추가 키워드 없이도 DSL 같은 표현을 가능하게 합니다. 사용자가 언어 키워드처럼 보이는 패턴을 라이브러리로 직접 만들 수 있다는 점이 핵심입니다.