Programmieren in Rust

Makros

Inhaltsverzeichnis

  1. Was sind Markos?
  2. Makrodefinition
  3. Alternative Syntax
  4. Literale
  5. Kontrollfluss-Operatoren
  6. Komprehensionen
  7. TT muncher

Was sind Makros?

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:

1. Deklarative Makros
Diese wandeln Syntax über einen Musterabgleich in andere Syntax um.
2. Prozedurale Makros
Diese dürfen eigene Parser und Syntax-Erzeuger enthalten.

Dieses Kapitel beschäftigt sich lediglich mit den deklarativen Makros.

Makrodefinition

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

Alternative Syntax

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.

Literale

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);
}

Kontrollfluss-Operatoren

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:

  1. Es funktioniert bezüglich beliebigen Enum-Varianten. Als Funktion lässt sich dies nicht ausdrücken, da im Pattern-Matching keine Varianten-Variablen gestattet sind.
  2. Natürliche Bedarfsauswertung von otherwise. Eine extra Methode ist nicht notwendig.
  3. Harmonie mit Sprunganweisungen und dem Fragezeichen-Operator.

Komprehensionen

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);
}

TT muncher

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);
}