문법

문법 (Grammars)

텍스트를 구조화 해체하고, 종종 그 텍스트를 해석해 만들어진 데이터 구조를 반환하는 데 쓰는 강력한 도구가 문법(grammar)이에요. Raku 자체도 Raku 스타일의 문법으로 파싱되고 실행돼요. 보통 Raku 사용자에게 더 실용적인 예로는 JSON::Tiny 모듈이 있는데, 이 모듈은 어떤 유효한 JSON 파일이든 역직렬화할 수 있어요. 그런데 그 역직렬화 코드는 100줄 미만의 간단하고 확장 가능한 코드로 작성돼 있어요.

출처: Raku 공식 문서 — Grammars

본문

학교에서 문법이라면 질색이었다고 해도, 그것 때문에 문법을 무서워할 필요는 없어요. 문법은 정규식들을 그룹화하게 해 주는데, 마치 클래스가 일반 코드의 메서드들을 그룹화하게 해 주는 것과 같아요.

이름 있는 정규식 (Named Regexes)

문법의 주 재료는 이름 있는 정규식(named regex)이에요. Raku 정규식의 문법 자체는 이 문서의 범위를 벗어나지만, 이름 있는 정규식은 서브루틴 정의와 비슷한 특별한 문법을 가져요. [1]

my regex number { \d+ [ \. \d+ ]? }

이 경우 정규식이 my 키워드로 어휘 스코프라는 걸 지정해야 해요. 이름 있는 정규식은 보통 문법 안에서 쓰이기 때문이에요.

이름이 있다는 건 그 정규식을 다른 곳에서 쉽게 재사용할 수 있다는 이점을 줘요.

say so "32.51" ~~ &number;                         # OUTPUT: «True␤»
say so "15 + 4.5" ~~ /<number>\s* '+' \s*<number>/ # OUTPUT: «True␤»

regex 가 이름 있는 정규식의 유일한 선언자는 아니에요. 사실 가장 흔하지 않아요. 대부분의 경우 token 또는 rule 선언자가 쓰여요. 이 둘은 모두 래칫(ratcheting) 방식이어서, 매치 엔진이 무언가 매치에 실패해도 되돌아가 다시 시도하지 않아요. 이는 보통 원하는 대로 동작하지만 모든 경우에 적합한 건 아니에요.

my regex works-but-slow { .+ q }
my token fails-but-fast { .+ q }
my $s = 'Tokens won\'t backtrack, which makes them fail quicker!';
say so $s ~~ &works-but-slow; # OUTPUT: «True␤»
say so $s ~~ &fails-but-fast; # OUTPUT: «False␤»
                              # the entire string is taken by the .+

비역추적(non-backtracking)은 항(term) 단위로 동작한다는 점에 주목하세요. 아래 예제처럼, 무언가 매치하면 절대 역추적하지 않아요. 하지만 매치에 실패했을 때 |||가 도입한 다른 후보가 있으면 다시 매치를 시도해요.

my token tok-a { .* d  };
my token tok-b { .* d | bd };
say so "bd" ~~ &tok-a;        # OUTPUT: «False␤»
say so "bd" ~~ &tok-b;        # OUTPUT: «True␤»

룰 (Rules)

tokenrule 선언자의 유일한 차이는 rule 선언자가 정규식에 :sigspace가 적용되게 한다는 거예요.

my token token-match { 'once' 'upon' 'a' 'time' }
my rule  rule-match  { 'once' 'upon' 'a' 'time' }
say so 'onceuponatime'    ~~ &token-match; # OUTPUT: «True␤»
say so 'once upon a time' ~~ &token-match; # OUTPUT: «False␤»
say so 'onceuponatime'    ~~ &rule-match;  # OUTPUT: «False␤»
say so 'once upon a time' ~~ &rule-match;  # OUTPUT: «True␤»

문법 만들기 (Creating grammars)

