문법 튜토리얼

문법 튜토리얼 (Grammar Tutorial)

문자열을 정리하고 해석해야 할 일은 프로그래밍에서 정말 흔해요. 파일을 섹션으로 나누거나, SMTP 같은 프로토콜에서 사용자 데이터 뒤에 어떤 "명령"이 와야 하는지 지정하거나, 자신만의 도메인 특화 언어를 설계하는 일이죠. Raku의 문법(grammar)은 바로 그런 문자열 파싱 작업에 쓸 수 있는 강력한 도구예요.

출처: Raku 공식 문서 — Grammar Tutorial

시작하기 전에

왜 문법인가?

문법은 문자열을 파싱해서 그 문자열로부터 데이터 구조를 반환해요. 문법은 프로그램을 실행하기 위해 준비하거나, 프로그램이 아예 실행될 수 있는지(유효한 프로그램인지) 판단하거나, 웹 페이지를 구성 요소로 분해하거나, 문장의 다른 부분을 식별하는 데 쓸 수 있어요.

언제 문법을 쓰나요?

다루거나 해석할 문자열이 있다면, 문법이 그 일을 할 도구를 제공해요.

그 문자열은 섹션으로 나누고 싶은 파일이 될 수도 있고, SMTP 같은 프로토콜(사용자가 제공한 데이터 뒤에 어떤 "명령"이 오는지 지정해야 하는)일 수도 있고, 자신만의 도메인 특화 언어를 설계하는 중일 수도 있어요. 문법이 도움이 돼요.

문법의 넓은 개념

정규식(Regex)은 문자열에서 패턴을 찾는 데 잘 동작해요. 하지만 한 번에 여러 패턴을 찾거나, 패턴을 결합하거나, 문자열을 감쌀 수 있는 패턴을 검사하는 같은 작업에는 정규식만으로는 충분하지 않아요.

HTML을 다룰 때, HTML 태그(열림·닫힘 요소와 그 사이의 텍스트)를 인식하는 문법을 정의할 수 있어요. 그런 다음 이 요소들을 배열이나 해시 같은 데이터 구조로 정리할 수 있어요.

더 기술적으로

개념적 개요

문법은 특별한 종류의 클래스예요. class 대신 grammar 키워드를 쓰는 것 외에는 다른 클래스와 똑같이 문법을 선언하고 정의해요.

grammar G { ... }

그런 클래스로서 문법은 정규식, 토큰, 또는 룰을 정의하는 메서드로 구성돼요. 이들은 모두 다양한 유형의 매치 메서드예요. 문법을 정의하고 나면 호출해서 파싱용 문자열을 전달해요.

my $matchObject = G.parse($string);

이제, 정의한 정규식들이 그냥 결과를 반환한다면, 다른 문자열에서 앞이나 뒤에 있는 문자열을 파싱하거나 많은 정규식을 결합해야 하는 일에 어떻게 도움이 될지 궁금할 거예요... 그리고 바로 여기서 문법 액션(grammar actions)이 등장해요.

문법에서 매치하는 모든 "메서드"에 대해 그 매치에 작용(act)할 수 있는 액션을 얻어요. 또한 모든 매치를 묶고 데이터 구조를 만드는 데 쓸 수 있는 전반적인(overarching) 액션도 얻어요. 이 전반적인 메서드는 기본적으로 TOP라고 불러요.

기술적 개요

이미 말했듯, 문법은 grammar 키워드로 선언되고, 그 "메서드"는 regex, token, rule로 선언돼요.

  • Regex 메서드는 느리지만 철저해요. 문자열을 되돌아보며 정말 시도해요.
  • Token 메서드는 regex 메서드보다 빠르고 공백을 무시해요. Token 메서드는 역추적하지 않아요. 첫 번째 가능한 매치 후에 포기해요.
  • Rule 메서드는 공백이 무시되지 않는다는 점을 제외하면 token 메서드와 같아요.

