23.08.2021

Tässä artikkelissa on joitakin esimerkkejä Rustin makroilla tehdyistä toiminnoista. Nämä kaikki makrot ovat proseduraalisia eli ne on toteutettu Rust-ohjelmalla, joka manipuloi Rust-ohjelmaa.

safe_regex

safe_regex::regex!() kääntää säännöllisen lauseen käännösaikana tilakoneeksi (struct), jolloin käännös ei voi epäonnistua enää suorituksen aikana. Tavallisin paketti regex ei tarkista säännöllisiä lauseita käännösaikana.

recap

recap on proseduraalinen derive-makro, jolla monimutkaiselle structille voidaan luoda literaali kuvaamalla sen sisältö säännöllisellä lauseella.

#[derive(Debug, Deserialize, Recap)]
#[recap(regex = r#"(?x)
    (?P<foo>\d+)
    \s+
    (?P<bar>true|false)
    \s+
    (?P<baz>\S+)
  "#)]
struct LogEntry {
    foo: usize,
    bar: bool,
    baz: String,
}

fn main() {
  let logs = r#"1 true hello 2 false world"#;
}

Literaalin purku tehdään käännösaikana, joten operaatio ei voi enää suoritusaikana epäonnistua.

derive_more

derive_more sisältää perusimplementaatiosta puuttuvia yleiskäyttöisiä derivejä.

Esimerkiksi structeille usein määritellään new-niminen konstruktori, vaikkei Rust sitä edellytäkään monen muun kielen tapaan. Tämä voidaan tehdä tästä cratesta löytyvällä derivellä Constructor.

#[derive(Constructor)]
struct MyInts(i32, i32);

Mikä siis lisää structille tavanomaisen idiomin fn new():

impl MyInts {
    pub fn new(__0: i32, __1: i32) -> MyInts {
        MyInts(__0, __1)
    }
}

Muita derive_more:sta löytyviä derivejä ovat

metered

metered -attribuuttimakro lisää funktiolle suoritusnopeusstatistiikan keräämisen.

strum

strum tarjoaa makrot (esim. #[derive(EnumString)) siihen, että enum-tietotyypin vaihtoehdot (esim. Red, Green, Blue) ovat käytettävissä ajon aikana myös merkkijonoina (esim. "red", "green", "blue") tarvitsematta aina luetella, että Red on sama kuin "red".

Jos enumille on määritelty #[derive(EnumIter)], enumeraation kaikki variantit voidaan käydä läpi for-silmukalla for v in e.iter() {}. Tämä onnistuu vain, jos yhdelläkään variantilla ei ole vaihtuvia parametreja.

shrinkwraprs

shrinkwraprs tarjoaa makron #[derive(Shrinkwrap), jolla luodaan helposti uusi "johdettu tyyppi" (vrt. esim. "derived type" Adassa). Johdettu tyyppi toimii täsmälleen samoin kuin alkuperäinen tyyppi, mutta se ei ole yhteensopiva alkuperäisen kanssa.

Esimerkiksi tällä tavalla voidaan luoda merkkijonotyyppi sähköpostiosoite. Tällöin sitä voi käyttää kuten mitä tahansa muutakin merkkijonoa, mutta siihen ei voi edes vahingossa sijoittaa tavallista merkkijonotyyppiä olevaa arvoa ilman funktiota, joka tarkistaa, että merkkijono edustaa käypää sähköpostiosoitetta.

Shrinkwrap toimii siten, että uusi tyyppi "piilotetaan" structin sisään tehden siitä epäyhteensopiva muiden samojen tyyppien kanssa, mutta Shrinkwrap "nostaa" kaikki tämän tyypin operaatiot käytettäväksi struktille, joka ohjaa ne edelleen "piilotetulle" tyypille. Siksi nimi on käärimiseen viittaava shrinkwrap.

Rocket www-framework

Yksi Rustin WWW-sovelluspalvelin on Rocket. Rocket-sovelluksessa kerrotaan attribuuttimakroilla mitä webbiosoitetta mikäkin funktio palvelee. Nämä kerätään näkymättömissä yhteen, eikä pyyntöjen reitittäminen funktioille ole sen monimutkaisempaa.

#[get("/hello/<name>/<age>")]
fn hello(name: &str, age: u8) -> String {
    format!("Hello, {} year old named {}!", age, name)
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![hello])
}

sqlx

sqlx on liittymä tietokantoihin, jolla voi antaa suoraan SQL-kyselyjä. Normaalisti käytetään funtiota query() tietojen saamiseksi.

Toinen mahdollisuus antaa kyselyjä on makro query!. Tämä tarkistaa SQL-kyselyn oikeellisuuden jo käännösaikana suoraan tietokannan skeemasta.

let countries = sqlx::query!("
SELECT country, COUNT(*) as count
FROM users
GROUP BY country
WHERE organization = ?
        ",
        organization
    )
    .fetch_all(&pool)
    .await?;

getset

getset tarjoaa derive-makron, joka luo structin kentille getterit ja setterit. Tämä tarjoittaa sitä, että jos structissa T on kenttä x, derivellä #[derive(Getters, Setters)] voidaan luoda automaattisesti funktiot T.get_x() ja T.set_x().

Gettereitä ja settereitä tarvitaan perinnällisessä olio-ohjelmoinnissa jossa halutaan kaikkien olion operaatioiden olevan funktioita, jotka voidaan määritellä perinnässä uudelleen. Rustissa ei ole samanlaista perintää, vaan olioiden polymorfisuus hoidetaan traiteilla. On siis kyseenalaista, pitäisikö gettereitä ja settereitä käyttää Rustissa ollenkaan.

core ja std -kirjastot eivät käytä proseduraalisia makroja

Standardifunktit voisivat toteuttaa erimerkiksi print!() -makron proseduraalisena makrona. Lähdekoodia lukiessa kuitenkin selviää, etteivät core tai std -kirjastot käytä proseduraalisia makroja.

print!() -makro kyllä lavennetaan makrona aluksi, mutta kun on päästy core-kirjaston makroon format_args!(), selviää, että makron ainoa sisältö on kommentti "compiler built-in".

Tämä oli pieni pettymys, koska tätä ei mainita dokumentaatiossa.