Ferris Talk #10: Constant Fun mit Rust – const fn

In Version 1.61 hat Rust Tricks in Sachen konstanter Code-Evaluierung gelernt. Diese Ausgabe der Kolumne zeigt, was const in Rust kann und was es Neues gibt.

In Pocket speichern vorlesen Druckansicht
Ferris Talk – Neuigkeiten zu Rust. Eine Heise-Kolumne von Rainer Stropek und Stefan Baumgartner für Rustaceans
Lesezeit: 9 Min.
Von
  • Rainer Stropek
Inhaltsverzeichnis

Egal, von welcher Programmiersprache man kommt, Konstanten gibt es praktisch überall. Rust wäre nicht seit Jahren eine der beliebtesten Programmiersprachen laut Stack Overflow Survey, wenn sie nicht auch im Bereich der Konstanten etwas zu bieten hätte, was man in vielen anderen Sprachen nicht findet. In der kürzlich erschienenen Version 1.61 hat Rust einige Tricks in Sachen Konstanten und konstanter Evaluierung von Code dazugelernt.

Die in nächster Zeit geplanten Versionen werden laut aktuell verfügbarer Nightly weitere Neuerungen in dem Bereich bringen. Das nehmen wir in der Rust-Kolumne zum Anlass, in dieser Ausgabe des Ferris Talk const und const fn in Rust vorzustellen und anhand von Beispielen zu zeigen, was es Neues gibt.

Ferris Talks – die Kolumne für Rustaceans

In diesem Artikel stehen die Codebeispiele im Mittelpunkt. Damit Interessierte den gezeigten Code ausprobieren und vor allem damit experimentieren können, ist bei jedem Beispiel ein Link angegeben, der den Code im Rust Playground öffnet. Dort lässt er sich ausführen und ändern, selbst wenn Rust noch nicht lokal installiert sein sollte.

Starten wir mit den Grundlagen, die in Rust ähnlich funktionieren wie in anderen Programmiersprachen. Folgender Code legt Konstanten mit verschiedenen Typen an (siehe Rust Playground, Sample 1). Die Zuweisung des Wertes zur Konstanten ANSWER ist besonders zu beachten. Der Wert lässt sich durch einen konstanten Ausdruck ermitteln. In einem solchen Kontext steht nicht alles zur Verfügung, was Rust zur Laufzeit an Ausdrücken beherrscht. Die konkreten Einschränkungen sind in der Rust-Dokumentation beschrieben.

// A simple constant value
const NEARLY_THE_ANSWER: i32 = 41;

// A constant expression that is evaluated at compile time.
// Note: Not everything is allowed in a constant expression. See
// https://doc.rust-lang.org/reference/const_eval.html#constant-expressions
// for details
const ANSWER: i32 = NEARLY_THE_ANSWER + 1;

// A simple constant string (because of static lifetime elision,
// we don't need 'static)
const TEXT: &str = "the quick brown fox jumps over the lazy dog!";

// A constant array
const NUMBERS: [i32; 5] = [1, 2, 3, 4, 5];

fn main() {
    // Constants are inlined at compile time wherever they are used.
    println!("The answer is {ANSWER}");
    println!("{TEXT}");
    println!("Numbers: {:?}", NUMBERS);

    // Constants can be declared in any scope, not just global.
    const VERSION: &str = "1.2.3";
    println!("Version: {}", VERSION);
}

Konstanten in Rust sind nicht auf Basisdatentypen wie Zahlen oder Zeichenketten beschränkt. Auch Strukturen können const sein, wie das folgende Codebeispiel zeigt (siehe auch Rust Playground, Sample 2).

// Constants in Rust don't need to be basic data types.
// Structs can also be constant.
#[derive(Debug)]
struct Customer<'a> {
    name: &'a str,
    age: i32,
}
const CUSTOMER: Customer = Customer {
    name: "John",
    age: 42,
};

fn main() {
    println!(
        "Customer {} is of age {} ({:?})",
        CUSTOMER.name, CUSTOMER.age, CUSTOMER
    );

    // Note that if you modify a const item, a new temporary
    // item is created. The original const item is not modified.
    CUSTOMER.age += 1;
}