문법에서 메서드(regex, token, rule)가 매치하면, 매치된 문자열은 매치 객체(match object)에 들어가고 메서드와 같은 이름으로 키가 붙어요.

grammar G {
    token TOP { <thingy> .* }
    token thingy { 'clever_text_keyword' }
}

my $match = G.parse($string)를 쓰고 문자열이 'clever_text_keyword'로 시작한다면, 매치 객체에서 <thingy>라는 이름으로 키가 붙은 'clever_text_keyword'를 포함하는 매치 객체를 돌려받아요. 예를 들어:

grammar G {
    token TOP { <thingy> .* }
    token thingy { 'Þor' }
}

my $match = G.parse("Þor is mighty");
say $match.raku;     # OUTPUT: «Match.new(made => Any, pos => 13, orig => "Þor is mighty",...»
say $/.raku;         # OUTPUT: «Match.new(made => Any, pos => 13, orig => "Þor is mighty",...»
say $/<thingy>.raku;
# OUTPUT: «Match.new(made => Any, pos => 3, orig => "Þor is mighty", hash => Map.new(()), list => (), from => 0)␤»

처음 두 줄의 출력은 $match가 파싱 결과를 담은 Match 객체를 포함한다는 걸 보여줘요. 하지만 그 결과는 또한 매치 변수 $/에도 할당돼요. 어느 매치 객체든 위에서처럼 thingy로 키를 붙여 그 특정 token에 대한 매치를 반환할 수 있어요.

TOP 메서드(regex, token, rule 중 무엇이든)는 (기본적으로) 모든 것을 매치해야 하는 전반적인 패턴이에요. 파싱된 문자열이 TOP regex와 매치하지 않으면 반환되는 매치 객체는 비어 있을 거예요 (Nil).

위에서 볼 수 있듯 TOP 안에 <thingy> 토큰이 언급돼요. <thingy>는 다음 줄에 정의돼요. 즉 'clever_text_keyword'가 문자열의 것이어야 하고, 그렇지 않으면 문법 파싱이 실패해서 빈 매치를 얻게 돼요. 잘못된 문자열을 인식해 버려야 할 때 아주 좋아요.

예제로 배우기 — REST 구성

URI를 RESTful 요청을 구성하는 요소로 파싱하고 싶다고 가정해 보죠. URI가 이렇게 동작하길 원해요.

  • URI의 첫 부분은 "주제(subject)"예요. 부품, 제품, 사람 같은 것이죠.
  • URI의 두 번째 부분은 "명령(command)"이에요. 표준 CRUD 함수(생성, 조회, 갱신, 삭제)예요.
  • URI의 세 번째 부분은 임의 데이터예요. 아마 우리가 다룰 특정 ID나 "/"로 구분된 긴 데이터 목록일 거예요.
  • URI를 받으면 1~3을 쉽게 다룰 수 있는(그리고 나중에 향상시킬 수 있는) 데이터 구조에 넣고 싶어요.

따라서 "/product/update/7/notify"가 있으면 문법이 subject가 "product", command가 "update", data가 "7/notify"인 매치 객체를 주길 원해요.

문법 클래스와 subject, command, data용 매치 메서드 몇 개를 정의하는 것부터 시작할게요. 공백은 신경 쓰지 않으므로 token 선언자를 쓸게요.

grammar REST {
    token subject { \w+ }
    token command { \w+ }
    token data    { .* }
}

지금까지 이 REST 문법은 subject는 단어 문자만, command는 단어 문자만, data는 문자열에 남은 모든 것으로 하겠다고 말하는 거예요.

다음으로, 이 매치 토큰들을 URI의 더 큰 맥락 안에 배치하고 싶어요. 그게 TOP 메서드가 하게 해 주는 일이에요. TOP 메서드를 추가하고 그 안에 토큰 이름들을 나머지 전체 패턴을 구성하는 부분들과 함께 넣을게요. 이름 있는 정규식들로 더 큰 정규식을 만드는 모습을 주목하세요.