Grammarclass 키워드 대신 grammar 키워드로 선언된 클래스가 자동으로 얻는 수퍼클래스예요. 문법은 텍스트 파싱에만 쓰여야 해요. 복잡한 데이터를 추출하고 싶다면 문법 안에 액션을 추가하거나 문법과 함께 액션 객체를 쓰면 돼요.

프로토 정규식 (Proto regexes)

Grammar는 룰, 토큰, 정규식으로 구성돼요. 문법은 클래스이므로 이것들은 사실 메서드예요. [2]

이 메서드들은 이름과 기능을 공유할 수 있으므로 proto를 사용할 수 있어요.

예를 들어 분기(alternation)가 많으면 읽기 쉬운 코드를 만들거나 문법을 서브클래싱하는 게 어려워질 수 있어요. 아래 Calculations 클래스에서 method TOP의 삼항 연산자는 이상적이지 않고, 연산을 더 추가할수록 더 나빠져요.

grammar Calculator {
    token TOP { [ <add> | <sub> ] }
    rule  add { <num> '+' <num> }
    rule  sub { <num> '-' <num> }
    token num { \d+ }
}

class Calculations {
    method TOP ($/) { make $<add> ?? $<add>.made !! $<sub>.made; }
    method add ($/) { make [+] $<num>; }
    method sub ($/) { make [-] $<num>; }
}

say Calculator.parse('2 + 3', actions => Calculations).made;

# OUTPUT: «5␤»

더 나아지게 하려면 토큰에 :sym<...> adverb처럼 보이는 프로토 정규식을 쓰면 돼요.

grammar Calculator {
    token TOP { <calc-op> }

    proto rule calc-op          {*}
          rule calc-op:sym<add> { <num> '+' <num> }
          rule calc-op:sym<sub> { <num> '-' <num> }

    token num { \d+ }
}

class Calculations {
    method TOP              ($/) { make $<calc-op>.made; }
    method calc-op:sym<add> ($/) { make [+] $<num>; }
    method calc-op:sym<sub> ($/) { make [-] $<num>; }
}

say Calculator.parse('2 + 3', actions => Calculations).made;

# OUTPUT: «5␤»

이 문법에서 분기는 이제 <calc-op>로 대체됐어요. 이것은 본질적으로 우리가 만들 값 그룹의 이름이에요. proto rule calc-op로 룰 프로토타입을 정의함으로써 그렇게 해요. 이전의 각 분기는 새 rule calc-op 정의로 대체되고, 분기 이름은 :sym<> adverb로 붙어요.

액션을 선언하는 클래스에서는 이제 삼항 연산자를 없애고 $<calc-op> 매치 객체에서 .made 값을 그냥 가져와요. 그리고 개별 분기에 대한 액션은 문법과 같은 명명 패턴을 따라요: method calc-op:sym<add>method calc-op:sym<sub>.

이 방법의 진짜 아름다움은 문법과 액션 클래스를 서브클래싱할 때 볼 수 있어요. 계산기에 곱셈 기능을 추가하고 싶다고 해 보죠.

grammar BetterCalculator is Calculator {
    rule calc-op:sym<mult> { <num> '*' <num> }
}

class BetterCalculations is Calculations {
    method calc-op:sym<mult> ($/) { make [*] $<num> }
}

say BetterCalculator.parse('2 * 3', actions => BetterCalculations).made;

# OUTPUT: «6␤»

calc-op 그룹에 룰과 액션을 하나씩 추가하기만 하면 동작해요. 모두 프로토 정규식 덕분이에요.

특별한 토큰 (Special tokens)

TOP

grammar Foo {
    token TOP { \d+ }
}

TOP 토큰은 문법으로 파싱할 때 처음 시도되는 기본 토큰이에요. .parse 메서드로 파싱하면 token TOP이 문자열의 시작과 끝에 자동으로 고정(anchor)된다는 점에 주목하세요. 문자열 전체를 파싱하고 싶지 않으면 .subparse를 찾아보세요.

