[Rust] macros - declarative macros
macros - declarative macros
Rust를 처음 사용할 때 대부분 println!()을 접하게 됩니다.
println!()은 C의 ptintf() 함수처럼 사용할 수 있지만 함수가 아닙니다. 매크로 (macros)입니다.
Vet! 도 매크로입니다.
매크로는 이름 뒤에 ‘!’가 붙어서 함수와 구분됩니다.
매크로는 컴파일러가 컴파일할 때 프로그래머가 의도한 코드를 생성하게 합니다. Metaprogramming의 일종입니다.
Rust의 매크로는 크게 두 종류가 있습니다.
Declarative macros와 procedural macros.
이 글에서는 declarative macros만 다루고 procedural macros는 다음 기회에 소개하겠습니다.
다음 코드를 보면서 declarative macros를 만드는 방법을 알아보겠습니다.
// 매크로 선언
macro_rules! hello {
() => {println!("Hello!")};
}
// 매크로 실행
fn main() {
hello!();
}
매크로를 선언하고 사용할 때 가장 먼저 주의할 점이 있습니다. 매크로 실행부보다 매크로 선언부가 앞에 있어야 합니다.
위 코드에서 매크로를 선언한 코드를 main함수 아래로 옮기면 에러가 발생합니다.
declarative macros는 macro_rules! 아래에 정의한 부분을 구문 분석해서 AST(Abstract syntax tree)로 만든 후에 실행 코드로 대체합니다. 이런 과정은 Python 같은 인터프리터 언어가 작동하는 방식과 비슷합니다.
프로그래머가 작성한 코드 -> Lexer(tokenizer) -> Parser -> 실행 코드
Lexer는 코드를 의미소(token)로 분해합니다. Parser는 의미소를 문법에 따라 AST로 만듭니다. 트리의 노드에 의미소를 문법에 따라 배치한 것입니다. AST는 실행 코드로 대체됩니다.
당연하지만 우리가 declarative macros를 정의할 때는 declarative macros를 만드는 규칙에 맞게 작성해야 합니다.
매크로 선언부의 기본 골격은 다음과 같습니다.
macro_rules! 이름 {
드인수) => {구현};
}
위에서 만든 hello!()매크로에는 인수부가 없고 구현부만 있습니다.
이번에는 인수부를 첨가해 보겠습니다.
/ 매크로 정의
macro_rules! hello {
($content: expr) => {
println!("{}", $content)};
}
// 매크로 실행
fn main() {
hello!("hello there!");
}
인수부에 $content: expr가 더해졌습니다.
$은 인수를 선언하는 기호입니다.
content는 인수명입니다.
:expr는 인수의 타입니다. 여기서는 이 변수가 expression이라는 의미입니다.
공식 문서에는 인수를 Metavariables이라고 부르고 있습니다. Metavariables의 타입에는 item, block, ty, ident 같은 것이 있습니다. 이밖에도 더 많은 타입이 있습니다.
인수를 사용할 때도 앞에 $을 붙여서 사용합니다.
println!("{}", $content)
Shell script와 비슷합니다.
이번에는 인수가 둘이고 반환값도 있는 매크로를 만들어 보겠습니다.
macro_rules! add_two {
($x:expr, $y:expr) => {
$x + $y
};
}
fn main() {
println!("{}", add_two!(1,2));
}
인수부에 두 개의 인수가 있습니다.
($x:expr, $y:expr)
두 인수의 사이에 “,”로 구분한 것에 주목하십시오.
매크로의 정의는 정규식처럼 패턴이 중요합니다. 매크로는 패턴을 인식해서 실행할 때 그대로 적용합니다.
위의 인수 사이를 “,”가 아닌 “;”으로 나눠보겠습니다.
macro_rules! add_two {
($x:expr; $y:expr) => {
$x + $y
};
}
fn main() {
println!("{}", add_two!(1; 2));
}
expr 타입의 구분은 => , ; 세 가지만 가능합니다.
빈 공백도 사용 가능한 타입도 있습니다.
이번에는 길이를 특정할 수 없는 복수의 인수를 사용해 보겠습니다.
macro_rules! make_vec {
($($x:expr),*) => {
{
let mut s = vec![];
$(s.push($x * 10);)*
s
}
};
}
fn main() {
println!("{:?}", make_vec!(1,2,3));
}
//
출력
[10, 20, 30]
make_vec! 매크로는 인수에 10을 곱해서 Vec을 만들어 반환합니다.
인수부는 `($($x:expr),*)`입니다. “,”로 구분된 인수를 길이에 제한받지 않고 입력할 수 있습니다.
코드 구현부는 `$(s.push($x * 10);)`입니다.
인수부와 구현부의 패턴을 살펴보면 `$( )*` 안의 부분이 반복되는 부분임을 알 수 있습니다.
인수부는 `$x:expr),` 부분이 반복 패턴으로 인식됩니다.
구현부에서는 `s.push($x * 10);` 부분이 반복해서 실행됩니다.
인수 사이의 구분은
=>
,
;
세 가지만 가능합니다.
이 내용을 응용해서 이번에는 모든 인수를 더해서 반환하는 매크로를 만들어 보겠습니다.
macro_rules! add_all {
($($x:expr),*) => {
{
let mut s = Vec::<i32>::new();
$(s.push($x);)*
s.iter().sum::<i32>()
}
};
}
fn main() {
println!("{}", add_all!(1,2,3));
}
// 출력
6
벡터 s를 선언하고 인수를 push() 매소드로 넣어준 다음
s.iter().sum::<i32>()
으로 합을 구했습니다.
iterator를 사용한 함수형 프로그래밍은 다른 글에서 다루겠습니다.
위에서 `$( )*` 에서 `*`은 인수가 없거나 다수라는 의미입니다.
`*` 대신 `+`를 사용할 수도 있습니다. 이때는 인수가 하나이거나 그 이상이라는 의미입니다.