grammar REST {
    token TOP     { '/' <subject> '/' <command> '/' <data> }
    token subject { \w+ }
    token command { \w+ }
    token data    { .* }
}

이 코드로 이미 RESTful 요청의 세 부분을 얻을 수 있어요.

my $match = REST.parse('/product/update/7/notify');
say $match;

# OUTPUT: «「/product/update/7/notify」␤
#          subject => 「product」
#          command => 「update」
#          data => 「7/notify」»

$match<subject> 또는 $match<command> 또는 $match<data>로 직접 접근하면 파싱된 값을 반환해요. 그것들은 각각 더 다룰 수 있는 매치 객체를 담고 있고, 문자열로 강제할 수도 있어요 ( $match<command>.Str ).

약간의 유연성 추가

지금까지 문법은 조회(retrieve), 삭제(delete), 갱신(update)을 처리해요. 하지만 생성(create) 명령에는 세 번째 부분(data 부분)이 없어요. 그래서 create URI를 파싱하려고 하면 문법이 매치에 실패해요. 이를 피하려면 마지막 data 위치와 그 앞의 '/'를 선택적으로 매치해야 해요. 일반 정규식처럼 TOP 토큰의 그룹화된 '/'와 data 구성 요소에 물음표를 붙이면 돼요.

이제 이렇게 됐어요.

grammar REST {
    token TOP     { '/' <subject> '/' <command> [ '/' <data> ]? }
    token subject { \w+ }
    token command { \w+ }
    token data    { .* }
}

my $m = REST.parse('/product/create');
say $m<subject>, $m<command>;

# OUTPUT: «「product」「create」␤»

다음으로, URI가 사용자에 의해 수동 입력되고 사용자가 '/' 사이에 실수로 공백을 넣을 수 있다고 가정해 보죠. 이를 수용하려면 TOP의 '/'들을 공백을 허용하는 토큰으로 교체하면 돼요.

grammar REST {
    token TOP     { <slash><subject><slash><command>[<slash><data>]? }
    token subject { \w+ }
    token command { \w+ }
    token data    { .* }

    token slash   { \s* '/' \s* }
}

my $m = REST.parse('/ product / update /7 /notify');
say $m;

# OUTPUT: «「/ product / update /7 /notify」␤
#          slash => 「/ 」
#          subject => 「product」
#          slash => 「 / 」
#          command => 「update」
#          slash => 「 /」
#          data => 「7 /notify」»

이제 매치 객체에 슬래시들과 함께 약간의 잡동사니가 생겼어요. 그것을 정리하는 기법이 있는데, 나중에 다룰게요.

문법에서 상속

문법은 클래스이므로 OOP 관점에서 다른 클래스와 똑같이 동작해요. 특히 일부 토큰이나 룰을 포함하는 기본 클래스에서 상속받을 수 있어요. 이렇게요.

grammar Letters {
    token letters { \w+ }
}

grammar Quote-Quotes {
    token quote { "\"" | "`" | "'" }
}

grammar Quote-Other {
    token quote { "|" | "/" | "¡" }
}

grammar Quoted-Quotes is Letters is Quote-Quotes {
    token TOP { ^  <quoted> $}
    token quoted { <quote>? <letters> <quote>?  }
}

grammar Quoted-Other is Letters is Quote-Other {
    token TOP { ^  <quoted> $}
    token quoted { <quote>? <letters> <quote>?  }
}

my $quoted = q{"enhanced"};
my $parsed = Quoted-Quotes.parse($quoted);
say $parsed;
# OUTPUT:
#「"enhanced"」
# quote => 「"」
# letters => 「enhanced」
#quote => 「"」

$quoted = "|barred|";
$parsed = Quoted-Other.parse($quoted);
say $parsed;
# OUTPUT:
#|barred|」
#quote => 「|」
#letters => 「barred」
#quote => 「|」