rule TOP이나 regex TOP을 쓰는 것도 괜찮아요.

.parse, .subparse, .parsefile:rule 이름 인자를 써서 처음 매치할 다른 토큰을 선택할 수 있어요. 이들은 모두 Grammar 메서드예요.

ws

기본 ws는 그 지점이 단어 안이 아니면 0개 이상의 공백 문자를 매치해요 (코드 형태로는 token ws { <!ww> \s* }).

# First <.ws> matches word boundary at the start of the line
# and second <.ws> matches the whitespace between 'b' and 'c'
say 'ab   c' ~~ /<.ws> ab <.ws> c /; # OUTPUT: «「ab   c」␤»

# Failed match: there is neither any whitespace nor a word
# boundary between 'a' and 'b'
say 'ab' ~~ /. <.ws> b/;             # OUTPUT: «Nil␤»

# Successful match: there is a word boundary between ')' and 'b'
say ')b' ~~ /. <.ws> b/;             # OUTPUT: «「)b」␤»

우리가 관심 없으므로 캡처를 피하기 위해 ws 앞에 점을 붙인다는 점을 기억하세요. 일반적으로 공백은 구분자이므로, 그게 대부분 발견되는 방식이에요.

token 대신 rule을 쓰면 :sigspace가 기본으로 활성화되고, 항과 닫는 괄호/대괄호 뒤의 공백이 <.ws>라고 쓰인 비캡처 ws 호출로 바뀌어요. 여기서 .는 비캡처를 뜻해요. 다시 말해:

rule entry { <key> '=' <value> }

이것은 다음과 같아요.

token entry { <key> <.ws> '=' <.ws> <value> <.ws> }

기본 ws 토큰을 재정의할 수도 있어요.

grammar Foo {
    rule TOP { \d \d }
}.parse: "4   \n\n 5"; # Succeeds

grammar Bar {
    rule TOP { \d \d }
    token ws { \h*   }
}.parse: "4   \n\n 5"; # Fails

심지어 캡처할 수도 있지만, 명시적으로 사용해야 해요. 다음 예제에서는 rule 대신 token을 쓴다는 점에 주목하세요. rule이면 암묵적 비캡처 .ws가 공백을 소비할 테니까요.

grammar Foo { token TOP {\d <ws> \d} };
my $parsed = Foo.parse: "3 3";
say $parsed<ws>; # OUTPUT: «「 」␤»

sym

<sym> 토큰은 프로토 정규식 안에서 그 특정 정규식의 :sym adverb의 문자열 값을 매치하는 데 쓸 수 있어요.

grammar Foo {
    token TOP { <letter>+ }
    proto token letter {*}
          token letter:sym<R> { <sym> }
          token letter:sym<a> { <sym> }
          token letter:sym<k> { <sym> }
          token letter:sym<u> { <sym> }
          token letter:sym<*> {   .   }
}.parse("I ♥ Raku", actions => class {
    method TOP($/) { make $<letter>.grep(*.<sym>).join }
}).made.say; # OUTPUT: «Raku␤»

이미 매치하려는 문자열로 프로토 정규식을 구분하고 있다면, <sym> 토큰을 쓰는 것이 그 문자열들의 반복을 막으므로 편리해요.

"항상 성공" 어서션 (Always succeed assertion)

<?>항상 성공 어서션(always succeed assertion)이에요. 문법 토큰으로 쓰면 Action 클래스 메서드를 트리거하는 데 쓸 수 있어요. 다음 문법에서는 아라비아 숫자를 찾고 항상 성공 어서션으로 succ 토큰을 정의해요.

액션 클래스에서는 succ 메서드 호출로 설정을 수행해요(이 경우 @!numbers에 새 요소를 준비해요). digit 메서드에서는 아라비아 숫자를 데바나가리 숫자 목록의 인덱스로 사용하고 그것을 @!numbers의 마지막 요소에 더해요. succ 덕분에 마지막 요소는 항상 현재 파싱된 digit 숫자들에 대한 숫자가 돼요.

