패턴 기반 구문 매칭

패턴 기반 구문 매칭 (Pattern-Based Syntax Matching)

이 섹션은 syntax-casesyntax를 중심으로, 구문 객체를 패턴과 매칭하고 템플릿으로 새 구문을 만드는 핵심 형식들을 다뤄요. 매크로 작성의 가장 기본적인 도구들이에요.

출처: 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 중에 없는 식별자)은 어떤 구문 객체와도 매칭돼요.

  • id id...이나 _에 바인딩되어 있지 않고 어떤 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-patternn번째 cdr에 대응하는 구문 객체(또는 가장 가까운 둘러싸는 구문 객체의 어휘 문맥과 소스 위치를 써서 데이터를 datum->syntax로 강제 변환한 것)와 매칭돼요.

  • (pattern ... pattern ellipsis pattern ...) (pattern ...) 종류의 패턴과 같지만, 다른 하위 패턴들에 대한 상대 위치에서 ellipsis 뒤의 하위 패턴과 매칭되는 요소를 (0개 이상의) 임의의 개수 가진 구문 객체와 매칭돼요.

    ellipsis 뒤의 하위 패턴이 바인딩하는 각 패턴 변수에 대해, 더 큰 패턴은 같은 패턴 변수를, 하위 패턴과 매칭된 구문 객체의 각 요소마다 하나씩 값들의 목록에 증가된 깊이 표시로 바인딩해요. (하위 패턴 자체가 ellipsis를 포함할 수 있고, 이는 깊이 표시 2의 구문 객체들의 목록들의 목록에 바인딩된 패턴 변수 등으로 이어져요.)

    ellipsis가 있는 모든 패턴 형식은 ellipsisliteral-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-datummake-prefab-struct의 유효한 첫 번째 인자에 대응해야 해요.

  • #s(key-datum pattern ... pattern ellipsis pattern ...) (pattern ... pattern ellipsis pattern ...) 패턴과 같지만, 요소들이 대응하는 하위 패턴과 매칭되는 prefab 구조체 구문 객체와 매칭돼요.

  • (ellipsis stat-pattern) stat-pattern과 같은 것과 매칭되는데, stat-pattern은 패턴과 같지만 ... 바인딩을 가진 식별자를 다른 id처럼 취급해요.

  • const const는 앞선 형식 중 어떤 것과도 매칭되지 않는 데이터예요. 구문 객체는 그 데이터가 인용된 constequal?일 때 const 패턴과 매칭돼요.

stx-expr이 구문 객체가 아닌 값을 만들면, 그 결과는 datum->syntaxstx-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-casefree-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-casewith-syntax가 바인딩한 패턴 변수를 포함할 수 있는 템플릿을 기반으로 구문 객체를 만들어요.

템플릿은 단일 구문 객체를 만들어내요. head-template은 0개 이상의 구문 객체 시퀀스를 만들어내요. stat-template은 ..., ~?, ~@가 템플릿 형식 대신 상수로 해석된다는 점만 빼면 템플릿과 같아요.

템플릿은 다음과 같이 구문 객체를 만들어내요.

  • id id가 패턴 변수로 바인딩되어 있으면, 템플릿으로서의 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-datummake-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처럼 취급돼요.

  • const const 템플릿은 앞선 경우 중 어떤 것과도 매칭되지 않는 형식이고, (quote-syntax const) 결과를 만들어내요.

head-template은 구문 객체들의 시퀀스를 만들어내며, 그 시퀀스는 둘러싼 템플릿의 결과에 "인라인"돼요. head-template의 결과는 다음과 같이 정의돼요.

  • template 위의 template 규칙에 따라 구문 객체 하나를 만들어내요.

  • head-template ellipsis ...+ head-template을 그 패턴 변수들의 값에 "매핑"해 구문 객체들의 시퀀스를 생성해요. 반복 횟수는 하위 템플릿 안에서 참조된 패턴 변수들의 값에 따라 달라져요.

    더 정확히 말하자면 outerinner 뒤에 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 안의 unquoteunquote-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/locloc-expr에 대한 계약을 강제하지 않았어요. 8.2.0.6 버전에서 loc-exprdatum->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-exprdatum->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-casesyntax를 보세요.

_

_ 변환기 바인딩은 _을 표현식으로 쓰는 것을 금지해요. 이 바인딩은 구문 패턴에서만 유용하며, 거기서 어떤 구문 객체와도 매칭되는 패턴을 나타내요. 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, syntax
  • syntax-parse: 더 풍부한 패턴 매칭(예: ~or, ~optional)
  • datum->syntax, quote-syntax