이 예제는 quotes에 해당하는 룰을 다르게 해 두 개의 다른 문법을 결합하는 데 다중 상속을 사용해요. 이 경우에는 사실 상속보다는 합성(composition)을 쓰고 있으므로, 상속 대신 Roles를 쓸 수도 있어요.

role Letters {
    token letters { \w+ }
}

role Quote-Quotes {
    token quote { "\"" | "`" | "'" }
}

role Quote-Other {
    token quote { "|" | "/" | "¡" }
}

grammar Quoted-Quotes does Letters does Quote-Quotes {
    token TOP { ^  <quoted> $}
    token quoted { <quote>? <letters> <quote>?  }
}

grammar Quoted-Other does Letters does Quote-Other {
    token TOP { ^  <quoted> $}
    token quoted { <quote>? <letters> <quote>?  }

}

위 코드와 정확히 같은 출력을 낼 거예요. Classes와 Roles의 차이를 보여주는 증상으로, Role 합성으로 token quote를 두 번 정의하는 것 같은 충돌은 오류를 낼 거예요.

grammar Quoted-Quotes does Letters does Quote-Quotes does Quote-Other { ... }
# OUTPUT: ... Error while compiling ... Method 'quote' must be resolved ...

제약 추가

우리의 RESTful 문법이 CRUD 연산만 허용하길 원해요. 그 외의 것은 파싱 실패해야 해요. 즉 위의 "command"는 네 값(create, retrieve, update, delete) 중 하나여야 해요.

이를 달성하는 방법은 여러 가지가 있어요. 예를 들어 command 메서드를 바꿀 수 있어요.

token command { \w+ }

# …becomes…

token command { 'create'|'retrieve'|'update'|'delete' }

URI가 성공적으로 파싱되려면 '/' 사이의 두 번째 부분이 그 CRUD 값 중 하나여야 하고, 아니면 파싱이 실패해요. 정확히 우리가 원하는 거예요.

옵션이 많아질 때 더 큰 유연성과 향상된 가독성을 제공하는 또 다른 기법은 proto-regex예요.

이 proto-regex(사실 multimethod)를 사용해 유효한 CRUD 옵션으로 제한하려면 token command를 다음과 같이 대체해요.

proto token command {*}
token command:sym<create>   { <sym> }
token command:sym<retrieve> { <sym> }
token command:sym<update>   { <sym> }
token command:sym<delete>   { <sym> }

sym 키워드는 다양한 proto-regex 옵션을 만드는 데 쓰여요. 각 옵션은 이름이 붙고(예: sym<update>), 그 옵션의 사용을 위해 특별한 <sym> 토큰이 같은 이름으로 자동 생성돼요.

<sym> 토큰과 다른 사용자 정의 토큰은 proto-regex 옵션 블록 안에서 특정 매치 조건을 정의하는 데 쓸 수 있어요. Regex 토큰은 컴파일된 형태이며, 정의된 후에는 adverb 액션(예: :i)으로 수정할 수 없어요. 그래서 자동 생성된 특별한 <sym> 토큰은 옵션 이름의 정확한 매치가 필요한 곳에서만 유용해요.

proto-regex 옵션 중 하나에서 매치 조건이 발생하면 전체 proto의 검색이 종료돼요. 매치 데이터는 매치 객체 형태로 부모 proto 토큰에 할당돼요. 특별한 <sym> 토큰이 실제 매치의 전부 또는 일부를 형성했다면 매치 객체의 하위 레벨로 보존되고, 그렇지 않으면 없어요.

이렇게 proto-regex를 쓰면 많은 유연성이 생겨요. 예를 들어 이 경우 전체 매치된 문자열인 <sym>을 반환하는 대신, 자신의 문자열을 넣거나 다른 재미있는 일을 할 수 있어요. token subject 메서드에도 같은 것을 적용해 올바른 subject('part'나 'people' 같은)에서만 파싱되도록 제한할 수도 있어요.

