예제 매크로
예제 매크로 (Macros by example)
macro_rules!를 쓰면 선언적(declarative) 방식으로 문법 확장을 정의할 수 있어요. 우리는 이런 확장을 "예제 매크로(macros by example)" 또는 줄여서 "매크로"라고 불러요.
출처: Rust Reference
본문
각 예제 매크로는 이름과 **하나 이상의 규칙(rule)**을 가져요. 각 규칙은 두 부분으로 이루어지는데요. 매치할 문법을 설명하는 **매처(matcher)**와, 성공적으로 매치된 호출을 대체할 문법을 설명하는 **트랜스크라이버(transcriber)**가 그거예요. 매처와 트랜스크라이버는 모두 구분자(delimiter)로 둘러싸여 있어야 해요. 매크로는 표현식, 구문, 항목(트레이트·구현·외부 항목 포함), 타입, 패턴으로 확장될 수 있어요.
트랜스크라이빙(Transcribing)
매크로가 호출되면, 매크로 확장기(expander)가 호출을 이름으로 찾아서 각 매크로 규칙을 차례로 시도해요. 첫 번째로 성공한 매치를 트랜스크라이브하는데, 그 결과가 에러라면 이후 매치는 시도되지 않아요.
매칭할 때 선행 검색(lookahead)은 수행되지 않아요. 컴파일러가 토큰 하나씩 봐도 호출을 모호하지 않게 파싱할 방법을 확정하지 못하면 그것은 에러예요. 아래 예시에서 컴파일러는 식별자 너머를 보지 않아요. 다음 토큰이 )인지 확인하면 호출을 명확히 파싱할 수 있는데도, 그렇게 하지 않죠.
#![allow(unused)]
fn main() {
macro_rules! ambiguity {
($($i:ident)* $j:ident) => { };
}
ambiguity!(error); // Error: local ambiguity
}
매처와 트랜스크라이버 양쪽에서 $ 토큰은 매크로 엔진의 특수 동작을 일으켜요(아래의 메타변수·반복에서 설명). 그런 호출의 일부가 아닌 토큰은 문자 그대로 매치되고 트랜스크라이브돼요. 단 한 가지 예외가 있어요. 매처의 바깥 구분자는 어떤 쌍의 구분자와도 매치할 수 있어요. 그래서 예를 들어 매처 (())는 {()}와 매치되지만 {{}}와는 매치되지 않아요. $ 문자 자체는 문자 그대로 매치되거나 트랜스크라이브될 수 없어요.
매치된 조각 전달하기
매치된 조각을 다른 예제 매크로로 전달할 때, 두 번째 매크로의 매처는 그 조각 타입의 불투명한 AST를 보게 돼요. 두 번째 매크로는 매처에서 리터럴 토큰으로 그 조각들을 매치할 수 없고, 같은 타입의 조각 지정자(fragment specifier)만 사용할 수 있어요. ident, lifetime, tt 조각 타입은 예외라서 리터럴 토큰으로 매치할 수 있어요. 아래 예시가 이 제약을 보여줘요.
#![allow(unused)]
fn main() {
macro_rules! foo {
($l:expr) => { bar!($l); }
// ERROR: ^^ no rules expected this token in macro call
}
macro_rules! bar {
(3) => {}
}
foo!(3);
}
반면 tt 조각을 매치한 뒤에는 토큰을 직접 매치할 수 있어요.
#![allow(unused)]
fn main() {
// compiles OK
macro_rules! foo {
($l:tt) => { bar!($l); }
}
macro_rules! bar {
(3) => {}
}
foo!(3);
}
메타변수(Metavariables)
매처에서 $ 이름 : 조각-지정자 는 해당 종류의 Rust 문법 조각을 매치하고 그것을 메타변수 $ 이름 에 바인딩해요.
유효한 조각 지정자는 다음과 같아요.
block:BlockExpressionNoInnerAttributesexpr:Expressionexpr_2021:UnderscoreExpression과ConstBlockExpression을 제외한Expression(아래 2024 에디션 차이 참고)ident:_를 제외한IDENTIFIER_OR_KEYWORD또는RAW_IDENTIFIER또는$crateitem:Itemlifetime:LIFETIME_TOKENliteral:-? LiteralExpression와 매치meta:Attr, 즉 속성의 내용물pat:Pattern(아래 2021 에디션 차이 참고)pat_param:PatternNoTopAltpath:TypePathstmt: 끝 세미콜론이 없는Statement(세미콜론이 필요한 항목 구문은 제외)tt:TokenTree(단일 토큰 또는 매칭되는 구분자(),[],{}안의 토큰들)ty:Typevis: 비어 있을 수도 있는Visibility한정자
트랜스크라이버에서는 메타변수를 그냥 $ 이름 으로 가리켜요. 조각 종류는 매처에서 이미 지정됐으니까요. 메타변수는 그것을 매치한 문법 요소로 대체돼요. 메타변수는 여러 번 트랜스크라이브되거나 전혀 되지 않을 수도 있어요.
키워드 메타변수 $crate는 현재 크레이트를 가리키는 데 쓸 수 있어요.
2021 에디션 차이점: 2021 에디션부터
pat조각 지정자는 최상위 or-패턴과 매치해요 (즉Pattern을 받아요). 2021 에디션 이전에는pat_param과 정확히 같은 조각(즉PatternNoTopAlt)을 매치했어요. 관련 에디션은macro_rules!정의에 적용되는 에디션이에요.
2024 에디션 차이점: 2024 에디션 이전의
expr조각 지정자는 최상위에서UnderscoreExpression이나ConstBlockExpression과 매치하지 않아요. 하위 표현식 안에서는 허용돼요.expr_2021조각 지정자는 2024 이전 에디션과의 하위 호환성을 유지하기 위해 존재해요.
반복(Repetitions)
매처와 트랜스크라이버 양쪽에서 반복은 $( … ) 안에 반복할 토큰을 두고, 뒤에 반복 연산자를 붙여서 나타내요. 선택적으로 그 사이에 구분자(separator) 토큰을 둘 수도 있어요.
구분자 토큰은 구분자나 반복 연산자 외의 어떤 토큰이든 될 수 있어요. 하지만 ;와 ,가 가장 흔해요. 예를 들어 $( $i:ident ),*는 쉼표로 구분된 임의 개수의 식별자를 나타내요. 반복은 중첩할 수도 있어요.
반복 연산자는 다음과 같아요.
*— 임의의 개수 반복+— 최소 하나 이상 반복?— 0개 또는 1개만 오는 선택적 조각
?는 최대 한 번만 오는 걸 나타내므로 구분자와 함께 쓸 수 없어요.
반복되는 조각은 지정된 개수만큼 매치되고 트랜스크라이브되며, 각각 구분자 토큰으로 나뉘어요. 메타변수는 대응하는 조각의 모든 반복에 매치돼요. 예를 들어 위의 $( $i:ident ),* 예시는 $i를 목록의 모든 식별자에 매치해요.
트랜스크라이브할 때는 반복에 추가 제약이 적용돼서, 컴파일러가 어떻게 확장할지 알 수 있게 해요.
- 메타변수는 트랜스크라이버에서 매처에서와 정확히 같은 개수, 종류, 중첩 순서의 반복에 나타나야 해요. 그래서 매처
$( $i:ident ),*에 대해=> { $i },=> { $( $( $i )* )* },=> { $( $i )+ }는 모두 불법이고,=> { $( $i );* }만 올바르며 쉼표 구분 식별자 목록을 세미콜론 구분 목록으로 바꿔요. - 트랜스크라이버의 각 반복에는 몇 번 확장할지 결정할 메타변수가 적어도 하나 있어야 해요. 같은 반복에 여러 메타변수가 나타나면, 그것들은 같은 개수의 조각에 바인딩되어야 해요. 예를 들어
( $( $i:ident ),* ; $( $j:ident ),* ) => (( $( ($i,$j) ),* ))는$i조각과$j조각의 개수가 같아야 해요. 즉(a, b, c; d, e, f)로 호출하면((a,d), (b,e), (c,f))로 확장되지만, 개수가 같지 않은(a, b, c; d, e)는 불법이에요. 이 요구사항은 중첩된 반복의 모든 층에 적용돼요.
스코프, 내보내기, 가져오기
역사적인 이유로 예제 매크로의 스코핑은 항목과 완전히 똑같이 동작하지는 않아요. 매크로는 두 가지 형태의 스코프를 가져요. **텍스트 스코프(textual scope)**와 **경로 기반 스코프(path-based scope)**예요. 텍스트 스코프는 소스 파일에 나타나는 순서(여러 파일에 걸쳐서도)에 기반하며 이것이 기본 스코핑이에요. 경로 기반 스코프는 항목 스코핑과 정확히 같은 방식으로 동작해요. 매크로의 스코핑, 내보내기, 가져오기는 대부분 속성에 의해 제어돼요.
매크로가 비-정규(일부 경로가 아닌) 식별자로 호출되면 먼저 텍스트 스코프에서 찾아요. 결과가 없으면 경로 기반 스코프에서 찾아요. 매크로 이름이 경로로 정규화되어 있으면 경로 기반 스코프에서만 찾아요.
use lazy_static::lazy_static; // Path-based import.
macro_rules! lazy_static { // Textual definition.
(lazy) => {};
}
lazy_static!{lazy} // Textual lookup finds our macro first.
self::lazy_static!{} // Path-based lookup ignores our macro, finds imported one.
텍스트 스코프
텍스트 스코프는 소스 파일에 나타나는 순서에 크게 기반하며, let으로 선언한 지역 변수의 스코프와 비슷하게 동작하되 모듈 수준에서도 적용된다는 점이 달라요. macro_rules!로 매크로를 정의하면, 그 매크로는 정의 이후부터(이름은 호출 지점에서 찾으므로 재귀적으로 사용할 수 있어요), 둘러싼 스코프(보통 모듈)가 닫힐 때까지 스코프에 들어와요. 이것은 하위 모듈로 들어가고 심지어 여러 파일에 걸쳐서도 적용될 수 있어요.
//// src/lib.rs
mod has_macro {
// m!{} // Error: m is not in scope.
macro_rules! m {
() => {};
}
m!{} // OK: appears after declaration of m.
mod uses_macro;
}
// m!{} // Error: m is not in scope.
//// src/has_macro/uses_macro.rs
m!{} // OK: appears after declaration of m in src/lib.rs
매크로를 여러 번 정의하는 것은 에러가 아니에요. 가장 최근의 선언이 이전 것을 가려요(shadow), 물론 그것이 스코프 밖으로 나가지 않는 한.
#![allow(unused)]
fn main() {
macro_rules! m {
(1) => {};
}
m!(1);
mod inner {
m!(1);
macro_rules! m {
(2) => {};
}
// m!(1); // Error: no rule matches '1'
m!(2);
macro_rules! m {
(3) => {};
}
m!(3);
}
m!(1);
}
매크로는 함수 안에서도 지역적으로 선언하고 사용할 수 있고, 비슷하게 동작해요.
#![allow(unused)]
fn main() {
fn foo() {
// m!(); // Error: m is not in scope.
macro_rules! m {
() => {};
}
m!();
}
// m!(); // Error: m is not in scope.
}
매크로에 대한 텍스트 스코프 이름 바인딩은 경로 기반 스코프 이름 바인딩을 가려요.
#![allow(unused)]
fn main() {
macro_rules! m2 {
() => {
println!("m2");
};
}
// Resolves to path-based candidate from use declaration below.
m!(); // prints "m2\n"
// Introduce second candidate for `m` with textual scope.
//
// This shadows path-based candidate from below for the rest of this
// example.
macro_rules! m {
() => {
println!("m");
};
}
// Introduce `m2` macro as path-based candidate.
//
// This item is in scope for this entire example, not just below the
// use declaration.
use m2 as m;
// Resolves to the textual macro candidate from above the use
// declaration.
m!(); // prints "m\n"
}
참고: shadowing이 허용되지 않는 영역에 대해서는 이름 해석 모호성(name resolution ambiguities)을 참고하세요.
경로 기반 스코프
기본적으로 매크로는 경로 기반 스코프가 없어요. 매크로는 두 가지 방법으로 경로 기반 스코프를 얻을 수 있어요.
use선언을 통한 재-내보내기macro_export
매크로를 재-내보내면 크레이트 루트가 아닌 모듈에서 경로 기반 스코프를 줄 수 있어요.
#![allow(unused)]
fn main() {
mac::m!(); // OK: Path-based lookup finds `m` in the mac module.
mod mac {
// Introduce macro `m` with textual scope.
macro_rules! m {
() => {};
}
// Reexport with path-based scope from within `m`'s textual scope.
pub(crate) use m;
}
}
매크로는 pub(crate)의 암시적 가시성을 가져요. #[macro_export]는 암시적 가시성을 pub으로 바꿔요.
#![allow(unused)]
fn main() {
// Implicit visibility is `pub(crate)`.
macro_rules! private_m {
() => {};
}
// Implicit visibility is `pub`.
#[macro_export]
macro_rules! pub_m {
() => {};
}
pub(crate) use private_m as private_macro; // OK.
pub use pub_m as pub_macro; // OK.
}
#![allow(unused)]
fn main() {
// Implicit visibility is `pub(crate)`.
macro_rules! private_m {
() => {};
}
// Implicit visibility is `pub`.
#[macro_export]
macro_rules! pub_m {
() => {};
}
pub(crate) use private_m as private_macro; // OK.
pub use pub_m as pub_macro; // OK.
pub use private_m; // ERROR: `private_m` is only public within
// the crate and cannot be re-exported outside.
}
macro_use 속성
macro_use 속성은 두 가지 목적이 있어요. 모듈에 쓰면 그 안에서 정의된 매크로의 스코프를 넓힐 수 있고, extern crate에 쓰면 다른 크레이트의 매크로를 macro_use prelude로 가져올 수 있어요.
모듈에 쓴 예시를 볼게요.
#![allow(unused)]
fn main() {
#[macro_use]
mod inner {
macro_rules! m {
() => {};
}
}
m!();
}
#[macro_use]
extern crate log;
모듈에 쓰면 macro_use 속성은 MetaWord 문법을 사용해요. extern crate에 쓰면 MetaWord와 MetaListIdents 문법을 사용하는데, 이 문법들을 어떻게 쓰는지에 대해선 아래 prelude 절을 참고해요.
macro_use 속성은 **모듈이나 extern crate**에 적용할 수 있어요. 다른 위치에서 쓰면 rustc가 무시하지만 린트로 잡아요. (이것은 나중에 에러가 될 수도 있어요.) extern crate self에는 쓸 수 없어요.
macro_use 속성은 한 형식에 몇 번이든 쓸 수 있어요. MetaListIdents 문법의 여러 인스턴스를 지정할 수 있고, 지정된 모든 매크로의 합집합이 가져와져요.
참고: 모듈에서는 첫 번째 이후의
MetaWordmacro_use속성을 rustc가 린트로 잡아요.extern crate에서는 다른macro_use속성이 이미 가져온 매크로만 가져와서 효과가 없는 속성들을 린트로 잡아요. 두 개 이상의MetaListIdentsmacro_use속성이 같은 매크로를 가져오면 첫 번째를 린트로 잡아요.MetaWord속성이 있으면 모든MetaListIdents속성을 린트로 잡아요.MetaWord속성이 두 개 이상 있으면 첫 번째 이후의 것들을 린트로 잡아요.
macro_use가 모듈에 쓰이면, 모듈의 매크로 스코프는 모듈의 어휘 스코프를 넘어서 확장돼요.
#![allow(unused)]
fn main() {
#[macro_use]
mod inner {
macro_rules! m {
() => {};
}
}
m!(); // OK
}
크레이트 루트에서 extern crate 선언에 macro_use를 지정하면 그 크레이트의 내보낸 매크로를 가져와요. 이렇게 가져온 매크로는 텍스트가 아니라 macro_use prelude로 가져와져요. 그 말은 다른 어떤 이름으로든 가려질 수 있다는 뜻이에요. macro_use로 가져온 매크로는 import 문 앞에서도 사용할 수 있어요.
참고: rustc는 현재 충돌 시 마지막에 가져온 매크로를 선호해요. 이 동작에 의존하지 마세요. 일반적으로 Rust의 import는 순서에 무관하므로 이 동작은 특이해요.
macro_use의 이 동작은 미래에 바뀔 수 있어요. 자세한 내용은 Rust issue #148025를 참고하세요.
MetaWord 문법을 쓰면 모든 내보낸 매크로를 가져오고, MetaListIdents 문법을 쓰면 지정된 매크로만 가져와요.
#[macro_use(lazy_static)] // Or `#[macro_use]` to import all macros.
extern crate lazy_static;
lazy_static!{}
// self::lazy_static!{} // ERROR: lazy_static is not defined in `self`.
macro_use로 가져올 매크로는 macro_export로 내보내져 있어야 해요.
macro_export 속성
macro_export 속성은 매크로를 크레이트에서 내보내고, 크레이트 루트에서 경로 기반 해석이 가능하도록 만들어요.
#![allow(unused)]
fn main() {
self::m!();
// ^^^^ OK: Path-based lookup finds `m` in the current module.
m!(); // As above.
mod inner {
super::m!();
crate::m!();
}
mod mac {
#[macro_export]
macro_rules! m {
() => {};
}
}
}
macro_export 속성은 MetaWord와 MetaListIdents 문법을 사용해요. MetaListIdents 문법으로는 단일 local_inner_macros 값을 받아요. macro_export 속성은 macro_rules 정의에 적용할 수 있어요. 다른 위치에서 쓰면 rustc가 무시하지만 린트로 잡아요. (이것은 나중에 에러가 될 수도 있어요.) 매크로에 macro_export를 처음 쓴 것만 효과가 있어요.
기본적으로 매크로는 텍스트 스코프만 있고 경로로는 해석될 수 없어요. macro_export 속성을 쓰면 매크로가 크레이트 루트에서 사용 가능해지고 경로로 참조할 수 있게 돼요. macro_export 없이는 경로 기반 해석이 실패해요.
macro_rules! m {
() => {};
}
self::m!(); // ERROR
crate::m!(); // ERROR
fn main() {}
macro_export가 있으면 경로 기반 해석이 동작해요.
#[macro_export]
macro_rules! m {
() => {};
}
self::m!(); // OK
crate::m!(); // OK
fn main() {}
macro_export 속성은 매크로를 크레이트 루트에서 내보내서, 다른 크레이트에서 경로로 참조할 수 있게 해줘요. 예를 들어 log 크레이트에 다음과 같이 정의되어 있다고 해볼게요.
#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! warn {
($message:expr) => { eprintln!("WARN: {}", $message) };
}
}
다른 크레이트에서는 경로로 매크로를 참조할 수 있어요.
fn main() {
log::warn!("example warning");
}
또 macro_export는 extern crate에 macro_use를 써서 그 매크로를 macro_use prelude로 가져올 수 있게 해줘요.
#[macro_use]
extern crate log;
pub mod util {
pub fn do_thing() {
// Resolved via macro prelude.
warn!("example warning");
}
}
macro_export 속성에 local_inner_macros를 추가하면, 매크로 정의 안의 모든 단일-세그먼트 매크로 호출에 $crate:: 접두사가 암시적으로 붙어요.
참고: 이것은 주로
$crate가 언어에 추가되기 전에 쓰인 코드를 Rust 2018의 경로 기반 매크로 import로 마이그레이션하기 위한 도구예요. 새 코드에서 쓰는 것은 권장되지 않아요.
#![allow(unused)]
fn main() {
#[macro_export(local_inner_macros)]
macro_rules! helped {
() => { helper!() } // Automatically converted to $crate::helper!().
}
#[macro_export]
macro_rules! helper {
() => { () }
}
}
위생(Hygiene)
예제 매크로는 **혼합-사이트 위생(mixed-site hygiene)**을 가져요. 이 말은 루프 라벨, 블록 라벨, 지역 변수는 매크로 정의 지점에서 찾고, 다른 기호들은 매크로 호출 지점에서 찾는다는 뜻이에요. 예시를 볼게요.
#![allow(unused)]
fn main() {
let x = 1;
fn func() {
unreachable!("this is never called")
}
macro_rules! check {
() => {
assert_eq!(x, 1); // Uses `x` from the definition site.
func(); // Uses `func` from the invocation site.
};
}
{
let x = 2;
fn func() { /* does not panic */ }
check!();
}
}
매크로 확장에서 정의된 라벨과 지역 변수는 호출 사이에 공유되지 않아서, 아래 코드는 컴파일되지 않아요.
#![allow(unused)]
fn main() {
macro_rules! m {
(define) => {
let x = 1;
};
(refer) => {
dbg!(x);
};
}
m!(define);
m!(refer);
}
특수한 경우가 $crate 메타변수예요. 이것은 매크로를 정의하는 크레이트를 가리키고, 호출 지점에서 스코프에 없는 항목이나 매크로를 찾기 위해 경로의 시작에 쓸 수 있어요.
//// Definitions in the `helper_macro` crate.
#[macro_export]
macro_rules! helped {
// () => { helper!() } // This might lead to an error due to 'helper' not being in scope.
() => { $crate::helper!() }
}
#[macro_export]
macro_rules! helper {
() => { () }
}
//// Usage in another crate.
// Note that `helper_macro::helper` is not imported!
use helper_macro::helped;
fn unit() {
helped!();
}
$crate는 현재 크레이트를 가리키므로, 비-매크로 항목을 참조할 때는 완전히 정규화된 모듈 경로와 함께 써야 해요.
#![allow(unused)]
fn main() {
pub mod inner {
#[macro_export]
macro_rules! call_foo {
() => { $crate::inner::foo() };
}
pub fn foo() {}
}
}
또한 $crate가 매크로로 하여금 자신의 크레이트 안의 항목을 확장 시 참조하게 해주지만, 가시성에는 영향을 주지 않아요. 참조되는 항목이나 매크로는 여전히 호출 지점에서 보여야(visible) 해요. 아래 예시에서 foo()가 public이 아니므로, 크레이트 밖에서 call_foo!()를 호출하려는 시도는 모두 실패해요.
#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! call_foo {
() => { $crate::foo() };
}
fn foo() {}
}
참고: Rust 1.30 이전에는
$crate와local_inner_macros가 지원되지 않았어요. 이들은 경로 기반 매크로 import와 함께 추가됐어요. 그 이유는 매크로를 내보내는 크레이트의 사용자가 helper 매크로를 수동으로 import하지 않아도 되게 하려는 거였어요. helper 매크로를 쓰는 더 오래된 Rust 버전용 크레이트는, 경로 기반 import와 잘 동작하려면$crate나local_inner_macros를 쓰도록 수정해야 해요.
후속-집합 모호성 제약(Follow-set ambiguity restrictions)
매크로 시스템이 쓰는 파서는 상당히 강력하지만, 언어의 현재 또는 미래 버전에서 모호함을 막기 위해 제한돼 있어요. 특히 모호한 확장에 대한 규칙에 더해, 메타변수가 매치한 비-터미널(nonterminal) 뒤에는 그런 종류의 매치 이후에 안전하게 올 수 있다고 결정된 토큰이 와야 해요.
예를 들어 $i:expr [ , ] 같은 매처는 이론적으로 오늘날 Rust에서 받아들여질 수 있어요. [,]는 합법적인 표현식의 일부가 될 수 없으니 파싱이 항상 명확하기 때문이에요. 하지만 [는 뒤따르는 표현식을 시작할 수 있으므로, 표현식 다음에 올 수 없다고 안전하게 배제할 수 있는 문자는 아니에요. 만약 [,]가 나중 버전의 Rust에서 받아들여지면 이 매처는 모호해지거나 잘못 파싱되어, 동작하던 코드를 깨뜨릴 수 있어요. 반면 $i:expr,나 $i:expr; 같은 매처는 합법적인데, ,와 ;는 합법적인 표현식 구분자이기 때문이에요. 구체적인 규칙은 다음과 같아요.
expr와stmt는 다음 중 하나만 뒤에 올 수 있어요:=>,,,;pat_param는 다음 중 하나만 뒤에 올 수 있어요:=>,,,=,|,if,inpat는 다음 중 하나만 뒤에 올 수 있어요:=>,,,=,if,inpath와ty는 다음 중 하나만 뒤에 올 수 있어요:=>,,,=,|,;,:,>,>>,[,{,as,where, 또는block조각 지정자의 매크로 변수vis는 다음 중 하나만 뒤에 올 수 있어요:,,priv가 아닌 식별자(non-raw priv), 타입을 시작할 수 있는 어떤 토큰, 또는ident/ty/path조각 지정자의 메타변수- 그 외의 모든 조각 지정자는 제약이 없어요.
2021 에디션 차이점: 2021 에디션 이전에는
pat뒤에|도 올 수 있었어요.
반복이 관련되면, 규칙은 구분자를 고려한 모든 가능한 확장 횟수에 적용돼요. 즉 다음과 같아요.
- 반복에 구분자가 있으면, 그 구분자는 반복 내용물을 뒤따를 수 있어야 해요.
- 반복이 여러 번 반복될 수 있으면(
*또는+), 내용물은 그 자신을 뒤따를 수 있어야 해요. - 반복의 내용물은 앞에 오는 것을 뒤따를 수 있어야 하고, 뒤에 오는 것은 반복 내용물을 뒤따를 수 있어야 해요.
- 반복이 0번 매치될 수 있으면(
*또는?), 뒤에 오는 것은 앞에 오는 것을 뒤따를 수 있어야 해요.
더 자세한 내용은 공식 명세(formal specification)를 참고하세요.
더 알아보기
- 메타변수 조각 지정자와 반복 연산자의 조합을 직접 실험해 보면 매크로가 실제로 어떻게 확장되는지 체감할 수 있어요.
macro_export,macro_use,$crate의 관계를 정리해 두면 크레이트 간 매크로 공유를 설계할 때 유용해요.