In Rust können Konstanten auf die Adressen anderer Konstanten verweisen. Das folgende Beispiel demonstriert das, indem es eine Struktur NamedNumbers definiert und im Rahmen einer Konstante verwendet (siehe Rust Playground, Sample 3). Die Konstante verweist auf andere Konstanten. Zu erwähnen ist dabei, dass beim Verwenden der Struktur im Rahmen der Konstantendeklaration die static Lifetime nicht explizit anzugeben ist, Rust ergänzt sie im Hintergrund automatisch.

const TEXT: &str = "the quick brown fox jumps over the lazy dog!";
const NUMBERS: [i32; 5] = [1, 2, 3, 4, 5];

// A constant struct.
// Note that constants may refer to the address of other constants.
// The lifetime defaults to 'static (elided).
struct NamedNumbers<'a> {
    name: &'a str,
    numbers: &'a [i32; 5],
}
const NAMED_NUMBERS: NamedNumbers = NamedNumbers {
    name: TEXT,
    numbers: &NUMBERS,
};

fn main() {
    println!("{TEXT:p}\n{:p}", NAMED_NUMBERS.name);
    println!("{}", NAMED_NUMBERS.numbers.iter().sum::<i32>());
}

Die nächste Besonderheit von Rust ist, dass Konstanten mit Typen assoziiert werden und sogar Teil eines Traits sein können. Im folgenden Beispiel verlangt der Trait HasNumbers, dass zwei Konstanten existieren (siehe Rust Playground, Sample 4). Für eine davon ist ein überschreibbarer Standardwert vorzugeben.

Empfohlener redaktioneller Inhalt

Mit Ihrer Zustimmmung wird hier ein externes YouTube-Video (Google Ireland Limited) geladen.

Ich bin damit einverstanden, dass mir externe Inhalte angezeigt werden. Damit können personenbezogene Daten an Drittplattformen (Google Ireland Limited) übermittelt werden. Mehr dazu in unserer Datenschutzerklärung.

// Constants in traits
trait HasNumbers {
    // Note that we have a constant here without a value.
    const NUMBERS: [i32; 5];

    // Constants in traits can have default values
    const LAST_NUMBER: i32 = 5;
}
struct IHaveNumbers {}
impl HasNumbers for IHaveNumbers {
    // This is an *associated constant*, as it is associated with a type.
    const NUMBERS: [i32; 5] = [1, 2, 3, 4, IHaveNumbers::LAST_NUMBER];
}
struct IHaveOtherNumbers {}
impl HasNumbers for IHaveOtherNumbers {
    // Here we override the default value of the trait.
    const LAST_NUMBER: i32 = 6;
    const NUMBERS: [i32; 5] = [1, 2, 3, 4, IHaveOtherNumbers::LAST_NUMBER];
}

fn main() {
    println!("{:?}", IHaveNumbers::NUMBERS);
    println!("{:?}", IHaveOtherNumbers::NUMBERS);
}

Jetzt verlassen wir denjenigen Bereich der Konstanten, der in anderen Programmiersprachen ähnlich ist, und sprechen über weiterführende Fähigkeiten von Rust. Als Erstes geht es um konstante Ausdrücke mit Destruktoren. Ja, richtig gehört: Konstante Ausdrücken können in Rust Destruktoren haben, die an der entsprechenden Stelle im Programm ausgeführt werden, wenn die Konstante verwendet wird. Etwas anders ist das bei statischen Variablen. Ihr Destruktor wird nicht aufgerufen, wenn das Programm bzw. der Thread beendet wird. Das folgende Listing (siehe auch Rust Playground, Sample 5) demonstriert dieses Verhalten.

// Constants can have destructors
struct WillSayGoodbye<'a>(&'a str);
impl<'a> Drop for WillSayGoodbye<'a> {
    fn drop(&mut self) {
        println!("{}", self.0);
    }
}
// Note that destructor on statics will not run on program/thread exit.
static _GOODBYE_IN_ENGLISH: WillSayGoodbye = WillSayGoodbye("Goodbye");
const GOODBYE_IN_GERMAN: WillSayGoodbye = WillSayGoodbye("Auf Wiedersehen");

