Programmieren in Rust

Funktionen

Inhaltsverzeichnis

  1. Tupelwertige Funktionen
  2. Referenzparameter
  3. Variadische Funktionen
  4. Endrekursion

Tupelwertige Funktionen

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

Referenzparameter

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

Variadische Funktionen

Homogene Argumente

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

Heterogene Argumente

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

Endrekursion

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