grammar Digifier {
    rule TOP {
        [ <.succ> <digit>+ ]+
    }
    token succ   { <?> }
    token digit { <[0..9]> }
}

class Devanagari {
    has @!numbers;
    method digit ($/) { @!numbers.tail ~= <०  १  २  ३  ४  ५  ६  ७  ८  ९>[$/] }
    method succ  ($)  { @!numbers.push: ''     }
    method TOP   ($/) { make @!numbers[^(*-1)] }
}

say Digifier.parse('255 435 777', actions => Devanagari.new).made;
# OUTPUT: «(२५५ ४३५ ७७७)␤»

문법의 메서드 (Methods in grammars)

문법에서 룰이나 토큰 대신 메서드를 쓰는 건 괜찮아요. Match를 반환하기만 하면요.

grammar DigitMatcher {
    method TOP (:$full-unicode) {
        $full-unicode ?? self.num-full !! self.num-basic;
    }
    token num-full  { \d+ }
    token num-basic { <[0..9]>+ }
}

위 문법은 subparse 메서드에 제공된 인자에 따라 다른 매치를 시도할 거예요.

say +DigitMatcher.subparse: '12७१७९०९', args => \(:full-unicode);
# OUTPUT: «12717909␤»

say +DigitMatcher.subparse: '12७१७९०९', args => \(:!full-unicode);
# OUTPUT: «12␤»

문법의 동적 변수 (Dynamic variables in grammars)

토큰에서 변수는 그것을 정의하는 코드 줄 앞에 :를 붙여 정의할 수 있어요. 임의의 코드는 중괄호로 감싸 토큰의 어디에든 임베드할 수 있어요. 이는 토큰 사이에 상태를 유지하는 데 유용하며, 문법이 텍스트를 파싱하는 방식을 바꾸는 데 쓸 수 있어요. 토큰에서 동적 변수($*, @*, &*, %* 트윌길을 가진 변수)를 쓰는 것은 그것이 정의된 토큰 이후에 정의된 모든 토큰으로 계단식으로 전파돼요. 토큰에서 토큰으로 인자로 전달하지 않아도 되게 해 주죠.

동적 변수의 용도 중 하나는 매치 가드(guard)예요. 이 예제는 어떤 regex 클래스가 공백을 문자 그대로 파싱하는지 설명하기 위해 가드를 사용해요.

grammar GrammarAdvice {
    rule TOP {
        :my Int $*USE-WS;
        "use" <type> "for" <significance> "whitespace by default"
    }
    token type {
        | "rules"   { $*USE-WS = 1 }
        | "tokens"  { $*USE-WS = 0 }
        | "regexes" { $*USE-WS = 0 }
    }
    token significance {
        | <?{ $*USE-WS == 1 }> "significant"
        | <?{ $*USE-WS == 0 }> "insignificant"
    }
}

여기서 "use rules for significant whitespace by default" 같은 텍스트는, rules/tokens/regexes 중 무엇이 언급되는지로 할당된 상태가 올바른 가드와 매치할 때만 매치돼요.

say GrammarAdvice.subparse("use rules for significant whitespace by default");
# OUTPUT: «use rules for significant whitespace by default␤»

say GrammarAdvice.subparse("use tokens for insignificant whitespace by default");
# OUTPUT: «use tokens for insignificant whitespace by default␤»

say GrammarAdvice.subparse("use regexes for insignificant whitespace by default");
# OUTPUT: «use regexes for insignificant whitespace by default␤»

say GrammarAdvice.subparse("use regexes for significant whitespace by default");
# OUTPUT: #<failed match>

문법의 속성 (Attributes in grammars)

