↑Programmieren in Rust
Rust erlaubt Funktionen mit mehr als einem Rückgabewert. Ermöglicht wird dies durch die Zusammenfassung der Rückgabewerte zu einem Tupel.
Als einfaches Beispiel bietet sich die Umrechnung zwischen kartesischen und polaren Koordinaten an.
fn from_polar(r: f64, phi: f64) -> (f64, f64) { (r*phi.cos(), r*phi.sin()) } fn polar_from(x: f64, y: f64) -> (f64, f64) { (x.hypot(y), y.atan2(x)) } fn main() { use std::f64::consts::PI; let (x, y) = from_polar(1.0, 0.5*PI); println!("x = {:.4}; y = {:.4}", x, y); let (r, phi) = polar_from(x, y); println!("r = {:.4}; phi = {:.4}", r, phi); }
Eingehen möchte ich außerdem noch auf unterschiedliche Arten der Parameterübergabe. Unter anderem tut sich die Frage auf, ob sich eine Variable durch eine Prozedur modifizieren lassen kann. Ein unbedarfter Einsteiger könnte auf den Gedanken kommen, die Inkrementierung einer Variablen ließe sich durch
fn inc(mut x: i32) { x = x + 1; }
beschreiben. Jedoch schlägt das Testprogramm
fn main() { let mut i = 1; inc(i); assert_eq!(i, 2); }
fehl. Zudem beschwert sich der Compiler, dass der Wert, der in
x
geschrieben wurde, nie gelesen wird, und dass
i
nicht veränderlich sein braucht. Der Grund dafür ist
die Parameterübergabe, die stets per Kopie oder per Move vonstattengeht,
man spricht von Wertparametern, engl. call by value.
Weil also lediglich eine Kopie der ursprünglichen Variable i
überschrieben wird, und nicht die Variable selbst, bewirkt der
Aufruf von inc
keine Veränderung von i
.
Zur Herbeiführung der gewünschten Veränderung bedarf es Referenzparametern, engl. call by reference. Das will heißen, dass bei der Parameterübergabe eine Referenz von der ursprüngliche Variable erstellt wird.
Obwohl Referenzparameter in Rust nicht als eigenständiges Konzept vorhanden sind, lassen sie sich hinreichend bequem mit Zeigern nachbilden. Möchte man also eine Variable ändern lassen, muss die Funktion eine alleinige Referenz auf die Variable als Parameter bekommen. Insofern ist das gewünschte Programm formulierbar als:
fn inc(x: &mut i32) { *x = *x + 1; } fn main() { let mut i = 1; inc(&mut i); assert_eq!(i, 2); }
Als variadisch bezeichnet man Funktionen, die keine feste Arität haben, das heißt, keine feste Anzahl an Argumenten. Obwohl solche Funktionen in Rust nicht direkt formulierbar sind, lassen sie sich ausreichend ergonomisch nachstellen, indem der variadische Teil in ein Slice verpackt wird.
Für die Funktion mean
, welche den Mittelwert der
Argumente bildet, findet sich bspw. die Formulierung:
fn mean(a: &[f64]) -> f64 { a.iter().sum::<f64>()/(a.len() as f64) } fn main() { let m = mean(&[1.0, 2.0, 3.0, 4.0]); println!("{}", m); }
Bei heterogenen Argumenten möchte man für die Argumente einer
variadischen Funktion jeweils einen unterschiedlichen Typ zulassen.
Ein Ansatz, wie sich das erreichen lässt, ist das Hochcasten der
Argumente in einen Trait-Typ. Das kann im Extremfall auch der
Trait Any
sein. Dafür muss von den Argumenten jeweils
ein Borrow gebildet werden. Wir definieren für die Borrows ein Makro
argv
, das sorgt für hübsche Syntax.
Das Beispiel zeigt eine variadische Funktion, die Argumente
unterschiedlichen Typs verträgt, solange diese nur den
Trait Display
implementieren.
macro_rules! argv { ($($x:expr),*) => {&[$(&$x),*]} } fn display(a: &[&dyn std::fmt::Display]) { for x in a { println!("{}", x); } } fn main() { display(argv!["Eule", 24, true]); }
Alternative Syntax:
macro_rules! call { ($f:expr, $($x:expr),*) => {$f(&[$(&$x),*])} } fn main() { call!(display, "Eule", 24, true); }
Es ist auch machbar, variadische Funktionen ohne Rückgriff auf Zeiger-Indirektionen zu konstruieren. Grundlage dafür ist ein Helfer-Trait, der ein vom Typ abhängiges Verhalten schafft. Die Idee ist nun, die Liste der Argumente als Tupel zu kodieren. Weil Tupel unterschiedlicher Länge einen unterschiedlichen Typ haben, kann man so das Verhalten von der Arität abhängig machen.
trait DisplayFn { fn display(self); } impl DisplayFn for (&str,) { fn display(self) { println!("{}", self.0); } } impl DisplayFn for (&str, i32) { fn display(self) { println!("{}", self.0); println!("{}", self.1); } } fn display<T: DisplayFn>(x: T) { DisplayFn::display(x) } fn main() { display(("Eule",)); display(("Eule", 24)); }
Man mag nun einwenden, dass die Arität bei der dargestellten
Konstruktion immer beschränkt bleibt. Von dieser Einschränkung können
wir uns aber lösen, indem wir anstelle der Tupel eine heterogene Liste
List
benutzen. Mit dieser gelingt eine rekursive
Formulierung, die display
allgemein implementiert.
struct Nil; struct List<Head, Tail> (Head, Tail); macro_rules! list { () => {Nil}; ($x:expr) => {List($x, Nil)}; ($x:expr, $($tail:expr),*) => {List($x, list![$($tail),*])} } trait DisplayFn { fn display(self); } impl DisplayFn for Nil { fn display(self) {} } impl<H, T> DisplayFn for List<H, T> where H: std::fmt::Display, T: DisplayFn { fn display(self) { println!("{}", self.0); DisplayFn::display(self.1); } } fn display<T: DisplayFn>(x: T) { DisplayFn::display(x) } fn main() { display(list!["Eule", 24, true]); }
Problem bei jeder Rekursion ist die Allokation von Stapelspeicher. Bei einer bestimmten Rekursionstiefe ist der Aufrufstapel aufgebraucht, so dass es zum Programmabbruch kommt. Eine endständige Rekursion lässt sich dahingehend entwickeln, dass der wesentliche Bedarf an Stapelspeicher verschwindet.
Leider ist bisweilen kein Endrekursion erzwingendes Konzept verfügbar. Man kann sie allerdings emulieren. Eine klassischer Ansatz nutzt ein sogenanntes Trampolin. Anstelle des Funktionsaufrufs kehrt der Kontrollfluss bei dieser Konstruktion mit den Argumenten von der Funktion zurück in eine Schleife, bei welcher die Funktion im nächsten Schleifendurchlauf mit den neuen Argumenten abermals aufgerufen wird.
Zu beachten ist noch, dass die Rekursion irgendwann bei einem Basisfall enden können muss. Stellt man diese Unterscheidung durch eine Enumeration dar, gelangt man zur folgenden Implementierung.
enum Rec<C, R> {Continue(C), Return(R)} use Rec::{Continue, Return}; fn tail_iter<C, R, F>(mut x: C, mut f: F) -> R where F: FnMut(C) -> Rec<C, R> { loop { match f(x) { Continue(c) => x = c, Return(r) => return r, } } }
In ihrer endständigen Formulierung nimmt die Fakultätsfunktion
fn fac(n: u32) -> u32 { if n == 0 {1} else {n*fac(n - 1)} }
beispielsweise die Gestalt
fn fac(n: u32) -> u32 { fn fac_rec(n: u32, y: u32) -> u32 { if n == 0 {y} else {fac_rec(n - 1, n*y)} } fac_rec(n, 1) }
an. Wir trampolinieren sie und erhalten:
fn fac(n: u32) -> u32 { tail_iter((n, 1), |(n, y)| { if n == 0 {Return(y)} else {Continue((n - 1, n*y))} }) }
Besteht Bedarf an wechselseitiger Rekursion, genügt die beschriebene Konstruktion nicht mehr. Wir bekommen sie trotzdem, indem wir die aufzurufende Funktion mit einer weiteren Enumeration unterscheiden. Betrachten wir
fn odd(n: u32) -> bool { if n == 0 {false} else {even(n - 1)} } fn even(n: u32) -> bool { if n == 0 {true} else {odd(n - 1)} }
mit der endständigen Rekursion:
fn even(n: u32) -> bool { fn even_rec(n: u32, y: bool) -> bool { if n == 0 {y} else {odd_rec(n - 1, y)} } fn odd_rec(n: u32, y: bool) -> bool { if n == 0 {!y} else {even_rec(n - 1, y)} } even_rec(n, true) }
Und nun das kleine Kunststück:
fn even(n: u32) -> bool { enum Call {Even(u32), Odd(u32)} use Call::{Even, Odd}; use Rec::{Return as R, Continue as C}; tail_iter((Even(n), true), |(app, y)| { match app { Even(n) => if n == 0 {R(y)} else {C((Odd(n - 1), y))}, Odd(n) => if n == 0 {R(!y)} else {C((Even(n - 1), y))} } }) }