표현식 래퍼: #%expression
표현식 래퍼: #%expression
폼(form)을 표현식으로 강제로 파싱하게 해 주는 #%expression을 살펴볼게요. 재귀적 정의 컨텍스트에서 뒤따르는 정의의 컴파일 타임 정보를 현재 표현식에 활용해야 할 때 특히 유용해요.
출처: Racket Reference
본문
syntax
(#%expression expr)
expr과 같은 결과를 만들어요. #%expression을 쓰면 폼을 표현식으로 파싱하도록 강제해요.
예시를 볼게요:
> (#%expression (+ 1 2))
3
> (#%expression (define x 10))
eval:8:0: define: not allowed in an expression context
in: (define x 10)
#%expression 폼은 재귀적 정의 컨텍스트에서 유용한데, 거기서 뒤따르는 정의를 확장하는 것이 현재 표현식에 컴파일 타임 정보를 제공할 수 있기 때문이에요. 예를 들어, 주어진 식별자에 심볼 몇 개를 컴파일 타임에 단순히 기록하는 define-sym-case 매크로를 생각해 봐요.
(define-syntax (define-sym-case stx)
(syntax-case stx ()
[(_ id sym ...)
(andmap identifier? (syntax->list #'(sym ...)))
#'(define-syntax id
'(sym ...))]))
그리고 표현식에 쓰인 심볼들이 앞서 정의된 것과 일치하는지 확인하는 case의 변형을 생각해 봐요:
(define-syntax (sym-case stx)
(syntax-case stx ()
[(_ id val-expr [(sym) expr] ...)
(let ()
(define expected-ids
(syntax-local-value
#'id
(λ ()
(raise-syntax-error
'sym-case
"expected an identifier bound via define-sym-case"
stx
#'id))))
(define actual-ids (syntax->datum #'(sym ...)))
(unless (equal? expected-ids actual-ids)
(raise-syntax-error
'sym-case
(format "expected the symbols ~s"
expected-ids)
stx))
#'(case val-expr [(sym) expr] ...))]))
정의가 이렇게 사용 뒤에 따라온다면, define-sym-case 매크로는 id를 바인딩할 기회를 갖지 못하고 sym-case 매크로가 오류를 신호해요:
> (let ()
(sym-case land-creatures 'bear
[(bear) 1]
[(fox) 2])
(define-sym-case land-creatures bear fox))
eval:11:0: sym-case: expected an identifier bound via
define-sym-case
at: land-creatures
in: (sym-case land-creatures (quote bear) ((bear) 1)
((fox) 2))
하지만 sym-case를 #%expression으로 감싸면, 확장기는 그것이 표현식임을 알기 위해 확장할 필요가 없어지고 define-sym-case 표현식으로 넘어가요.
> (let ()
(#%expression (sym-case sea-creatures 'whale
[(whale) 1]
[(squid) 2]))
(define-sym-case sea-creatures whale squid)
'more...)
'more...
물론, sym-case 같은 매크로는 클라이언트가 #%expression을 직접 추가하도록 요구하면 안 돼요. 대신 인자의 기본 형태를 확인한 뒤, syntax-local-value를 호출하고 확장을 마무리하는 헬퍼 매크로를 감싼 #%expression으로 확장해야 해요.