Programmieren in Rust

Module

Inhaltsverzeichnis

  1. Module
  2. Reexport
  3. Bibliotheken
  4. Konstanten
  5. Binärdaten einbetten
  6. Zeichenketten einbetten
  7. Quelltext einbinden
  8. Zyklische Definitionen

Module

Größere Programme werden durch Strukturierung leichter verständlich. Auf der untersten Ebene benutzt man dazu Kontrollstrukturen anstelle von Sprunganweisungen. Dann kommen Funktionen, die Vorgänge abstrahieren und über ihre Argumente und Rückgabewerte Schnittstellen liefern. Auf der nächsten Ebene bündelt man zusammengehörige Funktionen und Datentypen in Module. Schnittstellen entstehen hierbei, indem man bestimmte Funktionen und Datentypen als öffentlich kennzeichnet.

Ein Modul wird mit dem Schlüsselwort mod eingeleitet. Der Inhalt des Moduls befindet sich entweder in geschweiften Klammern, oder bei längeren Modulen in einer extra Datei. Funktionen, Datentypen, Konstanten und Variablen sind nur dann öffentlich, wenn sie mit dem Schlüsselwort pub markiert wurden.

Die Grundstruktur von Modulen sei hier an der Bestimmung der Parität verdeutlicht:

mod parity {
    pub fn even(x: i32) -> bool {x%2 == 0}
}

use parity::even;

fn main() {
    println!("{}", even(0));
}

Die use-Anweisung macht lediglich deutlich, welchem Modul die Funktion even entstammen soll. Es könnte ja mehrere Module mit einer so benannten Funktion geben. Zudem gibt die use-Anweisung darüber Aufschluss, welche Abhängigkeiten ein Modul besitzt.

Möchte man sich unmissverständlich ausdrücken, kann zudem klargestellt werden, aus welchem Crate das Modul entstammen soll. Mit dem Schlüsselwort crate bezieht man sich hierbei auf das gegenwärtige Crate. So kann

use parity::even;

als Kurzform von

use crate::parity::even;

gelesen werden.

Alternativ zur use-Anweisung lässt sich der Aufruf als parity::even(0) oder in aller Ausführlichkeit als crate::parity::even(0) qualifizieren.

Das Crate std bezeichnet die Standardbibliothek. Mit std::collections::Vec ist also der Typ Vec aus dem in der Standardbibliothek befindlichen Modul collections gemeint. Ich möchte anmerken, dass die Standardbibliothek nicht fest mit Rust verdrahtet ist, dass man sie also gegen eine alternative Standardbibliothek austauschen könnte, was für eine Systemprogrammiersprache unter manchen Umständen vorteilhaft sein kann. Des Weiteren ist die Standardbibliothek kein monolitischer Block, sondern baut selbst auf den Crates core und alloc auf.

Reexport

Ein Modul kann seinerseits Untermodule enthalten. Mittels pub use lässt sich Funktionalität aus einem Untermodul als öffentliche Schnittstelle weitergeben. Das heißt, die Funktionalität erscheint von Außen betrachtet so, als wäre sie im Modul selbst definiert.

mod parity {
    mod even {
        pub fn even(x: i32) -> bool {x%2 == 0}
    }
    mod odd {
        pub fn odd(x: i32) -> bool {x%2 == 1}
    }
    pub use {even::even, odd::odd};
}

Über die Existenz der Untermodule ist von Außen betrachtet quasi nichts bekannt. Wollte man sie zugänglich machen, müsste man pub mod schreiben.

Woher die reexportierte Funktionalität entstammt, ist beliebig. Beispielsweise hindert einen nichts daran, die verschachtelte Struktur aufzulösen:

mod even {
    pub fn even(x: i32) -> bool {x%2 == 0}
}
mod odd {
    pub fn odd(x: i32) -> bool {x%2 == 1}
}

mod parity {
    pub use crate::{even::even, odd::odd};
}

Bibliotheken

Auf der nächsthöheren Ebene kann man auch wieder zusammengehörige Module bündeln. Man spricht dann von einer Bibliothek, die in Rust kurz als Crate bezeichnet wird. Eine Bibliothek muss aber nicht unbedingt in Module aufgeteilt sein.

Die Datei lib.rs bildet die Wurzel einer Bibliothek. Alle ihre öffentlichen Schnittstellen werden wie bei einem Modul mit pub gekennzeichnet. Das gilt gleichermaßen für ihre öffentlichen Module, die also als pub mod erscheinen müssen.

Der Befehl

cargo new --lib lib0

erzeugt die neue Bibliothek lib0 mit der Verzeichnisstruktur:

lib0/
└─ src/
   └─ lib.rs

Eigentlich umfasst der Begriff Crate auch ausführbare Programme. Ein ausführbares Programm unterscheidet sich von einer Bibliothek lediglich dadurch, dass die Wurzel durch die Datei main.rs anstelle von lib.rs gebildet wird.

Innerhalb einer Bibliothek darf es zyklische Definitionen geben. Zwischen Bibliotheken sind solchen Verwebungen nicht erlaubt.

Konstanten

Das folgende Programm arbeitet zwar korrekt, ist jedoch in schlechtem Stil geschrieben.

fn is_ascii_alphabetic(c: char) -> bool {
    let x = u32::from(c);
    65 <= x && x <= 90 || 97 <= x && x <= 122
}

fn main() {
    let a: Vec<&str> = "Ein Schiff taucht im Nebel auf."
        .split(|x| !is_ascii_alphabetic(x)).collect();
    println!("{:?}", a);
}