fn main() {
    {
        let _goodbye_sayer = GOODBYE_IN_GERMAN;
        // Destructor will run at appropriate point when const is used. Therefore,
        // this code will print "Auf Wiedersehen" on stdout when var goes out of scope.    }
}

Neben den üblichen, konstanten Werten erlaubt Rust mit const auch das Ausführen von Code beim Kompilieren. Das folgende Codebeispiel dient als Einstieg. Das Sample 6 aus dem Rust Playground ist in mehrerlei Hinsicht interessant.

Zunächst handelt es sich um eine anonyme Konstante, die also keinen Namen hat. Manch einer mag sich jetzt fragen, ob eine Konstante sinnvoll ist, die sich ohne Namen nirgends referenzieren lässt. Bevor das Sinn ergibt, wird es noch eigenartiger: Die Konstante ist vom Typ Unit, hat also nur einen möglichen Wert: ().

Sinnvoll wird das, indem ein Codeblock den Unit-Wert erzeugt. Durch const findet das Ausführen des Codes jedoch nicht zur Laufzeit statt, sondern Rust führt ihn zur Übersetzungszeit aus. Das folgende Listing enthält Code, der prüft, ob die Struktur IHaveNumbers den Trait HasNumbers implementiert.

Das Konzept, Code auszuführen, um den Wert einer Konstante zu ermitteln, heißt in Rust Constant Evaluation. Für Codeblöcke, die sich konstant evaluieren lassen, gibt es Einschränkungen, und nicht jeder Rust-Code ist in diesem Kontext erlaubt. Auf einige dieser Einschränkungen gehen spätere Beispiele noch ein.

trait HasNumbers {
    const NUMBERS: [i32; 5];
    const LAST_NUMBER: i32 = 5;
}
struct IHaveNumbers {}
impl HasNumbers for IHaveNumbers {
    const NUMBERS: [i32; 5] = [1, 2, 3, 4, IHaveNumbers::LAST_NUMBER];
}

// Constants do not need to have a name. They can be unnamed.
// This is useful for running code at compile time.
const _: () = {
    // Code that makes sure that IHaveNumbers implements HasNumbers
    use std::marker::PhantomData;
    struct ImplementsMyTrait<T: HasNumbers>(PhantomData<T>);
    let _ = ImplementsMyTrait(PhantomData::<IHaveNumbers>);
};

fn main() {
}

Konstant evaluierte Codeblöcke können natürlich nicht nur Unit zurückgeben. Sie können jeden beliebigen Wert zur Übersetzungszeit berechnen, um ihn einer Konstanten oder einer statischen Variablen zuzuweisen. Es lohnt sich, diese Möglichkeit anhand eines weiteren Codebeispiels genauer anzusehen (beispielsweise im Rust Playground, Sample 7).

Die Funktion five_numbers ist eine const fn. Sie ermittelt die Zahlen 1 bis 5 mithilfe einer Schleife. Im Beispiel sollte das Augenmerk auf den Kommentaren im Code liegen, die auf Einschränkungen bei konstanten Funktionen in Rust hinweisen.

Der Compiler ermittelt die Werte beim Übersetzen. Das lässt sich überprüfen, indem man sich im Rust-Playground den generierten Assembler-Code ansieht (siehe weiterhin Sample 7 im Rust Playground). Dort steht unter anderem das generierte Array mit fünf Integer-Werten bereit.

In der main-Methode sieht man die Aufrufe der five_numbers-Funktion. Einmal findet der Aufruf zur Übersetzungszeit statt (zum Ermitteln des Wertes einer Konstante) und einmal zur Laufzeit (zum Bestimmen des Wertes einer Variablen).

Die Methode numbers ist eine Verallgemeinerung der zuvor erwähnten konstanten Funktion. Sie kann eine beliebige Anzahl an Werten erzeugen. Die gewünschte Anzahl wird in dieser Umsetzung als const generic-Parameter an die Methode übergeben.

// Functions can be constant to be usable
// in const and static scenarios.
// Note that some limitations apply (see examples below).
const fn five_numbers() -> [i32; 5] {
    let mut numbers = [0i32; 5];

    // Note that for loops can not (yet) be used
    // in constant functions.
    // So we have to go for a while loop.
    let mut i = 0;
    while i < 5 {
        numbers[i] = i as i32 + 1;
        i += 1;
    }

    numbers
}

// Const functions can be generic and can also receive const
// generic parameters.
const fn numbers<const N: usize>() -> [i32; N] {
    let mut numbers = [0i32; N];

    let mut i = 0;
    while i < N {
        numbers[i] = i as i32 + 1;
        i += 1;
    }

    numbers
}

fn main() {
    const FIVE_NUMBERS: [i32; 5] = five_numbers();
    println!("{:?}", FIVE_NUMBERS);

    const TEN_NUMBERS: [i32; 10] = numbers();
    println!("{:?}", TEN_NUMBERS);

    // Const functions can also be called at runtime.
    // They are not limited to constants or static variables.
    let five_numbers = five_numbers();
    println!("{:?}", five_numbers);
}

Die zuvor gezeigten Beispiele haben die Grundlagen der konstanten Evaluierung in Rust veranschaulicht. Im folgenden Codebeispiel geht es an die Umsetzung eines etwas größeren, praxisnahen Anwendungsfalls (siehe Rust Playground, Sample 8). Mehrere konstante Strings sind zur Übersetzungszeit zu verketten. Rust verfügt zwar über ein eingebautes concat!-Makro, allerdings akzeptiert es nur Literale. Diese Einschränkung ist in der Umsetzung unerwünscht. Der Beispielcode ist in folgende Teile gegliedert:

  • len kann zur Übersetzungszeit die Gesamtlänge aller Strings in einem Slice ermitteln.
  • concat allokiert beim Kompilieren einen ausreichend großen Puffer und kopiert einen String nach dem anderen hinein.
  • Das Makro my_concat! vereinfacht den Einsatz von len und concat.
  • Die main-Methode zeigt, wie sich die konstanten Funktionen und das Makro aufrufen lassen.
// Slightly more complex samples for concatenation
// of strings in const fn.
// Inspired by crates like
// https://docs.rs/const-str/latest/const_str/.

/// Gets total length of all provided strings
const fn len(strs: &[&str]) -> usize {
    let mut result = 0;
    let mut remaining = strs;

    // Note that we cannot use iter() here because
    // it is not a constant expression. Therefore, we
    // have to iterate using slice deconstruction.
    while let [current, tail @ ..] = remaining {
        result += current.len();
        remaining = tail;
    }

    result
}

/// Helper struct for concatenation of strings in const fn.
struct Buf<const N: usize>([u8; N]);

/// Concatenates all provided strings into a single string *at compile time*.
const fn concat<const N: usize>(strs: &[&str]) -> Buf<N> {
    let mut buffer = [0; N];
    let mut position_in_buffer = 0;

    let mut remaining = strs;
    while let [current, tail @ ..] = remaining {
        let x = current.as_bytes();
        let mut i = 0;

        // Note that we cannot use
        // copy_from_slice because mutable
        // references are not allowed in const functions.
        // buffer.copy_from_slice(x);

        // Note that for loop is not (yet) allowed
        // in const functions.
        // We have to use while instead.
        while i < x.len() {
            buffer[position_in_buffer] = x[i];
            position_in_buffer += 1;
            i += 1;
        }

        remaining = tail;
    }

    Buf(buffer)
}

macro_rules! my_concat {
    ($($x: expr),+ $(,)?) => {{
        const STRS: &[&str] = &[$($x),+];
        const LEN: usize = len(STRS);
        const CONCAT_BUF: Buf<LEN> = concat(STRS);
        unsafe { core::str::from_utf8_unchecked(&CONCAT_BUF.0) }
    }}
}

fn main() {
    // Define constant strings and concat them at compile time
    const STRS: &[&str] = &["Hello", " ", "World!"];
    const LEN: usize = len(STRS);
    const CONCAT_BUF: Buf<LEN> = concat(STRS);
    const GREETING: &str = unsafe { core::str::from_utf8_unchecked(&CONCAT_BUF.0) };
    println!("{}", GREETING);

    // Const functions can be called at runtime, too
    let strs: &[&str] = &["Hello", " ", "World!"];
    let concat_buf: Buf<LEN> = concat(strs);
    let greeting: &str = unsafe { core::str::from_utf8_unchecked(&concat_buf.0) };
    println!("{}", greeting);


    const CAT: &str = "CAT";
    const MOUSE: &str = "MOUSE";

    // Rust's built-in concat! macro only accepts literals.
    // We cannot concat two string constants.
    //const GREETING3: &str = concat!(CAT, " LOVES ", MOUSE);

    // Our macro makes it easy to call the concat const fns
    // we defined before. It is not limited to literals.
    const GREETING2: &str = my_concat!(CAT, " LOVES ", MOUSE);

    println!("{}", GREETING2);
}

Das im Mai veröffentlichte Rust 1.61 bringt in Sachen const fn wichtige Neuerungen. Einige davon werden wir im Folgenden an Beispielen zeigen. Eine komplette Auflistung ist in der englischsprachigen Ankündigung der Version 1.61 im Rust-Blog zu finden.

Das erste Beispiel zum Thema Neuerungen zeigt, dass Rust jetzt bei konstanten Funktionen Trait Bounds beherrscht. Im Code verlangt die Funktion nth die Übergabe eines Array, dessen Elemente den Copy-Trait implementieren (siehe Rust Playground, Sample 9).

#![allow(dead_code)]

// Constants in Rust do not need to be basic data types.
// Structs can also be constant.
#[derive(Debug, Clone, Copy)]
struct Customer<'a> {
    name: &'a str,
    age: i32,
}
const CUSTOMER: Customer = Customer {
    name: "John",
    age: 42,
};

