↑Programmieren in Rust
Bei der Programmierung von generischen arithmetischen Funktionen ist zu beachten, dass jede Operation einen bestimmten Trait benötigt, welcher die Operation implementiert.
Als erstes Beispiel soll die Funktion range(a,b)
dienen, die für einen beliebigen Ganzzahltyp das Feld
[a, a+1, a+2, …, b]
erzeugt.
use std::ops::Add;
trait Cast {fn cast(n: i32) -> Self;}
impl Cast for i8 {fn cast(n: i32) -> Self {n as i8}}
impl Cast for i16 {fn cast(n: i32) -> Self {n as i16}}
impl Cast for i32 {fn cast(n: i32) -> Self {n as i32}}
impl Cast for i64 {fn cast(n: i32) -> Self {n as i64}}
impl Cast for u8 {fn cast(n: i32) -> Self {n as u8}}
impl Cast for u16 {fn cast(n: i32) -> Self {n as u16}}
impl Cast for u32 {fn cast(n: i32) -> Self {n as u32}}
impl Cast for u64 {fn cast(n: i32) -> Self {n as u64}}
fn range<T>(a: T, b: T) -> Vec<T>
where T: Copy + Cast + Add<Output=T> + PartialOrd
{
let mut v: Vec<T> = Vec::new();
let mut k = a;
let one = T::cast(1);
while k<=b {
v.push(k);
k = k+one;
}
return v;
}
fn main() {
let v = range::<i32>(1,10);
println!("{:?}",v);
}
Das hätten wir natürlich auch einfacher haben können:
fn main() {
let v: Vec<i32> = (1..11).collect();
println!("{:?}",v);
}
Bei der Formulierung von Cast wurde dasselbe
Muster immer wiederholt. Oft lassen sich solche Wiederholungen
durch where-Klauseln faktorisieren. Fast dasselbe
Verhalten bekommen wir, wenn as durch from
ersetzt wird. Nur die potentiell verlustbehaftete Typumwandlung
von u8 nach i8 ist nicht erlaubt.
trait Cast {fn cast(n: u8) -> Self;}
impl<T> Cast for T where T: From<u8> {
fn cast(n: u8) -> T {T::from(n)}
}
Man kann auch einfach Cast gegen
From<u8> austauschen und T::cast(1)
gegen T::from(1).
Nun schaut es etwas ungeschickt aus wenn jede Funktion lange
where-Klauseln bekommt. Um dies zu vermeiden, lässt
sich ein Trait als Zusammenfassung der verwendeten Traits formulieren.
Man spricht auch von einem Alias-Trait. Hierzu wird ein Trait mit
Trait-Bounds beschränkt, wobei die Liste der Methoden aber leer bleiben
kann. Wir führen nun den neuen Trait Int ein, der mehr oder
weniger die ganzen Zahlen repräsentieren soll.
trait Int: Copy + From<u8> + Add<Output=Self> + PartialOrd {}
impl<T> Int for T
where T: Copy + From<u8> + Add<Output=T> + PartialOrd {}
fn range<T: Int>(a: T, b: T) -> Vec<T> {
let mut v: Vec<T> = Vec::new();
let mut k = a;
let one = T::from(1);
while k<=b {
v.push(k);
k = k+one;
}
return v;
}
Wie man sieht lassen sich auf diese Weise recht hübsch generische numerische Algorithmen schreiben. Für vordefinierte Traits und Datentypen für numerische Zwecke steht auch das Crate num[1] zur Verfügung. Da Algorithmen aber durch unnötige Trait-Bounds eventuell zu stark beschränkt werden, ist es in manchen Fällen eleganter, zusätzliche Zwischen-Traits einzuführen, die nur einige der Trait-Bounds enthalten.
Verzichtet man auf den Trait-Bound Copy und führt ggf.
die Trait-Bounds Sized und 'static ein,
lassen sich auch Algorithmen schreiben, die mit größeren
Datenstrukturen umgehen können.
In manchen Programmiersprachen besteht die Möglichkeit, nachträglich weitere Methoden zu einem Datentyp hinzuzufügen. Man spricht hier von Monkey patching oder Extension methods. Diese Technik ist eigentlich verpönt, da sie zu Bezeichnerkonflikten führen kann. Implementieren zwei Module nämlich denselben Bezeichner, liegt Zweideutigkeit vor.
Aus didaktischen Gründen soll trotzdem einmal gezeigt werden, wie
sich das Trait-System dafür »missbrauchen« lässt. Als Beispiel soll
zum Typ Vec eine Methode sq hinzugefügt
werden, die alle Elemente quadriert und das Ergebnis als neues
Array zurückgibt.
use std::ops::Mul;
use std::iter::FromIterator;
trait Sq<T> {
fn sq(&self) -> Vec<T>;
}
impl<T: Copy + Mul> Sq<T> for Vec<T>
where Vec<T>: FromIterator<<T as Mul>::Output>
{
fn sq(&self) -> Vec<T> {
self.iter().map(|&x| x*x).collect()
}
}
fn main() {
println!("{:?}", vec![1, 2, 3, 4].sq());
}
Die Trait-Bounds T: Copy+Mul und in der
where-Klausel können erst einmal ausgeblendet werden.
Diese sind dafür da, dass sich ein Wert x vom Typ
T auch multiplizieren und ein Iterator über die
Ergebnisse auch zu einem neuen Array sammeln lässt.
Glücklicherweise meckert der Compiler bei Bezeichnerkonflikten sofort. Außerdem lassen sie sich auf zwei Arten verhindern:
use eingebunden werden.
v.sq() darf man
auch Sq::sq(&v) schreiben, wobei der verwendete
Trait hier explizit angegeben wird.
Es kann später aber trotzdem zu ein paar Konflikten kommen, wenn zum Typ oder zum Trait neue Methoden hinzugefügt werden. Aus diesem Grund würde ich eher dazu neigen, solche Erweiterungsmethoden zu vermeiden.
Eine elegante Alternative zur Wucherung von Erweiterungsmethoden
bietet die allgemeine Beschreibung des Postfix-Aufrufs durch eine
einzige Erweiterungsmethode pipe:
trait Pipe {
fn pipe<Y>(self, f: impl FnOnce(Self) -> Y) -> Y
where Self: Sized
{f(self)}
fn pipe_ref<Y>(&self, f: impl FnOnce(&Self) -> Y) -> Y {f(self)}
}
impl<T> Pipe for T {}
Man schreibt nun freistehende Funktionen anstelle von Erweiterungsmethoden:
fn sq<T: Copy + Mul>(a: &Vec<T>) -> Vec<T>
where Vec<T>: FromIterator<<T as Mul>::Output>
{
a.iter().map(|&x| x*x).collect()
}
fn main() {
println!("{:?}", vec![1, 2, 3, 4].pipe_ref(sq));
}
Bei mehrstelligen Funktionen ist nun partielle Applikation erforderlich. Eine solche formuliert man üblicherweise als anonyme Funktion. Kurzes Beispiel:
fn add(x: i32, y: i32) -> i32 {x + y}
let a = 0;
let b = a.pipe(|x| add(x, 1));
Eine vollständige Implementierung von Pipe ist
im Crate tap [2] zu finden.