Programmieren in Rust

Kontrollfluss

Fortführung von ›Grundbegriffe: Kontrollfluss‹.

Inhaltsverzeichnis

  1. Kurzschreibweisen
  2. Ausbruch aus Schleifen in Schleifen
  3. Pattern-Matching
    1. Oder-Verknüpfung von Mustern
    2. Bereiche als Muster
    3. Tupel-Muster
    4. Enum-Muster
    5. Wächter
  4. Tiefer Ausbruch

Kurzschreibweisen

Manchmal gibt man nur für eine bestimmte Enum-Variante eine Ausführung an. Das ist vor allem dann der Fall, wenn es wie bei Option und Result nur zwei Varianten gibt.

match expression {
    E::A(x) => {/* Ausführung von Zweig A */},
    _ => {}
}

Hierfür steht eine Kurzschreibweise zur Verfügung:

if let E::A(x) = expression {
    /* Ausführung von Zweig A */
}

Für das Konstrukt

match expression {
    E::A(x) => {/* Ausführung von Zweig A */},
    _ => {/* Ausführung sonst */}
}

gibt es entsprechend:

if let E::A(x) = expression {
    /* Ausführung von Zweig A */
} else {
    /* Ausführung sonst */
}

Erlaubt ist auch else if let. Mehr noch, man darf je Zweig frei zwischen if bzw. else if und if let bzw. else if let wählen.

Ausbruch aus Schleifen in Schleifen

Bekommt eine Schleife eine Marke 'label, führt break 'label zum Ausbruch aus dieser Schleife. Dies erlaubt den Ausbruch aus mehreren verschachtelten Schleifen.

Dieses Mittel sei an der folgenden Funktion demonstriert, die eine Sequenz von Werten in ein Array von vollständigen Blöcken der Größe n zerlegt.

fn complete_chunks<T: Clone>(a: &[T], n: u32) -> Vec<Vec<T>> {
    let mut acc = vec![];
    let mut i = a.into_iter();
    'outer: loop {
        let mut t = vec![];
        for _ in 0..n {
            match i.next() {
                Some(x) => t.push(x.clone()),
                None => break 'outer
            }
        }
        acc.push(t);
    }
    acc
}

fn main() {
    let a: Vec<u32> = (0..10).collect();
    println!("{:?}", complete_chunks(&a,4));
}

Außerdem ist es möglich, mit einem Wert aus der Schleife auszubrechen. Man könnte das Beispiel also auch so schreiben:

fn complete_chunks<T: Clone>(a: &[T], n: u32) -> Vec<Vec<T>> {
    let mut acc = vec![];
    let mut i = a.into_iter();
    'outer: loop {
        let mut t = vec![];
        for _ in 0..n {
            match i.next() {
                Some(x) => t.push(x.clone()),
                None => break 'outer acc
            }
        }
        acc.push(t);
    }
}

Natürlich hätte man das in diesem Fall auch anders formulieren können, z. B. durch Einführung einer Hilfsvariable oder einfach

None => return acc

Pattern-Matching

Oder-Verknüpfung von Mustern

Sollen mehrere Muster zum gleichen Ausführungszweig führen, lassen sich diese mit einem senkrechten Strich Oder-verknüpfen. Betrachten wir dazu die Fibonacci-Folge

a0 := 0; a1 := 1; an := an−1 + an−2.

Die Funktion

fn fib(n: u32) -> u32 {
    if n == 0 || n == 1 {1} else {fib(n-1) + fib(n-2)}
}

ist äquivalent zu:

fn fib(n: u32) -> u32 {
    match n {
        0 | 1 => 1,
        n => fib(n-1) + fib(n-2)
    }
}

Bereiche als Muster

Als Muster sind auch Bereiche erlaubt. So lässt sich in der obigen Funktion fib das Muster 0 | 1 gegen 0..=1 ersetzen. Ein besseres Beispiel ist die folgende Funktion char_class, die ASCII-Zeichen auf eine Zeichenklasse projiziert.

enum CharClass {None, Digit, Lower, Upper}

fn char_class(c: char) -> CharClass {
    match c {
        '0'..='9' => CharClass::Digit,
        'a'..='b' => CharClass::Lower,
        'A'..='B' => CharClass::Upper,
        _ => CharClass::None
    }
}

Auch Bereiche lassen sich mit dem senkrechten Strich Oder-verknüpfen. Dies sei an einer Variation der letzten Funktion verdeutlicht.

enum CharClass {None, Digit, Alpha}

fn char_class(c: char) -> CharClass {
    match c {
        '0'..='9' => CharClass::Digit,
        'a'..='b' |
        'A'..='B' => CharClass::Alpha,
        _ => CharClass::None
    }
}

Tupel-Muster

Entpacken eines Tupels t = (1,2) als

let x = t.0;
let y = t.1;

geht auch kürzer mittels:

let (x, y) = t;

Zudem ist auch teilweises Entpacken möglich. Z. B. sind für ein Tupel t = (1,2,3) die folgenden drei Zeilen gleichbedeutend:

let x = t.0;
let (x, _, _) = t;
let (x, ..) = t;

Man bekommt sogar

let x = t.0;
let z = t.2;

mittels:

let (x, .., z) = t;

Des Weiteren darf das Entpacken von Tupeln auch in Verbindung mit dem Pattern-Matching vorkommen. Wollen wir bspw. die Potenzfunktion