문법에 속성을 정의할 수 있어요. 하지만 그것은 메서드로만 접근할 수 있어요. 토큰 안에서 사용하려 하면 예외가 던져져요. 토큰은 문법 자체가 아니라 Match의 메서드이기 때문이에요. 토큰 안에서 호출된 메서드로 속성을 수정하면 그 토큰 자신의 매치 객체에 대해서만 속성을 수정한다는 점에 주목하세요! 공개로 만들면 파싱 후 반환된 매치에서 문법 속성에 접근할 수 있어요.

grammar HTTPRequest {
    has Bool $.invalid;

    token TOP {
        <type> <.ns> <path> <.ns> 'HTTP/1.1' <.crlf>
        [ <field> <.crlf> ]+
        <.crlf>
        $<body>=.*
    }

    token type {
        | [ GET | POST | OPTIONS | HEAD | PUT | DELETE | TRACE | CONNECT ] <.accept>
        | <-[\/]>+ <.error>
    }

    token path {
        | '/' [[\w+]+ % \/] [\.\w+]? <.accept>
        | '*' <.accept>
        | \S+ <.error>
    }

    token field {
        | $<name>=\w+ <.ns> ':' <.ns> $<value>=<-crlf>* <.accept>
        | <-crlf>+ <.error>
    }

    method error(--> ::?CLASS:D) {
        $!invalid = True;
        self;
    }

    method accept(--> ::?CLASS:D) {
        $!invalid = False;
        self;
    }

    token crlf { # network new line (usually seen as "\r\n")
        # Several internet protocols (such as HTTP, RF 2616) mandate
        # the use of ASCII CR+LF (0x0D 0x0A) to terminate lines at
        # the protocol level (even though, in practice, some applications
        # tolerate a single LF).
        # Raku, Raku grammars and strings (Str) adhere to Unicode
        # conformance. Thus, CR+LF cannot be expressed unambiguously
        # as \r\n in in Raku grammars or strings (Str), as Unicode
        # conformance requires \r\n to be interpreted as \n alone.
        \x[0d] \x[0a]
    }
    token ns { # network space
        # <ws> would consume, e.g., newlines, and \h (and \s) would accept
        # more codepoints than just ASCII single space and the tab character.
        [ ' ' | <[\t]> ]*
    }
}

my $crlf = "\x[0d]\x[0a]";
my $header = "GOT /index.html HTTP/1.1{$crlf}Host: docs.raku.org{$crlf}{$crlf}body";
my $m = HTTPRequest.parse($header);
say "type(\"$m.<type>\")={$m.<type>.invalid}";
# OUTPUT: type("GOT ")=True
say "path(\"$m.<path>\")={$m.<path>.invalid}";
# OUTPUT: path("/index.html")=False
say "field(\"$m.<field>[0]\")={$m.<field>[0].invalid}";
# OUTPUT: field("Host: docs.raku.org")=False

참고: $crlf<.crlf> 토큰은 (이 불완전한 예제의 맥락에서) 엄격히 HTTP/1.1(RFC 2616)을 따르고 싶다면 필요해요. 그 이유는 Raku가 RFC 2616과 달리 Unicode를 준수하고, \r\n을 단독 \n으로 해석해야 하므로, 문법이 HTTP 프로토콜이 기대하는 의미로 \r\n을 포함한 문자열을 제대로 파싱하지 못하기 때문이에요. invalid 속성이 각 구성 요소에 지역적이라는 점(예: <type>의 값은 True이지만 <path>의 값은 False)을 주목하세요. 또한 accept에 대한 메서드가 있다는 점도 주목하세요. 그렇지 않으면 invalid 속성이 (존재하더라도) 초기화되지 않았을 테니까요.

문법에 인자 전달 (Passing arguments into grammars)

문법에 인자를 전달하려면 문법의 파싱 메서드 중 아무 거나 :args 이름 인자를 쓰면 돼요. 전달되는 인자는 list에 있어야 해요.

grammar demonstrate-arguments {
    rule TOP ($word) {
    "I like" $word
    }
}