// Brand new (1.61, stable): Trait bounds on generic parameters for
// const functions are now supported.
const fn nth<T: Copy, const N: usize>(items: [T; N], index: usize) -> T {
    items[index]
}

fn main() {
    const CUSTOMERS: [Customer; 2] = [
        Customer {
            name: "John",
            age: 30,
        },
        Customer {
            name: "Jane",
            age: 25,
        },
    ];
    const NTH_CUSTOMER: Customer = nth(CUSTOMERS, 1);
    println!("{:?}", NTH_CUSTOMER);
}

Das zweite Beispiel zeigt, dass konstante Funktionen in Rust neuerdings impl Traits und dyn Traits unterstützen (auch zu finden im Rust Playground, Sample 10).

trait Animal {
    fn make_sound<'a>(&self) -> &'a str;
}
struct Cat {}
struct Dog {}
impl Animal for Cat {
    fn make_sound<'a>(&self) -> &'a str {
        "meow"
    }
}
impl Animal for Dog {
    fn make_sound<'a>(&self) -> &'a str {
        "woof"
    }
}

// Brand new (1.61): Arguments and return values for const fns
// now support impl trait.
const fn favorite_animal() -> impl Animal {
    Cat {}
}

// Brand new (1.61): Arguments and return values for const fns
// now support dyn trait.
const fn animal_by_sound<'a>(can_purr: bool) -> &'a dyn Animal {
    match can_purr {
        true => &Cat {},
        false => &Dog {},
    }
}