fn pow(x: u32, n: u32) > u32 {
    if n == 0 {1} else {x*pow(x, n-1)}
}

als einstellige Funktion formulieren, führt das zu:

fn pow(t: (u32, u32)) -> u32 {
    match t {
        (_, 0) => 1,
        (x, n) => x*pow((x, n-1))
    }
}

Obendrein darf das Entpacken sogar in der Angabe der Argumente vorkommen, womit sich die alternative Formulierung

fn pow((x, n): (u32,u32)) -> u32 {
    if n == 0 {1} else {x*pow((x, n-1))}
}

erlaubt.

Enum-Muster

Matching von Enumerationen ist schon öfters vorgekommen. Der Vollständigkeit halber hier nochmals ein Beispiel. Die Funktion checked_mul hat Rückgabewerte vom Typ

enum Option<T> {None, Some(T)}.

Hiermit gestattet sich die Formulierung der Potenzfunktion mit Überlauf-Prüfung als:

fn pow(x: u32, n: u32) -> Option<u32> {
    if n == 0 {Some(1)} else {
        x.checked_mul(match pow(x, n-1) {
            Some(value) => value,
            None => return None
        })
    }
}

Dieser spezielle Fall gewährt zudem die Abkürzung mit dem Fragezeichen-Operator:

fn pow(x: u32, n: u32) -> Option<u32> {
    if n == 0 {Some(1)} else {x.checked_mul(pow(x, n-1)?)}
}

Wächter

Ein Match-Arm kann eine zusätzliche logische Bedingung bekommen, die Wächter, engl. match guard, genannt wird. Die Bedingung wird durch ein dem Muster nachgestelltes if eingeleitet.

Das folgende Programm zeigt den klassischen euklidischen Algorithmus, wobei ein Wächter in der Fallunterscheidung zum Einsatz kommt.

fn gcd(a: u32, b: u32) -> u32 {
    match (a, b) {
        (a, 0) => a,
        (0, b) => b,
        (a, b) if a > b => gcd(a-b, b),
        (a, b) => gcd(a, b-a)
    }
}

fn lcd(a: u32, b: u32) -> u32 {
    a*b/gcd(a,b)
}

fn main() {
    println!("{}", (1..=10).fold(1, lcd));
}

Die gezeigte Formulierung ist äquivalent zu:

fn gcd(a: u32, b: u32) -> u32 {
    if b == 0 {a}
    else if a == 0 {b}
    else if a > b {gcd(a-b, b)}
    else {gcd(a, b-a)}
}

Pattern-Matching mit Wächtern ist so allgemein, dass sich beliebige Verzweigungen in diese Form umformulieren lassen. Beispielsweise ist die Potenzfunktion auch als

fn pow(x: u32, n: u32) -> u32 {
    match n {
        n if n == 0 => 1,
        n => x*pow(x, n-1)
    }
}

oder

fn pow(x: u32, n: u32) -> u32 {
    match () {
        () if n == 0 => 1,
        () => x*pow(x, n-1)
    }
}

formulierbar. Allgemein kann man jede Verzweigung

if cond {block1} else {block2}

in die Form

match () {
    () if cond => {block1},
    () => {block2}
}

bringen.

Tiefer Ausbruch

Erwähnen möchte ich noch eine recht entbehrliche Funktionalität die nur in sehr speziellen Situationen von Nutzen ist. Betrachten wir die folgende Funktion, die zu einem Feld von Feldern von Zahlen die Stelle des ersten Vorkommens einer gegebenen Zahl findet.

fn position(a: &[Vec<i32>], x: i32) -> Option<(usize, usize)> {
    for (i, b) in a.iter().enumerate() {
        for (j, &y) in b.iter().enumerate() {
            if x == y {return Some((i, j));}
        }
    }
    None
}

fn main() {
    let a = vec![vec![1, 2], vec![3, 4, 5]];
    assert_eq!(Some((1, 0)), position(&a, 3));
}

Möchte man das Programm nun funktional schreiben oder in Funktionen zerlegen, gelangt man zur Komplikation, dass der Ausbruch mittels return oder break nur bei gewöhnlichen Schleifen möglich ist. Um das dennoch zu erreichen, steht als eine Art Verallgemeinerung der tiefe Ausbruch mit dem Typ ControlFlow<B, C> zur Verfügung, dabei handelt es sich wie bei Result um eine Enumeration von zwei Varianten. Sie ist als

enum ControlFlow<B, C = ()> {
    Continue(C),
    Break(B)
}

definiert. Damit lässt sich das Programm in die folgende Gestalt bringen.

use std::ops::ControlFlow;
use ControlFlow::{Break, Continue};

fn position_at(i: usize, b: &[i32], x: i32) -> ControlFlow<(usize, usize)> {
    b.iter().enumerate().try_for_each(|(j, &y)| {
        if x == y {Break((i, j))} else {Continue(())}
    })
}

fn position(a: &[Vec<i32>], x: i32) -> Option<(usize, usize)> {
    a.iter().enumerate()
        .try_for_each(|(i, b)| position_at(i, b, x))
        .break_value()
}

Des Weiteren unterstützt ControlFlow den Fragezeichen-Operator, wobei der Aufstieg erwartungsgemäß zur Variante Break gehört.

Bemerkenswert ist, dass der tiefe Ausbruch gänzlich als Funktionalität einer Bibliothek formuliert werden kann ohne die Sprache erweitern zu müssen.