# Notice the comma after "sweets" when passed to :args to coerce it to a list
say demonstrate-arguments.parse("I like sweets", :args(("sweets",)));
# OUTPUT: «「I like sweets」␤»

인자가 전달되면 문법 안의 이름 있는 정규식 호출에서 사용할 수 있어요.

grammar demonstrate-arguments-again {
    rule TOP ($word) {
    <phrase-stem><added-word($word)>
    }

    rule phrase-stem {
       "I like"
    }

    rule added-word($passed-word) {
       $passed-word
    }
}

say demonstrate-arguments-again.parse("I like vegetables", :args(("vegetables",)));
# OUTPUT: 「I like vegetables」␤»
# OUTPUT:  «phrase-stem => 「I like 」␤»
# OUTPUT:  «added-word => 「vegetables」␤»

대안으로 동적 변수를 초기화하고 문법 안에서 인자를 그렇게 사용할 수 있어요.

grammar demonstrate-arguments-dynamic {
   rule TOP ($*word, $*extra) {
      <phrase-stem><added-words>
   }
   rule phrase-stem {
      "I like"
   }
   rule added-words {
      $*word $*extra
   }
}

say demonstrate-arguments-dynamic.parse("I like everything else",
  :args(("everything", "else")));
# OUTPUT: «「I like everything else」␤»
# OUTPUT:  «phrase-stem => 「I like 」␤»
# OUTPUT:  «added-words => 「everything else」␤»

액션 객체 (Action objects)

성공적인 문법 매치는 Match 객체의 파스 트리를 주는데, 그 매치 트리가 깊어질수록, 문법의 가지가 많을수록, 실제로 관심 있는 정보를 얻기 위해 매치 트리를 탐색하기가 더 어려워져요.

매치 트리를 깊이 파고들 필요를 피하려면 actions 객체를 제공하면 돼요. 문법의 이름 있는 룰을 각각 성공적으로 파싱한 후, 그것은 문법 룰과 같은 이름의 메서드를 호출하려 하고, 새로 만들어진 Match 객체를 위치 인자로 줘요. 그런 메서드가 없으면 건너뛰어요.

문법과 액션이 작동하는 조작적(contrived) 예제는 이래요.

grammar TestGrammar {
    token TOP { \d+ }
}

class TestActions {
    method TOP($/) {
        make(2 + $/);
    }
}

my $match = TestGrammar.parse('40', actions => TestActions.new);
say $match;         # OUTPUT: «「40」␤»
say $match.made;    # OUTPUT: «42␤»

TestActions의 인스턴스가 actions 이름 인자로 parse 호출에 전달되고, TOP 토큰이 성공적으로 매치하면 TOP 메서드를 자동으로 호출하며 매치 객체를 인자로 전달해요.

인자가 매치 객체라는 것을 명확히 하기 위해 예제는 $/를 액션 메서드의 파라미터 이름으로 써요. 이건 그냥 편리한 관례이지 본질적인 건 아니에요. $match도 동작했을 거예요. 다만 $/를 쓰면 $<capture>$/<capture>의 단축키로 제공하는 이점이 있긴 해요. 어쨌든 TOP의 액션에서는 다른 인자를 써요.

약간 더 복잡한 예제가 이어져요.

grammar KeyValuePairs {
    token TOP {
        [<pair> \v+]*
    }

    token pair {
        <key=.identifier> '=' <value=.identifier>
    }

    token identifier {
        \w+
    }
}

class KeyValuePairsActions {
    method pair      ($/) {
        make $/<key>.made => $/<value>.made
    }
    method identifier($/) {
        # subroutine `make` is the same as calling .make on $/
        make ~$/
    }

    method TOP ($match) {
        # can use any variable name for parameter, not just $/
        $match.make: $match<pair>».made
    }
}

my $actions = KeyValuePairsActions;
my @res = KeyValuePairs.parse(q:to/EOI/, :$actions).made;
second=b
hits=42
raku=d
EOI