RESTful 문법 조립하기

RESTful URI를 처리하기 위해 지금까지 가진 것은 이래요.

grammar REST
{
    token TOP { <slash><subject><slash><command>[<slash><data>]? }

    proto token command {*}
    token command:sym<create>   { <sym> }
    token command:sym<retrieve> { <sym> }
    token command:sym<update>   { <sym> }
    token command:sym<delete>   { <sym> }

    token subject { \w+ }
    token data    { .* }
    token slash   { \s* '/' \s* }
}

다양한 URI를 보고 문법과 어떻게 동작하는지 봐요.

my @uris = ['/product/update/7/notify',
            '/product/create',
            '/item/delete/4'];

for @uris -> $uri {
    my $m = REST.parse($uri);
    say "Sub: $m<subject> Cmd: $m<command> Dat: $m<data>";
}

# OUTPUT: «Sub: product Cmd: update Dat: 7/notify␤
#          Sub: product Cmd: create Dat: ␤
#          Sub: item Cmd: delete Dat: 4␤»

두 번째 문자열에서 <data>가 아무것도 매치하지 않으므로 $m<data>Nil이 되고, say 함수에서 그걸 문자열 컨텍스트로 쓰면 경고가 난다는 점에 주목하세요.

문법의 이 부분만으로 우리가 찾는 것의 거의 전부를 얻고 있어요. URI가 파싱되고 데이터가 든 데이터 구조를 얻어요.

data 토큰은 URI의 끝 전체를 하나의 문자열로 반환해요. 4는 괜찮아요. 하지만 '7/notify'에서는 7만 원해요. 7만 얻으려면 문법 클래스의 또 다른 기능인 액션(actions) 을 쓸 거예요.

문법 액션 (Grammar actions)

문법 액션은 문법 클래스 안에서 매치로 무언가를 하기 위해 사용돼요. 액션은 문법 클래스와 구별되는 자기 자신의 클래스에 정의돼요.

문법 액션은 문법을 위한 일종의 플러그인 확장 모듈로 생각할 수 있어요. 대부분의 경우 문법만으로 충분히 만족할 거예요. 하지만 그 문자열들을 더 처리해야 할 때, Actions 확장 모듈을 끼워 넣을 수 있어요.

액션과 함께 작업하려면 actions라는 이름 파라미터를 써야 하는데, 여기에는 액션 클래스의 인스턴스가 들어가야 해요. 위 코드에서 액션 클래스가 REST-actions라고 한다면, URI 문자열을 이렇게 파싱하면 돼요.

my $matchObject = REST.parse($uri, actions => REST-actions.new);

#   …or if you prefer…

my $matchObject = REST.parse($uri, :actions(REST-actions.new));

액션 메서드를 문법 메서드와 같은 이름으로 지정하면, 문법 메서드가 매치할 때 같은 이름의 액션 메서드가 자동으로 호출돼요. 그 메서드에는 대응하는 매치 객체($/ 변수로 표현)도 전달돼요.

예제로 넘어가 볼게요.

액션과 함께하는 문법 예제

여기 다시 우리 문법으로 왔어요.

grammar REST
{
    token TOP { <slash><subject><slash><command>[<slash><data>]? }

    proto token command {*}
    token command:sym<create>   { <sym> }
    token command:sym<retrieve> { <sym> }
    token command:sym<update>   { <sym> }
    token command:sym<delete>   { <sym> }

    token subject { \w+ }
    token data    { .* }
    token slash   { \s* '/' \s* }
}

data 토큰 "7/notify"를 더 처리해 7을 얻고 싶다는 걸 기억하세요. 그러려면 이름 있는 토큰과 같은 이름의 메서드를 가진 액션 클래스를 만들 거예요. 이 경우 우리 토큰은 data이므로 메서드도 data예요.

