↑Programmieren in Rust
Makros sind zur Kompilierzeit ausgeführte »Funktionen« zur Umformung von Syntax. Sie dienen dort als Hilfsmittel zur Faktorisierung wiederkehrender Muster, wo gewöhnliche Funktionen dafür nicht genügen. Charakteristisch für Makros ist, dass sie in der Programmiersprache an Oberfläche agieren, – eine tiefere Interaktion mit dem Typsystem findet nicht statt.
In Rust gibt es zwei Arten von Makros:
Dieses Kapitel beschäftigt sich lediglich mit den deklarativen Makros.
Die Definition eines deklarativen Makros hat eine ähnliche Gestalt wie das Pattern-Matching. Beim Aufruf des Makros wird zu der Liste von Syntaxmustern die Umformung zum ersten passenden Muster gewählt.
macro_rules! Name { (Muster) => {Umformung}; (Muster) => {Umformung}; ... (Muster) => {Umformung} }
Im Muster dürfen durch ein Dollar-Zeichen eingeleitete Syntaxvariablen auftreten. Die Syntaxvariable nimmt eine bestimmte Art von Syntaxfragment auf. Im Muster bekommt jede Syntaxvariable hinter einem Doppelpunkt einen Spezifikator nachgestellt, der zur Angabe der Art von Fragment dient. Die vorhandenen Spezifikatoren sind in der folgenden Tabelle aufgelistet.
Spez. | Erklärung | Beispiele |
---|---|---|
ident
| ein Bezeichner | x , bool , self
|
lifetime
| eine Lebenszeit | 'x , 'static
|
literal
| ein Literal | 24 , 'a' , "Tee"
|
ty
| ein Typ | i32 , Result<i32>
|
path
| ein Pfad | x::y::z , E::X
|
expr
| ein Ausdruck | a*x + b
|
stmt
| eine Anweisung | x = f(u) , let x = f(u)
|
pat
| ein Muster | Ok(x) , (x,y)
|
block
| ein Blockausdruck | {let x = f(u); a*x + b}
|
meta
| ein Attribut | attr , attr(x)
|
vis
| eine Sichtbarkeit | pub , pub(crate)
|
item
| ein Programmteil | fn f() {} , struct S {} , type X = Y;
|
tt
| ein Token | x , (x + y) , [1, 2] , {f(); g()}
|
Spez. | erlaubte Nachfolgetoken |
---|---|
ident
| beliebig |
lifetime
| beliebig |
literal
| beliebig |
ty
| { [ => , > = : ; | as where
|
path
| { [ => , > = : ; | as where
|
expr
| => , ;
|
stmt
| => , ;
|
pat
| => , = | if in
|
block
| beliebig |
meta
| beliebig |
vis
| , Typ Bezeichner
|
item
| beliebig |
tt
| beliebig |
Als erstes möchte ich zeigen wie Makros allerhand syntaktische Spielereien ermöglichen. Das folgende Beispiel zeigt die Erstellung von klassischer mathematischer Notation für Fallunterscheidungen, bei der die Bedingung jeweils hinter dem Wert steht.
macro_rules! case { ($($v:expr, if $cond:expr),* $(, else $other:expr)?) => { $(if $cond {$v}) else* $(else {$other})? } } fn pow(x: i32, n: u32) -> i32 { case!{ 1, if n == 0, else x*pow(x, n - 1) } }
Ähnlich wie bei Backus-Naur-Form und regulären Ausdrücken dürfen im Muster Wiederholungen und Optionen von Teilmustern vorkommen. Im Körper des Makros kommt die jeweilige Regel dann nochmals vor und dient dabei zur wiederholten bzw. optionalen Erzeugung von Syntax. Eine Wiederholung eines Musters ist als
$(Muster)*
oder $(Muster) Separator*
notiert. Ein optionales Muster ist als
$(Muster)?
notiert.
Makros können genutzt werden, um Datentypen mit Literalen auszustatten, welche zur Initialisierung der Datenstrukturen genutzt werden.
Die Standardbibliothek enthält zwar schon ein Makro für dynamische Felder, jedoch kann dies etwas umständlich sein:
let v: Vec<String> = vec![ "Ahorn".to_string(), "Eiche".to_string(), "Erle" .to_string(), "Esche".to_string() ];
Ein Literal für ein dynamisches Feld von Zeichenketten erlaubt die gewünschte Verkürzung:
macro_rules! vec_string { ( $( $x:expr ),* ) => {{ let mut _temp_vec = Vec::new(); $(_temp_vec.push($x.to_string());)* _temp_vec }} } fn main() { let v: Vec<String> = vec_string![ "Ahorn", "Eiche", "Erle", "Esche" ]; println!("{:?}", v); }
Der Unterstrich vor temp_vec
unterdrückt beim
leeren Array vec_string![]
die Meldung, dass
die Veränderbarkeit von temp_vec
überflüssig ist.
Mit den Verbesserungen geht es aber noch weiter voran. Der Ausdruck
s.to_string()
lässt sich auch ersetzen gegen
s.into()
, mit dem Vorteil typgenerisch zu sein.
Außerdem kann man das Literal auch auf vec![]
zurückführen,
welches effizienter implementiert ist. Beides kombiniert ergibt
folgendes Makro:
macro_rules! vec_from { ($($item:expr),* $(,)?) => {vec![$($item.into(),)*]} }
Literal für ein assoziatives Feld:
use std::collections::HashMap; macro_rules! map { ( $( [$key:expr]: $value:expr ),* ) => {{ let mut _temp_map = HashMap::new(); $(_temp_map.insert($key.to_string(), $value.to_string());)* _temp_map }} } fn main() { let m: HashMap<String, String> = map!{ ["Ahorn"]: "Acer", ["Eiche"]: "Quercus", ["Erle" ]: "Alnus", ["Esche"]: "Fraxinus excelsior" }; println!("{:?}", m); }
Die eckigen Klammern um die Schlüssel sind etwas umständlich.
Sie sind vorhanden weil nach $key:expr
kein Doppelpunkt
folgen darf, die erlaubten Zeichen sind "=> , ;
"
und schließende Klammern. Das expr
zeigt uns
hier, dass es sich um einen Ausdruck handelt. Tatsächlich können
wir aber mit tt
(single token tree) sagen, dass es
sich um ein Atom handeln soll, wobei auch ein geklammerter Ausdruck
als Atom zugelassen ist. Nach leichter Modifikation ergibt sich
also:
use std::collections::HashMap; macro_rules! map { ( $( $key:tt: $value:expr ),* ) => {{ let mut _temp_map = HashMap::new(); $(_temp_map.insert($key.to_string(), $value.to_string());)* _temp_map }} } fn main() { let m: HashMap<String, String> = map!{ "Ahorn": "Acer", "Eiche": "Quercus", "Erle" : "Alnus", "Esche": "Fraxinus excelsior" }; println!("{:?}", m); }
Das Makro ist bis jetzt auf den Typ
HashMap<String,String>
begrenzt.
Mit der Trait-Methode into
lässt es sich
aber auf andere Typen verallgemeinern:
use std::collections::HashMap; macro_rules! map { ( $( $key:tt: $value:expr ),* ) => {{ let mut _temp_map = HashMap::new(); $(_temp_map.insert($key.into(), $value.into());)* _temp_map }} } fn main() { let m: HashMap<String, String> = map!{ "Ahorn": "Acer", "Eiche": "Quercus", "Erle" : "Alnus", "Esche": "Fraxinus excelsior" }; println!("{:?}", m); }
Etwas allgemeiner lassen sich beliebige Transformationen
tk
und tv
anwenden:
use std::collections::HashMap; macro_rules! map { ( $tk:expr, $tv:expr; $( $key:tt: $value:expr ),*) => {{ let mut _temp_map = HashMap::new(); $(_temp_map.insert($tk($key), $tv($value));)* _temp_map }} } fn main() { let m = map!{String::from, String::from; "Ahorn": "Acer", "Eiche": "Quercus", "Erle" : "Alnus", "Esche": "Fraxinus excelsior" }; println!("{:?}", m); }
Gewöhnliche Funktionen sind mit dem Hindernis behaftet, nicht mit Kontrollfluss zu harmonisieren. Zunächst ist die Auswertung von Argumenten in Rust immer eifrig, engl. eager evaluation. D. h. die Auswertung von Argumenten findet immer statt, egal ob sie benötigt werden oder nicht. Die Nachstellung der Fallunterscheidung als Funktion
fn cond<T>(c: bool, x: T, y: T) -> T { if c {x} else {y} }
ist bspw. nicht zielführend. Hier möchte man eine Bedarfsauswertung, engl. lazy evaluation, haben. Diese ist darstellbar durch Verhüllung der Argumente durch Funktionen:
fn cond<Fx, Fy, T>(c: bool, x: Fx, y: Fy) -> T where Fx: FnOnce() -> T, Fy: FnOnce() -> T { if c {x()} else {y()} }
Eine Funktion der Standardbibliothek, wo von dieser Methodik
Gebrauch gemacht wird, ist unwrap_or_else
. Leider
verträgt sich die Konstruktion nicht mit den Sprunganweisungen
break
, continue
, return
und dem Fragezeichen-Operator. Die Formulierung als Makro
befreit uns schließlich von diesen Beschränkungen.
macro_rules! cond { ($c:expr, $x:expr) => {if $c {$x}}; ($c:expr, $x:expr, $y:expr) => {if $c {$x} else {$y}} }
Ein nützliches Beispiel ist der folgende allgemeine Erwartungsoperator zum Entpacken von Enum-Varianten.
macro_rules! expect { ($e:expr, $variant:path) => { match $e {$variant(value) => value, _ => panic!()} }; ($e:expr, $variant:path, $otherwise:expr) => { match $e {$variant(value) => value, _ => $otherwise} } }
Gegenüber den Methoden unwrap
, unwrap_or
und unwrap_or_else
hat expect
drei Vorteile:
otherwise
.
Eine extra Methode ist nicht notwendig.
For-Audrücke, auch Listen-Komprehension oder Iterator-Komprehension genannt, gibt es in Rust nicht. Da Rust aber Ausdruck-orientiert ist, lassen sich diese mithilfe von Makros selbst programmieren.
macro_rules! list { ($expr:expr; for $x:tt in $range:expr) => {{ let mut _a = Vec::new(); for $x in $range {_a.push($expr);} _a }}; } fn main() { let a = list![2*x; for x in 0..10]; println!("{:?}", a); }
Die Anzahl der notwendigen Reallokationen lässt sich durch
Ausnutzen von size_hint
erheblich reduzieren:
macro_rules! list { ($expr:expr; for $x:tt in $range:expr) => {{ let _r = $range; let mut _a = Vec::with_capacity(_r.size_hint().0); for $x in _r {_a.push($expr);} _a }}; }
Mit mehrstufigen for-Ausdrücken bekommt man kartesische Produkte als Definitionsbereich. Diese for-Ausdrücke kann man jeweils separat bis zu einer maximalen Anzahl implementieren:
macro_rules! list { ($expr:expr; for $x:tt in $range:expr) => {{ let mut _a = Vec::new(); for $x in $range {_a.push($expr);} _a }}; ($expr:expr; for $x:tt in $xrange:expr; for $y:tt in $yrange:expr ) => {{ let mut _a = Vec::new(); for $x in $xrange { for $y in $yrange {_a.push($expr);} } _a }}; } fn main() { let a = list![x*y; for x in 0..2; for y in 0..2]; println!("{:?}", a); }
Das Makro für die for-Ausdrücke lässt sich aber auch variadisch
programmieren. Zur Umsetzung ist ein Verfahren gewinnbringend,
das sich »TT muncher« nennt. Hierbei wird das zu parsende Muster
in einen Anfang und einen Rest $tail
zerlegt.
Mit einem weiteren Makro-Aufruf geschieht dann das Parsen von
$tail
, was sich auch rekursiv formulieren lässt.
macro_rules! expand_tail { ($expr:tt; $v:tt; for $x:tt in $range:expr) => { for $x in $range {$v.push($expr);} }; ($expr:tt; $v:tt; for $x:tt in $range:expr; $($tail:tt)*) => { for $x in $range {expand_tail!($expr; $v; $($tail)*)} }; } macro_rules! list { ($expr:expr; $($tail:tt)*) => {{ let mut _a = Vec::new(); expand_tail!{$expr; _a; $($tail)*} _a }}; } fn main() { let a = list![[x, y, z]; for x in 0..2; for y in 0..2; for z in 0..2]; println!("{:?}", a); }
Bei for-Ausdrücken sind auch Bedingungen erlaubt, mit denen sich Elemente ausfiltern lassen. Diese können wir nun auch noch einbauen. Als einfaches Beispiel ist die Auflistung von pythagoräischen Tripeln angegeben.
macro_rules! expand_tail { ($expr:tt; $v:tt; for $x:tt in $range:expr) => { for $x in $range {$v.push($expr);} }; ($expr:tt; $v:tt; for $x:tt in $range:expr; if $cond:expr) => { for $x in $range {if $cond {$v.push($expr);}} }; ($expr:tt; $v:tt; for $x:tt in $range:expr; if $cond:expr; $($tail:tt)* ) => { for $x in $range { if $cond {expand_tail!($expr; $v; $($tail)*)} } }; ($expr:tt; $v:tt; for $x:tt in $range:expr; $($tail:tt)*) => { for $x in $range {expand_tail!($expr; $v; $($tail)*)} }; } macro_rules! list { ($expr:expr; $($tail:tt)*) => {{ let mut _a = Vec::new(); expand_tail!{$expr; _a; $($tail)*} _a }}; } fn main() { let a = list![[x, y, z]; for x in 1..100; for y in 1..100; for z in 1..100; if x < y && x*x + y*y == z*z]; println!("{:?}", a); }