for @res -> $p {
    say "Key: $p.key()\tValue: $p.value()";
}

이것은 다음 출력을 만들어요.

Key: second     Value: b
Key: hits       Value: 42
Key: raku       Value: d

등호로 구분된 쌍을 파싱한 pair 룰은 identifier 토큰에 대한 두 호출을 별도의 캡처 이름으로 별칭해 그들이 대응하는 Action에서처럼 더 쉽고 직관적으로 사용 가능하게 해요. 대응하는 액션 메서드는 Pair 객체를 만들고 하위 매치 객체의 .made 속성을 사용해요. 그래서 (액션 메서드 TOP처럼) 하위 매치에 대한 액션 메서드가 호출/외부 regex의 그것보다 먼저 호출된다는 사실을 이용해요. 그래서 액션 메서드는 후위 순서(post-order)로 호출돼요.

액션 메서드 TOP은 단순히 pair 룰의 여러 매치가 .made한 모든 객체를 모아 목록으로 반환해요. 이 경우 액션 메서드의 인자가 $/일 때만 루틴 형태를 쓸 수 있으므로 make의 메서드 형태를 써야 한다는 점에 유의하세요. 반대로 메서드의 인자가 $/라면, $/.make과 동등한 make를 간단히 쓸 수 있어요.

또한 KeyValuePairsActions가 타입 객체로 parse 메서드에 전달된 것도 주목하세요. 이것은 액션 메서드 중 어느 것도 속성(인스턴스에서만 사용 가능한)을 사용하지 않기 때문에 가능했어요.

상속을 사용해 위 예제를 확장할 수 있어요.

use KeyValuePairs;

unit grammar ConfigurationSets is KeyValuePairs;

token TOP {
    <configuration-element>+ %% \v
}

token configuration-element {
    <pair>+ %% \v
}

token comment {
    \s* '#' .+? $$
}

token pair {
    <key=.identifier> '=' <value=.identifier> <comment>?
}

이전 예제를 서브클래싱(실제로는 서브-그래머링)하고 있어요. comment를 추가해 pair의 정의를 오버라이드했고, 이전 TOP 룰은 configuration-element로 강등됐으며, 이제 수직 공백으로 구분된 구성 요소 집합을 고려하는 새 TOP이 있어요. 액션 클래스를 서브클래싱해 액션도 재사용할 수 있어요.

use KeyValuePairs;

unit class ConfigurationSetsActions is KeyValuePairsActions;

method configuration-element($match) {
    $match.make: $match<pair>».made
}

method TOP ($match) {
    my @made-elements = gather for $match<configuration-element> {
        take $_.made
    };
    $match.make( @made-elements );

}

기존 액션은 모두 재사용되지만, 문법의 새 요소( TOP 포함)에는 당연히 새 액션을 써야 해요. 이것들은 이 스크립트에서 함께 사용할 수 있어요.

use ConfigurationSets;
use ConfigurationSetsActions;

my $actions = ConfigurationSetsActions;
my $sets = ConfigurationSets.parse(q:to/EOI/, :$actions).made;
second=b # Just a thing
hits=42
raku=d

third=c # New one
hits=33
EOI

for @$sets -> $set {
    say "Element→ $set";
}

이것은

Element→ second b hits 42 raku d
Element→ third c hits 33

을 출력할 거예요.

다른 경우 액션 메서드는 속성에 상태를 유지하고 싶어할 수 있어요. 그러면 당연히 parse 메서드에 인스턴스를 전달해야 해요.

token ws는 특별하다는 점에 유의하세요. :sigspace가 활성화되면(rule을 쓸 때 그렇죠) 특정 공백 시퀀스를 대체해요. 그게 rule pair에서 등호 주변의 공백이 잘 동작하는 이유이고, 닫는 } 앞의 공백이 token TOP에서 찾는 개행을 삼키지 않는 이유예요.