class REST-actions
{
    method data($/) { $/.split('/') }
}

이제 URI 문자열을 문법에 통과시키면 data 토큰 매치REST-actions의 data 메서드에 전달돼요. 액션 메서드는 문자열을 '/' 문자로 분리하고, 반환된 목록의 첫 요소가 ID 번호("7/notify"의 경우 7)가 돼요.

하지만 사실은 좀 더 있어요.

makemade로 문법 액션 정리하기

문법이 위 액션을 data에 호출하면 data 메서드는 호출되지만, 우리 프로그램에 반환되는 큰 TOP 문법 매치 결과에는 아무것도 나타나지 않아요. 액션 결과를 나타나게 하려면 그 결과에 make를 호출해야 해요. 결과는 문자열, 배열, 해시 구조 등 다양한 것이 될 수 있어요.

make가 문법을 위해 결과를 특별한 포함 영역에 넣는다고 상상할 수 있어요. 우리가 make한 모든 것은 나중에 made로 접근할 수 있어요.

그래서 위 REST-actions 클래스 대신 이렇게 써야 해요.

class REST-actions
{
    method data($/) { make $/.split('/') }
}

매치 분리(목록을 반환하는)에 make를 추가하면, 액션은 원래 문법의 data 토큰과는 별도로 저장될 데이터 구조를 문법에 반환해요. 이렇게 하면 필요할 때 둘 다 사용할 수 있어요.

그 긴 URI에서 ID 7만 접근하고 싶다면, data 액션에서 make한 것 중 반환된 목록의 첫 요소에 접근해요.

my $uri = '/product/update/7/notify';

my $match = REST.parse($uri, actions => REST-actions.new);

say $match<data>.made[0];  # OUTPUT: «7␤»
say $match<command>.Str;   # OUTPUT: «update␤»

여기서 data에 made를 호출해요. make(make로)한 액션의 결과, 즉 분리된 배열을 얻고 싶으니까요. 멋지죠! 하지만 타입을 강제하고 배열을 기억해야 하는 대신, 우리가 원하는 모든 것을 담은 더 친근한 데이터 구조를 make할 수 있다면 더 멋지지 않을까요?

문법의 TOP이 전체 문자열을 매치하는 것처럼, 액션에는 TOP 메서드도 있어요. datasubjectcommand 같은 개별 매치 구성 요소를 make한 다음, TOP에서 make할 데이터 구조에 그것들을 넣을 수 있어요. 최종 매치 객체를 반환하면 이 데이터 구조에 접근할 수 있어요.

이를 위해 액션 클래스에 TOP 메서드를 추가하고 구성 요소들로 어떤 데이터 구조든 make하면 돼요.

그래서 우리 액션 클래스는 이렇게 돼요.

class REST-actions
{
    method TOP ($/) {
        make { subject => $<subject>.Str,
               command => $<command>.Str,
               data    => $<data>.made }
    }

    method data($/) { make $/.split('/') }
}

TOP 메서드에서 subject는 문법에서 매치한 subject와 동일해요. command는 매치된 유효한 <sym>(create, update, retrieve, delete)을 반환해요. 전체 매치 객체가 필요 없으므로 각각 .Str로 강제해요.

$<data> 객체에는 made 메서드를 써야 해요. 우리가 액션에서 make로 만든 분리된 것을 접근하고 싶으니까요. 원본 $<data> 객체가 아니라요.

문법 액션의 TOP 메서드에서 무언가 make한 후에는, 문법 결과 객체에 made 메서드를 호출해 모든 사용자 정의 값을 접근할 수 있어요. 코드는 이제 이렇게 돼요.

my $uri = '/product/update/7/notify';

my $match = REST.parse($uri, actions => REST-actions.new);

my $rest = $match.made;
say $rest<data>[0];   # OUTPUT: «7␤»
say $rest<command>;   # OUTPUT: «update␤»
say $rest<subject>;   # OUTPUT: «product␤»