const fn _can_purr(_animal: &dyn Animal) -> Result<bool, &str> {
    // Note that the following if statement
    // is not possible in const fn
    // because calls to trait methods are not allowed.
    // if (animal.make_sound() == "meow") {
    //     return Ok(true);
    // }

    Err("Sorry, cannot find that out")
}

fn main() {
    const FAVORITE_ANIMAL: &dyn Animal = &favorite_animal();
    println!("My favorite animal makes {}", FAVORITE_ANIMAL.make_sound());

    const PURRING_ANIMAL: &dyn Animal = animal_by_sound(true);
    println!("Animals that can purr say {}", PURRING_ANIMAL.make_sound());
}

Die Arbeit, die das Core-Team von Rust und insgesamt über 300 Beitragende unter anderem von Unis und großen Tech-Konzernen in das Entwickeln neuer Funktionsweisen konstanter Funktionen in Rust investieren, ermöglicht es, in der Standardbibliothek mehr auf const fn umzustellen. Das letzte Beispiel dieser Ausgabe der Kolumne zeigt das am Beispiel des Typs std::sync::Mutex (siehe Rust Playground, Sample 11).

Wer in der stabilen Rust-Version 1.61 einen Mutex statisch deklarieren möchte, braucht dafür Tricks (Static Mutex – mehr dazu auf Stack Overflow in der Diskussion zur Frage: "How do I create a global, mutable Singleton?"). Die Nightly-Version von Rust ändert das. Mutex::new wurde zur const fn und man kann daher Zusätze wie lazy_static zur Initialisierung einer statischen Variable verwenden.