Im Programm tauchen »magische« Konstanten mit irgendeiner bestimmten Bedeutung auf, diese ist jedoch nicht klar erkennbar. Der Leser muss sich die Bedeutung der Konstanten erst erschließen. Hier mag das wohl einfach sein. Bei komplizierten Programmen steht man da aber schnell im Regen. Ganz kritisch ist die Situation wo dieselbe Konstante mehrmals im Programm vorkommt. Würde jemand die Konstante an einer Stelle zur Anpassung verändern, blieben die anderen Stellen wahrscheinlich übersehen, es kommt zu einem logischen Fehler.

Für besseres Verständnis, Abstraktion und Faktorisierung lassen sich globale oder auch lokale Konstanten mit dem Schlüsselwort const definieren. Konstanten in einem Modul sind privat. Öffentlich sind sie nur dann, wenn sie mit pub markiert wurden. Das verbesserte Programm ist hoffentlich angenehmer zu lesen:

mod charsets {
    const A: u32 = 'a' as u32;
    const Z: u32 = 'z' as u32;
    const CAPA: u32 = 'A' as u32;
    const CAPZ: u32 = 'Z' as u32;

    pub fn is_ascii_alphabetic(c: char) -> bool {
        let x = u32::from(c);
        CAPA <= x && x <= CAPZ || A <= x && x <= Z
    }
}

use charsets::is_ascii_alphabetic;

fn main() {
    let a: Vec<&str> = "Ein Schiff taucht im Nebel auf."
        .split(|x| !is_ascii_alphabetic(x)).collect();
    println!("{:?}", a);
}

Unter normalen Umständen werden Konstanten in großen Buchstaben geschrieben, das wird auch vom Compiler überprüft. Empfindet man das in einer bestimmten Situation zu unschön, lässt es sich auch ausschalten. Man muss dafür die Zeile

#[allow(non_upper_case_globals)]

vor die Definition der Konstante schreiben, oder wenn es dutzende solche Konstanten gibt,

#![allow(non_upper_case_globals)]

am Anfang des Moduls.

Konstante Zeichenketten werden normalerweise mit dem Schlüsselwort static definiert, nicht mit const:

static TEXT: &str = "Ein Schiff taucht im Nebel auf.";

Binärdaten einbetten

Manchmal möchte man Binärdaten nicht zur Laufzeit aus einer Datei laden, sonern bereits zur Kompilierzeit in die ausführbare Datei einbetten. Handelt es sich lediglich um ein paar wenige Bytes, kann man die Daten wie in

static DATA: &[u8] = &[0x45, 0x75, 0x6c, 0x65];

direkt als Literal angeben. Für große Datenmengen steht stattdessen das Makro include_bytes zur Verfügung, das die Daten zur Kompilierzeit aus der angegebenen Datei lädt:

static DATA: &[u8] = include_bytes!("data.bin");

Zeichenketten einbetten

Weiterhin existiert auch ein Makro inlude_str zur Einbettung von UTF-8-kodierten Dateien als Zeichenketten. Man kann sich damit unter anderem ein Quine ermogeln. Unter einem Quine wird ein Programm verstanden, das seinen eigenen Quelltext repliziert. Mit include_str geht das ganz einfach:

static SELF: &str = include_str!("main.rs");

fn main() {
    println!("{}", SELF);
}

Eine sinnvollere Idee ist meiner Ansicht nach die nummerierte Ausgabe des eigenen Quelltextes, die man mit

for (i, line) in SELF.lines().enumerate() {
    println!("{:02} {}", i + 1, line);
}

bewerkstelligen kann.

Quelltext einbinden

Das Makro include bietet die Möglichkeit, einen Quelltext-Abschnitt eines Moduls in eine andere Datei auszulagern. Wer dazu geneigt ist, ein übermäßig langes Modul zu schreiben, kann dieses damit trotzdem übersichtlich zu Dateien auftrennen. Es sei aber gesagt, dass dieser Weg eigentlich nicht beschritten werden muss, da das Modulsystem ebenfalls die notwendigen Fähigkeiten dafür besitzt. Es ist ja mit dem qualifizierten Import ausgestattet und enthält mit dem öffentlichen Import einen Reexport.

In gewisser Hinsicht darf man

mod m;

als Kurzschreibweise für

mod m {include!("m.rs");}

betrachten. Allerdings besteht zwischen beidem in der näheren Betrachtung ein Unterschied. Das Modul m enthalte beispielsweise ein Untermodul m0 und das Verzeichnis haben die folgende Struktur:

src/
├─ main.rs
├─ m/
│  └─ m0.rs
└─ m.rs

Einbindung vermittels include macht dann die explizite Pfadangabe erforderlich, weil die Umstellung auf das Unterverzeichnis nicht mehr automatisch stattfindet.

/* Datei m.rs */

#[path = "m/m0.rs"]
pub mod m0;

Zyklische Definitionen

Definitionen dürfen sich wechselseitig aufeinander beziehen. Darüber hinaus darf man solche Definitionen in Module aufteilen. So zieht sich die wechselseitig rekursive Definition

mod even {
    use super::odd::odd;
    pub fn even(x: u32) -> bool {
        if x == 0 {true} else {odd(x - 1)}
    }
}

mod odd {
    use super::even::even;
    pub fn odd(x: u32) -> bool {
        if x == 0 {false} else {even(x - 1)}
    }
}

der beiden Funktionen even und odd durch zwei Module.