패턴 기반 구문 매칭
패턴 기반 구문 매칭 (Pattern-Based Syntax Matching)
이 섹션은 syntax-case와 syntax를 중심으로, 구문 객체를 패턴과 매칭하고 템플릿으로 새 구문을 만드는 핵심 형식들을 다뤄요. 매크로 작성의 가장 기본적인 도구들이에요.
출처: Racket Reference
본문
(syntax-case stx-expr (literal-id ...)
clause ...)
clause = [pattern result-expr]
| [pattern fender-expr result-expr]
pattern = np-pattern
| (pattern ...)
| (pattern ...+ . np-pattern)
| (pattern ... pattern ellipsis pattern ... . np-pattern)
np-pattern = _
| id
| #(pattern ...)
| #(pattern ... pattern ellipsis pattern ...)
| #&pattern
| #s(key-datum pattern ...)
| #s(key-datum pattern ... pattern ellipsis pattern ...)
| (ellipsis stat-pattern)
| const
stat-pattern = id
| (stat-pattern ...)
| (stat-pattern ...+ . stat-pattern)
| #(stat-pattern ...)
| #&stat-pattern
| #s(key-datum stat-pattern ...)
| const
ellipsis = ...
stx-expr가 만든 구문 객체와 매칭되고, 대응하는 fender-expr(있으면)가 참 값을 만드는 첫 번째 절을 찾아요. 결과는 대응하는 result-expr에서 오며, 이는 syntax-case 형식에 대해 꼬리 위치에 있어요. 매칭되는 절이 없으면 exn:fail:syntax 예외가 발생해요. 이 예외는 raise-syntax-error를 "name" 인자로 #f, 일반 오류 메시지가 든 문자열, 그리고 stx-expr의 결과로 호출해 만들어져요.
구문 객체는 다음과 같이 패턴과 매칭돼요.
-
__패턴(즉_과 같은 바인딩을 가지면서literal-id중에 없는 식별자)은 어떤 구문 객체와도 매칭돼요. -
idid는...이나_에 바인딩되어 있지 않고 어떤literal-id와도 같은 바인딩을 가지지 않을 때 어떤 구문 객체와도 매칭돼요.id는 대응하는fender-expr(있으면)과result-expr을 위해 패턴 변수로 추가 바인딩돼요. 패턴 변수 바인딩은 변환기 바인딩이에요. 패턴 변수는syntax같은 형식을 통해서만 참조할 수 있어요. 바인딩의 값은 깊이 표시(depth marker) 0으로 패턴과 매칭된 구문 객체예요.stat-pattern에서는...이 특별히 취급되지 않아요. 그것은literal-id와 매칭되거나 패턴 변수로 바인딩돼요.어떤
literal-id와 같은 바인딩을 가지는id는,free-identifier=?의 의미로 같은 바인딩을 가지는 식별자이면서 구문 객체인 것과 매칭돼요. 매칭은 패턴 변수를 도입하지 않아요. -
(pattern ...)(pattern ...)패턴은, 데이터 형태(즉 어휘 정보 없는)가 패턴의 하위 패턴 수만큼의 요소를 가진 목록이고, 목록의 요소에 대응하는 각 구문 객체가 대응하는 하위 패턴과 매칭되는 구문 객체와 매칭돼요.하위 패턴들이 바인딩하는 모든 패턴 변수는 완전한 패턴이 바인딩하며, 바인딩은 모두 구별되어야 해요.
-
(pattern ...+ . np-pattern)이전 종류의 패턴과 같지만 반드시 목록일 필요는 없는 구문 객체와 매칭돼요. 마지막np-pattern앞의n개 하위 패턴에 대해, 구문 객체의 데이터는n-1번의cdr이 쌍을 만드는 쌍이어야 해요. 마지막np-pattern은n번째cdr에 대응하는 구문 객체(또는 가장 가까운 둘러싸는 구문 객체의 어휘 문맥과 소스 위치를 써서 데이터를datum->syntax로 강제 변환한 것)와 매칭돼요. -
(pattern ... pattern ellipsis pattern ...)(pattern ...)종류의 패턴과 같지만, 다른 하위 패턴들에 대한 상대 위치에서ellipsis뒤의 하위 패턴과 매칭되는 요소를 (0개 이상의) 임의의 개수 가진 구문 객체와 매칭돼요.ellipsis뒤의 하위 패턴이 바인딩하는 각 패턴 변수에 대해, 더 큰 패턴은 같은 패턴 변수를, 하위 패턴과 매칭된 구문 객체의 각 요소마다 하나씩 값들의 목록에 증가된 깊이 표시로 바인딩해요. (하위 패턴 자체가 ellipsis를 포함할 수 있고, 이는 깊이 표시 2의 구문 객체들의 목록들의 목록에 바인딩된 패턴 변수 등으로 이어져요.)ellipsis가 있는 모든 패턴 형식은
ellipsis가literal-id중에 없을 때만 적용돼요. -
(pattern ... pattern ellipsis pattern ... . np-pattern)이전 종류의 패턴과 같지만,(pattern ...+ . np-pattern)처럼 마지막np-pattern이 있어요. 마지막np-pattern은 데이터가 쌍인 구문 객체와 결코 매칭되지 않아요. -
#(pattern ...)(pattern ...)패턴과 같지만, 요소들이 대응하는 하위 패턴과 매칭되는 벡터 구문 객체와 매칭돼요. -
#(pattern ... pattern ellipsis pattern ...)(pattern ... pattern ellipsis pattern ...)패턴과 같지만, 요소들이 대응하는 하위 패턴과 매칭되는 벡터 구문 객체와 매칭돼요. -
#&pattern내용이 패턴과 매칭되는 상자(box) 구문 객체와 매칭돼요. -
#s(key-datum pattern ...)(pattern ...)패턴과 같지만, 필드들이 대응하는 하위 패턴과 매칭되는 prefab 구조체 구문 객체와 매칭돼요.key-datum은make-prefab-struct의 유효한 첫 번째 인자에 대응해야 해요. -
#s(key-datum pattern ... pattern ellipsis pattern ...)(pattern ... pattern ellipsis pattern ...)패턴과 같지만, 요소들이 대응하는 하위 패턴과 매칭되는 prefab 구조체 구문 객체와 매칭돼요. -
(ellipsis stat-pattern)stat-pattern과 같은 것과 매칭되는데,stat-pattern은 패턴과 같지만...바인딩을 가진 식별자를 다른id처럼 취급해요. -
constconst는 앞선 형식 중 어떤 것과도 매칭되지 않는 데이터예요. 구문 객체는 그 데이터가 인용된const와equal?일 때const패턴과 매칭돼요.
stx-expr이 구문 객체가 아닌 값을 만들면, 그 결과는 datum->syntax와 stx-expr의 어휘 문맥 및 소스 위치로 구문 객체로 변환돼요.
stx-expr이 오염된 구문 객체를 만들면, 패턴이 바인딩하는 모든 구문 객체는 오염돼요.
예제:
> (require (for-syntax racket/base))
> (define-syntax (swap stx)
(syntax-case stx ()
[(_ a b) #'(let ([t a])
(set! a b)
(set! b t))]))
> (let ([x 5] [y 10])
(swap x y)
(list x y))
'(10 5)
> (syntax-case #'(ops 1 2 3 => +) (=>)
[(_ x ... => op) #'(op x ...)])
#<syntax:eval:687:0 (+ 1 2 3)>
> (syntax-case #'(let ([x 5] [y 9] [z 12])
(+ x y z))
(let)
[(let ([var expr] ...) body ...)
(list #'(var ...)
#'(expr ...))])
'(#<syntax:eval:688:0 (x y z)> #<syntax:eval:688:0 (5 9 12)>)
(syntax-case* stx-expr (literal-id ...) id-compare-expr
clause ...)
syntax-case와 같지만, id-compare-expr은 두 인자를 받는 프로시저를 만들어내야 해요. 패턴의 literal-id는, (첫 번째 인자로) 매칭할 식별자와 (두 번째 인자로) 패턴의 식별자를 주었을 때 그 프로시저가 참을 반환하는 식별자와 매칭돼요.
즉 syntax-case는 free-identifier=?를 만들어내는 id-compare-expr을 가진 syntax-case*와 같아요.
(with-syntax ([pattern stx-expr] ...)
body ...+)
stx-case와 비슷하게 패턴을 구문 객체에 매칭해요. syntax-case와 달리 모든 패턴이 각각 대응하는 stx-expr의 결과에 매칭되고, 모든 매칭(구별되어야 함)의 패턴 변수들은 단일 본문 시퀀스로 바인딩돼요. with-syntax 형식의 결과는 마지막 본문의 결과이고, 이는 with-syntax 형식에 대해 꼬리 위치에 있어요.
어떤 패턴이든 대응하는 stx-expr과 매칭하지 못하면 exn:fail:syntax 예외가 발생해요.
with-syntax 형식은 대략 다음 syntax-case 형식과 동등해요.
(syntax-case (list stx-expr ...) ()
[(pattern ...) (let () body ...+)])
다만 어떤 개별 stx-expr이 구문 객체가 아닌 값을 만들면, datum->syntax와 그 개별 stx-expr의 어휘 문맥 및 소스 위치로 구문 객체로 변환돼요.
예제:
> (define-syntax (hello stx)
(syntax-case stx ()
[(_ name place)
(with-syntax ([print-name #'(printf "~a\n" 'name)]
[print-place #'(printf "~a\n" 'place)])
#'(begin
(define (name times)
(printf "Hello\n")
(for ([i (in-range 0 times)])
print-name))
(define (place times)
(printf "From\n")
(for ([i (in-range 0 times)])
print-place))))]))
> (hello jon utah)
> (jon 2)
Hello
jon
jon
> (utah 2)
From
utah
utah
> (define-syntax (math stx)
(define (make+1 expression)
(with-syntax ([e expression])
#'(+ e 1)))
(syntax-case stx ()
[(_ numbers ...)
(with-syntax ([(added ...)
(map make+1
(syntax->list #'(numbers ...)))])
#'(begin
(printf "got ~a\n" added)
...))]))
> (math 3 1 4 1 5 9)
got 4
got 2
got 5
got 2
got 6
got 10
(syntax template)
template = id
| (head-template ...)
| (head-template ...+ . template)
| #(head-template ...)
| #&template
| #s(key-datum head-template ...)
| (~? template template)
| (ellipsis stat-template)
| const
head-template = template
| head-template ellipsis ...+
| (~@ . template)
| (~? head-template head-template)
| (~? head-template)
stat-template = like template, but without ..., ~?, and ~@
ellipsis = ...
stx-case나 with-syntax가 바인딩한 패턴 변수를 포함할 수 있는 템플릿을 기반으로 구문 객체를 만들어요.
템플릿은 단일 구문 객체를 만들어내요. head-template은 0개 이상의 구문 객체 시퀀스를 만들어내요. stat-template은 ..., ~?, ~@가 템플릿 형식 대신 상수로 해석된다는 점만 빼면 템플릿과 같아요.
템플릿은 다음과 같이 구문 객체를 만들어내요.
-
idid가 패턴 변수로 바인딩되어 있으면, 템플릿으로서의id는 패턴 변수의 매칭 결과를 만들어내요.id가 더 큰 템플릿에서 ellipsis로 복제되는 하위 템플릿이 아니라면, 패턴 변수의 값은 깊이 표시 0의 구문 객체여야 해요(매칭 목록이 아니라).더 일반적으로 패턴 변수의 값이 깊이 표시
n을 가지면, 그 값은 적어도n개의 ellipsis로 복제되는 템플릿 안에만 나타날 수 있어요. 그 경우 템플릿은 각 매칭 결과를 적어도 한 번 사용하도록 충분히 복제돼요.id가 패턴 변수로 바인딩되어 있지 않으면, 템플릿으로서의id는(quote-syntax id)를 만들어내요. -
(head-template ...)데이터가 목록이고 목록의 요소들이 head-template들이 만든 구문 객체들에 대응하는 구문 객체를 만들어내요. -
(head-template ... . template)이전 형식과 같지만 결과가 반드시 목록은 아니고, 결과 구문 객체의 데이터에서 빈 목록의 자리는template이 만든 구문 객체가 차지해요. -
#(head-template ...)(head-template ...)형식과 같지만, 데이터가 목록 대신 벡터인 구문 객체를 만들어내요. -
#&template데이터가template이 만든 구문 객체를 담은 상자인 구문 객체를 만들어내요. -
#s(key-datum head-template ...)(head-template ...)형식과 같지만, 데이터가 목록 대신 prefab 구조체인 구문 객체를 만들어내요.key-datum은make-prefab-struct의 유효한 첫 번째 인자에 대응해야 해요. -
(~? template1 template2)template1에 "누락된 값(missing values)"을 가진 패턴 변수가 없으면template1의 결과를, 그렇지 않으면template2의 결과를 만들어내요.syntax-case가 바인딩한 패턴 변수는 결코 누락된 값을 가지지 않지만,syntax-parse가 바인딩한 패턴 변수(예:~or또는~optional패턴)는 가질 수 있어요.예제:
> (syntax-parse #'(m 1 2 3) [(_ (~optional (~seq #:op op:expr)) arg:expr ...) #'((~? op +) arg ...)]) #<syntax:eval:3:0 (+ 1 2 3)> > (syntax-parse #'(m #:op max 1 2 3) [(_ (~optional (~seq #:op op:expr)) arg:expr ...) #'((~? op +) arg ...)]) #<syntax:eval:4:0 (max 1 2 3)> -
(ellipsis stat-template)stat-template과 같은 결과를 만들어내는데,stat-template은 템플릿과 같지만...,~?,~@가 (패턴 바인딩 없는)id처럼 취급돼요. -
constconst템플릿은 앞선 경우 중 어떤 것과도 매칭되지 않는 형식이고,(quote-syntax const)결과를 만들어내요.
head-template은 구문 객체들의 시퀀스를 만들어내며, 그 시퀀스는 둘러싼 템플릿의 결과에 "인라인"돼요. head-template의 결과는 다음과 같이 정의돼요.
-
template위의 template 규칙에 따라 구문 객체 하나를 만들어내요. -
head-template ellipsis ...+head-template을 그 패턴 변수들의 값에 "매핑"해 구문 객체들의 시퀀스를 생성해요. 반복 횟수는 하위 템플릿 안에서 참조된 패턴 변수들의 값에 따라 달라져요.더 정확히 말하자면
outer를inner뒤에 ellipsis 하나가 온 것이라고 하자. 패턴 변수가 그 깊이 표시와 같은 깊이에서 나타나면 그것은outer의 반복(iteration) 패턴 변수예요. 적어도 하나는 있어야 해요. 그렇지 않으면 오류가 발생해요. 반복 변수가 여러 개라면 그 값들은 모두 같은 길이의 목록이어야 해요.outer의 결과는inner템플릿을 반복 패턴 변수 값들에 매핑하고 그 안에서 유효 깊이 표시를 1 감소시켜 만들어져요.outer결과는inner결과들을 이어붙여 형성돼요.결과적으로 패턴 변수가 깊이 표시보다 큰 깊이에서 나타나면, 그것은 가장 안쪽 ellipsis에 대해서는 반복 패턴 변수로 쓰이지만 가장 바깥쪽에는 그렇지 않아요. 패턴 변수는 깊이 표시보다 작은 깊이에서 나타나면 안 되고, 그렇지 않으면 오류가 발생해요.
(~@ . template)
template이 만든 구문 목록의 요소들의 시퀀스를 만들어내요. template이 올바른 구문 목록을 만들지 않으면 예외가 발생해요.
예제:
> (with-syntax ([(key ...) #'('a 'b 'c)]
[(val ...) #'(1 2 3)])
#'(hash (~@ key val) ...))
#<syntax:eval:2:0 (hash (quote a) 1 (quote b) 2 (quote c) 3)>
> (with-syntax ([xs #'(2 3 4)])
#'(list 1 (~@ . xs) 5))
#<syntax:eval:3:0 (list 1 2 3 4 5)>
(~? head-template1 head-template2)
head-template1의 패턴 변수 중 어떤 것도 "누락된 값"을 가지지 않으면 head-template1의 결과를, 그렇지 않으면 head-template2의 결과를 만들어내요.
(~? head-template)
head-template의 패턴 변수 중 어떤 것도 "누락된 값"을 가지지 않으면 head-template의 결과를, 그렇지 않으면 아무것도 만들지 않아요.
(~? head-template (~@))와 동등해요.
(syntax template) 형식은 보통 #'template으로 줄여 써요. 읽기 인용(Reading Quotes)도 함께 보세요. 템플릿에 패턴 변수가 없다면 #'template은 (quote-syntax template)과 동등해요.
base 패키지 6.90.0.25 버전에서
~@와~?가 추가됐어요.
(quasisyntax template)
syntax와 같지만, (unsyntax expr)과 (unsyntax-splicing expr)이 템플릿 안의 표현식으로 탈출해요.
expr은 탈출 위치를 대신해 치환될 구문 객체(또는 구문 목록)를 만들어야 해요. 이는 quasiquote 안의 unquote와 unquote-splicing과 마찬가지지만, 해시 테이블 값 위치는 quasisyntax의 탈출 위치가 아니라는 점이 달라요. (탈출된 표현식이 구문 객체를 만들지 않으면, with-syntax의 오른쪽 변과 같은 방식으로 구문 객체로 변환돼요.) 중첩된 quasisyntax는 중첩된 quasiquote처럼 쿼시쿼팅 층을 도입해요.
또한 quasiquote와 유사하게, 리더는 #``을 quasisyntax로, #,을 unsyntax로, #,@를 unsyntax-splicing`으로 변환해요. 읽기 인용(Reading Quotes)도 함께 보세요.
(unsyntax expr)
표현식 형식으로는 불법이에요. unsyntax 형식은 quasisyntax 템플릿에서만 쓰기 위한 것이에요.
(unsyntax-splicing expr)
표현식 형식으로는 불법이에요. unsyntax-splicing 형식은 quasisyntax 템플릿에서만 쓰기 위한 것이에요.
(syntax/loc loc-expr template)
loc-expr : (or/c #f srcloc? syntax?
(list/c any/c
(or/c exact-positive-integer? #f)
(or/c exact-nonnegative-integer? #f)
(or/c exact-positive-integer? #f)
(or/c exact-nonnegative-integer? #f))
(vector/c any/c
(or/c exact-positive-integer? #f)
(or/c exact-nonnegative-integer? #f)
(or/c exact-positive-integer? #f)
(or/c exact-nonnegative-integer? #f)))
syntax와 같지만, 즉시 결과 구문 객체가 loc-expr의 결과에서 소스 위치 정보를 가져와요.
즉시 결과—"가장 바깥" 구문 객체—의 소스 위치만 조정돼요. loc-expr의 소스와 위치가 둘 다 #f면 소스 위치는 조정되지 않아요. 결과 구문 객체가 구문 패턴 변수의 값이 아니라 템플릿 자체에서 온 경우에만 소스 위치가 조정돼요. 예를 들어 x가 구문 패턴 변수라면 (syntax/loc loc-expr x)는 loc-expr의 위치를 사용하지 않아요.
변경 사항: base 패키지 6.90.0.25 버전에서 이전에는
template이 그냥 패턴 변수일 때syntax/loc이loc-expr에 대한 계약을 강제하지 않았어요. 8.2.0.6 버전에서loc-expr이datum->syntax가 받아들이는 어떤 소스 위치 값이든 허용하도록 바뀌었어요.
(quasisyntax/loc loc-expr template)
loc-expr : (or/c #f srcloc? syntax?
(list/c any/c
(or/c exact-positive-integer? #f)
(or/c exact-nonnegative-integer? #f)
(or/c exact-positive-integer? #f)
(or/c exact-nonnegative-integer? #f))
(vector/c any/c
(or/c exact-positive-integer? #f)
(or/c exact-nonnegative-integer? #f)
(or/c exact-positive-integer? #f)
(or/c exact-nonnegative-integer? #f)))
quasisyntax와 같지만, syntax/loc처럼 소스 위치 배정을 해요.
base 패키지 8.2.0.6 버전에서
loc-expr이datum->syntax가 받아들이는 어떤 소스 위치 값이든 허용하도록 바뀌었어요.
(quote-syntax/prune id)
quote-syntax와 같지만, id의 어휘 문맥이 identifier-prune-lexical-context로 가지치기되어 id의 기호 이름과 '#%top에 대한 바인딩만 포함돼요. 어휘 정보가 다른 구문 객체로 전송되지 않을 식별자(최상위 바인딩일 때 '#%top으로의 전송은 제외)를 인용할 때 이 형식을 쓰세요.
(syntax-rules (literal-id ...)
[(id . pattern) template] ...)
다음과 동등해요.
(lambda (stx)
(syntax-case stx (literal-id ...)
[(generated-id . pattern) (syntax-protect #'template)] ...))
여기서 각 generated-id는 대응하는 템플릿에서 어떤 식별자도 바인딩하지 않아요. 이는 특히 id 위치들이 무시됨을 의미해요. 관례상 id 위치는 _ 식별자여야 해요.
예제:
> (define-syntax my-let*
(syntax-rules ()
[(_ () body ...) (let () body ...)]
[(_ ([x v] binding ...) body ...)
(let ([x v])
(my-let* (binding ...) body ...))]))
> (my-let* ([x 42]
[x (+ x 1)])
x)
43
(syntax-id-rules (literal-id ...)
[pattern template] ...)
다음과 동등해요.
(make-set!-transformer
(lambda (stx)
(syntax-case stx (literal-id ...)
[pattern (syntax-protect #'template)] ...)))
(define-syntax-rule (id . pattern) template)
다음과 동등해요.
(define-syntax id
(syntax-rules ()
[(id . pattern) template]))
다만 구문 오류가 잠재적으로 pattern의 관점에서 표현돼요.
...
... 변환기 바인딩은 ...을 표현식으로 쓰는 것을 금지해요. 이 바인딩은 구문 패턴과 템플릿(또는 ->처럼 그것을 특별히 취급하는 다른 관련 없는 표현식 형식)에서만 유용하며, 거기서 패턴이나 템플릿의 반복을 나타내요. syntax-case와 syntax를 보세요.
_
_ 변환기 바인딩은 _을 표현식으로 쓰는 것을 금지해요. 이 바인딩은 구문 패턴에서만 유용하며, 거기서 어떤 구문 객체와도 매칭되는 패턴을 나타내요. syntax-case를 보세요.
~?
~@
~?와 ~@ 변환기 바인딩은 이 형식들을 표현식으로 쓰는 것을 금지해요. 이 바인딩들은 구문 템플릿에서만 유용해요. syntax를 보세요.
base 패키지 6.90.0.25 버전에서 추가.
(syntax-pattern-variable? v) → boolean?
v : any/c
v가, 변환기 바인딩 값으로서 바인딩된 변수를 syntax 및 다른 형식에서 패턴 변수로 만드는 값이면 #t를 반환해요. 식별자가 패턴 변수인지 확인하려면 syntax-local-value로 식별자의 변환기 값을 얻은 뒤 그 값을 syntax-pattern-variable?으로 검사하세요.
syntax-pattern-variable? 프로시저는 racket/base가 for-syntax로 제공해요.
더 알아보기
syntax-case,with-syntax,syntaxsyntax-parse: 더 풍부한 패턴 매칭(예:~or,~optional)datum->syntax,quote-syntax