// Brand new (1.63): Mutex::new and RwLock::new are const functions.
// No need for lazy_static & friends anymore (see also
// https://stackoverflow.com/a/27826181/3548127).
static ARRAY: std::sync::Mutex<Vec<u8>> = std::sync::Mutex::new(vec![]);

fn main() {
    {
        let mut arr = ARRAY.lock().unwrap();
        for _ in 0..10 {
            arr.push(1);
        }
    }
    println!("Called push {} times", ARRAY.lock().unwrap().len());
}

Konstanten in Rust überraschen mit einer Vielzahl an Möglichkeiten, die man in zahlreichen anderen Programmiersprachen nicht findet. Neben den üblichen konstanten Werten erlaubt const fn in Rust die Ausführung von Code zur Übersetzungszeit. Diese Ausgabe der Rust-Kolumne hat die Sprachfunktion anhand vieler Beispiele gezeigt.

Derartiger Code schränkt das Entwickeln mit Rust funktional allerdings etwas ein. Nicht alles, was Rust zur Laufzeit erlaubt, dürfen Entwicklungsteams in konstant evaluierten Funktionen auch tatsächlich machen. Die dargestellten Listings zeigen exemplarisch solche Einschränkungen.

Die aktuelle Rust-Version 1.61 beseitigt einige wesentliche Einschränkungen. const fn beherrscht jetzt unter anderen Trait Bounds, impl und dyn Trait Types. Damit eröffnet sie für kommende Rust-Versionen neue Einsatzbereiche von const und const fn im eigenen Code und in der Rust-Standardbibliothek. Mehr Funktionen lassen sich in Zukunft als const fn umsetzen und dadurch im Kontext von Konstanten und statischen Variablen einsetzen. Der Rust-Code wird dadurch einfacher und effizienter.

Ferris Talk – Neuigkeiten zu Rust. Kolumnist:
Rainer Stropek, timecockpit.com, Rust Meetup Linz, Autor der Ferris Talks, der Kolumne über die Programmiersprache Rust bei Heise Developer

Rainer Stropek

ist Softwareentwickler, Trainer, Autor und Vortragender im Microsoft-Umfeld und seit über 25 Jahren als Unternehmer in der IT-Industrie tätig.

Er gründete und führte in dieser Zeit mehrere IT-Dienstleistungsunternehmen. Neben der Tätigkeit als Trainer und Berater in seiner Firma software architects entwickelt er mit seinem Team die preisgekrönte Software time cockpit. Rainer hat Abschlüsse der höheren technischen Schule für Informatik Leonding (AT) sowie der University of Derby (UK). Er ist Autor mehrerer Fachbücher und Artikel in Magazinen im Umfeld von Microsoft .NET und C#, Azure, Go und Rust. Seine technischen Schwerpunkte sind Cloud Computing, die Entwicklung verteilter Systeme sowie Datenbanksysteme.

Regelmäßig tritt er als Speaker und Trainer auf namhaften Konferenzen in Europa und den USA auf. 2010 wurde er von Microsoft zu einem der ersten MVPs (Most Valuable Professionals) für die Azure-Plattform ernannt. Seit 2015 ist er Microsoft Regional Director. 2016 hat er zudem den MVP Award für Visual Studio und Developer Technologies erhalten.

(sih)