완전한 반환 매치 객체가 필요 없다면, 액션의 TOP에서 made 데이터만 반환할 수도 있어요.

my $uri = '/product/update/7/notify';

my $rest = REST.parse($uri, actions => REST-actions.new).made;

say $rest<data>[0];   # OUTPUT: «7␤»
say $rest<command>;   # OUTPUT: «update␤»
say $rest<subject>;   # OUTPUT: «product␤»

아, 그 추한 배열 요소 번호를 없애는 걸 잊었네요? 흠. TOP의 문법 사용자 반환에 새 것을 만들어 볼게요... subject-id라고 하고 <data>의 요소 0으로 설정하는 건 어떨까요.

class REST-actions
{
    method TOP ($/) {
        make { subject    => $<subject>.Str,
               command    => $<command>.Str,
               data       => $<data>.made,
               subject-id => $<data>.made[0] }
    }

    method data($/) { make $/.split('/') }
}

이제 대신 이렇게 할 수 있어요.

my $uri = '/product/update/7/notify';

my $rest = REST.parse($uri, actions => REST-actions.new).made;

say $rest<command>;    # OUTPUT: «update␤»
say $rest<subject>;    # OUTPUT: «product␤»
say $rest<subject-id>; # OUTPUT: «7␤»

최종 코드는 이래요.

grammar REST
{
    token TOP { <slash><subject><slash><command>[<slash><data>]? }

    proto token command {*}
    token command:sym<create>   { <sym> }
    token command:sym<retrieve> { <sym> }
    token command:sym<update>   { <sym> }
    token command:sym<delete>   { <sym> }

    token subject { \w+ }
    token data    { .* }
    token slash   { \s* '/' \s* }
}

class REST-actions
{
    method TOP ($/) {
        make { subject    => $<subject>.Str,
               command    => $<command>.Str,
               data       => $<data>.made,
               subject-id => $<data>.made[0] }
    }

    method data($/) { make $/.split('/') }
}

액션 직접 추가

위에서 문법과 액션 객체를 연결하고 매치 객체에 액션을 수행하는 방법을 봤어요. 하지만 매치 객체를 다뤄야 할 때, 그게 유일한 방법은 아니에요. 아래 예제를 보세요.

grammar G {
  rule TOP { <function-define> }
  rule function-define {
    'sub' <identifier>
    {
      say "func " ~ $<identifier>.made;
      make $<identifier>.made;
    }
    '(' <parameter> ')' '{' '}'
    { say "end " ~ $/.made; }
  }
  token identifier { \w+ { make ~$/; } }
  token parameter { \w+ { say "param " ~ $/; } }
}

G.parse('sub f ( a ) { }');
# OUTPUT: «func f␤param a␤end f␤»

이 예제는 파서의 축소된 부분이에요. 그것이 보여주는 기능에 더 집중해 보죠.

첫째, 문법 자체 안에 액션을 추가할 수 있고, 그런 액션은 regex의 제어 흐름이 그 지점에 도달하면 수행돼요. 액션 객체의 메서드는 항상 전체 regex 항목이 매치된 후에 수행된다는 점에 주목하세요. 둘째, make가 실제로 무엇을 하는지 보여주는데, 이는 $/.made = ...의 설탕(sugar)에 지나지 않아요. 그리고 이 트릭은 regex 항목 내에서 메시지를 전달하는 방법을 소개해요.

이 글이 Raku의 문법을 소개하고, 문법과 문법 액션 클래스가 어떻게 함께 동작하는지 보여줬길 바라요. 더 자세한 내용은 더 고급인 Raku Grammar Guide를 확인하세요.

문법 디버깅에 대해서는 Grammar::Debugger를 보세요. 이것은 각 문법 토큰에 대한 중단점과 색상 코딩된 MATCH와 FAIL 출력을 제공해요.