Hlavní navigace

Generické typy v programovacím jazyku Rust

Pavel Tišnovský

V šesté části seriálu se budeme věnovat problematice generických datových typů, protože ty mají v Rustu nezastupitelné místo a je na nich mj. postavena i celá základní knihovna tohoto jazyka.

Obsah

1. Generické typy v programovacím jazyku Rust

2. Komplexní čísla s volitelným typem reálné a imaginární složky

3. Motivace pro implementaci generických funkcí

4. Vytvoření generické funkce

5. Silná typová kontrola

6. Tisk hodnoty komplexního čísla s volitelným typem reálné a imaginární složky

7. Trait „Debug“

8. Přetížení operátoru + pro komplexní čísla

9. Změna operátoru + podle typu druhého operandu

10. Porovnání dvou komplexních čísel

11. Trait specifikující chování operátoru == (porovnání)

12. Obsah další části seriálu

13. Repositář s demonstračními příklady

14. Odkazy na Internetu

1. Generické typy v programovacím jazyku Rust

Programovací jazyk Rust podporuje deklaraci a následné použití generických datových typů, které mohou při správném použití zjednodušit tvorbu znovupoužitelného programového kódu a současně zajistit silnou typovou kontrolu při překladu. Podívejme se nejprve na motivační příklad, v němž prozatím nejsou použity generické datové typy. Jedná se opět o implementaci datového typu představujícího komplexní čísla. Naše první verze vypadala zhruba takto – Complex je zde pouhá datová struktura:

struct Complex {
    real: f32,
    imag: f32,
}
 
fn main() {
    let c1 = Complex{real:10.0, imag:20.0};
    let c2 = Complex{real:10.1, imag:20.1};
    let c3 = Complex{real:10.2, imag:20.2};
    let c4 = Complex{real:1., imag:2.};
 
    println!("{}+{}i", c1.real, c1.imag);
    println!("{}+{}i", c2.real, c2.imag);
    println!("{}+{}i", c3.real, c3.imag);
    println!("{}+{}i", c4.real, c4.imag);
}

V tomto zdrojovém kódu je deklarováno, že reálná a imaginární složka je představována datovým typem float/single, což je překladačem striktně hlídáno (a to mnohem silněji, než například v céčku). Pokud se například pokusíme do reálné či imaginární složky vložit celé číslo, dojde k chybě při překladu:

struct Complex {
    real: f32,
    imag: f32,
}
 
fn main() {
    let c1 = Complex{real:10, imag:20};
    let c2 = Complex{real:10.1, imag:20.1};
    let c3 = Complex{real:10.2, imag:20.2};
    let c4 = Complex{real:1, imag:2};
 
    println!("{}+{}i", c1.real, c1.imag);
    println!("{}+{}i", c2.real, c2.imag);
    println!("{}+{}i", c3.real, c3.imag);
    println!("{}+{}i", c4.real, c4.imag);
}

Při pokusu o překlad tohoto příkladu by se mělo vypsat následující chybové hlášení, a to pro všechny výskyty hodnoty odlišného datového typu:

error[E0308]: mismatched types
 --> test.rs:7:27
  |
7 |     let c1 = Complex{real:10, imag:20};
  |                           ^^ expected f32, found integral variable
  |
  = note: expected type `f32`
  = note:    found type `{integer}`
 
error[E0308]: mismatched types
 --> test.rs:7:36
  |
7 |     let c1 = Complex{real:10, imag:20};
  |                                    ^^ expected f32, found integral variable
  |
  = note: expected type `f32`
  = note:    found type `{integer}`
 
error[E0308]: mismatched types
  --> test.rs:10:27
   |
10 |     let c4 = Complex{real:1, imag:2};
   |                           ^ expected f32, found integral variable
   |
   = note: expected type `f32`
   = note:    found type `{integer}`
 
error[E0308]: mismatched types
  --> test.rs:10:35
   |
10 |     let c4 = Complex{real:1, imag:2};
   |                                   ^ expected f32, found integral variable
   |
   = note: expected type `f32`
   = note:    found type `{integer}`
 
error: aborting due to 4 previous errors

Podle očekávání nebude překlad dokončen ani ve chvíli, kdy se namísto typu float/single použijí konstanty datového typu double:

fn main() {
    let c1 = Complex{real:10f64, imag:20f64};
 
    println!("{}+{}i", c1.real, c1.imag);
}

V průběhu překladu by se měla vypsat tato dvě chybová hlášení:

error[E0308]: mismatched types
 --> test.rs:7:27
  |
7 |     let c1 = Complex{real:10f64, imag:20f64};
  |                           ^^^^^ expected f32, found f64
 
error[E0308]: mismatched types
 --> test.rs:7:39
  |
7 |     let c1 = Complex{real:10f64, imag:20f64};
  |                                       ^^^^^ expected f32, found f64
 
error: aborting due to 2 previous errors

2. Komplexní čísla s volitelným typem reálné a imaginární složky

Předchozí příklad sice fungoval korektně a podle předpokladů, ovšem jen ve chvíli, kdy nám postačovalo použití komplexních čísel, jejichž složky byly reprezentovány typem float/single. Ovšem je jen otázkou času, kdy nějaký vývojář bude chtít použít podobný kód, ovšem například pro datový typ double, pro typ „zlomek“ atd. V takovém případě může být nejvýhodnější deklaraci datové struktury upravit takovým způsobem, aby se konkrétní typ složek komplexního čísla rozpoznal až v době překladu na základě typů konkrétních hodnot či výrazů použitých pro konstrukci datové struktury. Programovací jazyk Rust tento přístup podporuje, protože umožňuje následující styl deklarace (znak T není klíčovým slovem, ovšem je v kontextu generických datových typů často používán, takže tento úzus taktéž dodržíme):

struct Complex<T> {
    real: T,
    imag: T,
}

Tento zápis znamená, že se za T při překladu doplní konkrétní rozpoznaný datový typ, což si můžeme snadno vyzkoušet:

fn main() {
    let c1 = Complex{real:10, imag:20};
    let c2 = Complex{real:10.1, imag:20.1};
    let c3 = Complex{real:10.2f64, imag:20.2f64};
    let c4 = Complex{real:true, imag:false};
 
    println!("{}+{}i", c1.real, c1.imag);
    println!("{}+{}i", c2.real, c2.imag);
    println!("{}+{}i", c3.real, c3.imag);
    println!("{}+{}i", c4.real, c4.imag);
}

Ve chvíli, kdy se pokusíme o kombinaci různých typů, budeme na to upozorněni překladačem, protože typ reálné i imaginární složky musí být totožný:

fn main() {
    let c1 = Complex{real:10, imag:true};

    println!("{}+{}i", c1.real, c1.imag);
}
error[E0308]: mismatched types
 --> test.rs:7:36
  |
7 |     let c1 = Complex{real:10, imag:true};
  |                                    ^^^^ expected integral variable, found bool
  |
  = note: expected type `{integer}`
  = note:    found type `bool`

error: aborting due to previous error

Jinými slovy – typ datové struktury Complex je parametrizovatelný, ovšem současně je stále zajištěna typová kontrola (nejedná se tedy o nic ve smyslu Complex(Object, Object) :-).

3. Motivace pro implementaci generických funkcí

V programovacím jazyku Rust je možné kromě deklarace generických datových typů vytvářet i generické funkce, tj. funkce, u nichž lze specifikovat parametrizovatelné typy argumentů i návratový typ. Podívejme se na sice poněkud umělý, ale o to kratší demonstrační příklad. V tomto příkladu nejprve deklarujeme výčtový typ a následně funkci, která akceptuje dva parametry typu i32 (celé číslo se znaménkem) a třetí parametr, na základě jehož hodnoty funkce vrátí buď první či druhý parametr. Nejprve si povšimněte, jak se používá výčtový typ (má vlastní jmenný prostor, proto se zapisuje Item::First a nikoli pouze First). Použití konstrukce match je v tomto případě idiomatické a mnohem lepší, než pokus o použití if, a to z toho důvodu, že překladač sám zkontroluje, zda v konstrukci match reagujeme na všechny možné vstupy (což samozřejmě děláme :-):

enum Item {
    First,
    Second,
}
 
fn select_item(first_item:i32, second_item:i32, item:Item) -> i32 {
    match item {
        Item::First  => first_item,
        Item::Second => second_item,
    }
}
 
fn main() {
    let x = 10;
    let y = 20;
    println!("1st item = {}", select_item(x, y, Item::First));
    println!("2nd item = {}", select_item(x, y, Item::Second));

}

Po překladu a spuštění by se na standardní výstup měly vypsat následující dva řádky znamenající, že poprvé funkce select_item vybrala a vrátila první argument (resp. zde jeho kopii!) a podruhé druhý argument:

1st item = 10
2nd item = 20

4. Vytvoření generické funkce

Přiznejme si, že funkce select_item v podobě, v jaké jsme si ji ukázali v předchozí kapitole, není příliš použitelná, protože ji lze volat pouze s parametry typu i32. Pokusme se tedy vytvořit podobnou funkci, ovšem generickou. V tomto případě to znamená, že typy prvních dvou parametrů musí být shodné a musí odpovídat návratovému typu funkce – ta totiž nemá provádět žádné konverze, pouze vybírat mezi prvním a druhým argumentem. Takto navržená generická funkce může vypadat následovně (povšimněte si především zápisu <T> za jménem funkce):

fn select_item<T>(first_item:T, second_item:T, item:Item) -> T {
    match item {
        Item::First  => first_item,
        Item::Second => second_item,
    }
}

Nově deklarovanou funkci je možné použít pro různé typy argumentů, samozřejmě za předpokladu, že oba dva argumenty budou stejného typu:

enum Item {
    First,
    Second,
}
 
fn select_item<T>(first_item:T, second_item:T, item:Item) -> T {
    match item {
        Item::First  => first_item,
        Item::Second => second_item,
    }
}
 
fn main() {
    let x = 10.1;
    let y = 20.2;
    println!("1st item = {}", select_item(x, y, Item::First));
    println!("2nd item = {}", select_item(x, y, Item::Second));
 
    let z:i32 = 10;
    let w:i32 = 20;
    println!("1st item = {}", select_item(z, w, Item::First));
    println!("2nd item = {}", select_item(z, w, Item::Second));
 
    let a = true;
    let b = false;
    println!("1st item = {}", select_item(a, b, Item::First));
    println!("2nd item = {}", select_item(a, b, Item::Second));
}

Po spuštění získáme následující řádky vypsané na standardní výstup:

1st item = 10.1
2nd item = 20.2
1st item = 10
2nd item = 20
1st item = true
2nd item = false

5. Silná typová kontrola

V předchozí kapitole jsme si řekli, že překladač pro novou podobu funkce select_item kontroluje, zda jsou typy prvních dvou argumentů shodné. Pojďme si toto tvrzení ověřit:

enum Item {
    First,
    Second,
}
 
fn select_item<T>(first_item:T, second_item:T, item:Item) -> T {
    match item {
        Item::First  => first_item,
        Item::Second => second_item,
    }
}
 
fn main() {
    let x = 10.1;
    let y = 20;
    println!("1st item = {}", select_item(x, y, Item::First));
    println!("2nd item = {}", select_item(x, y, Item::Second));
 
    let z:f32 = 10;
    let w:i32 = 20;
    println!("1st item = {}", select_item(z, w, Item::First));
    println!("2nd item = {}", select_item(z, w, Item::Second));
 
    let a = 10;
    let b = false;
    println!("1st item = {}", select_item(a, b, Item::First));
    println!("2nd item = {}", select_item(a, b, Item::Second));
}

Spusťme nyní překladač na tento zdrojový kód, aby bylo patrné, jak pracuje statická typová kontrola (navíc je ukázána i kontrola typů proměnných a hodnot přiřazovaných do proměnných, což je téma, kterému jsme se již věnovali):

error[E0308]: mismatched types
  --> test.rs:16:46
   |
16 |     println!("1st item = {}", select_item(x, y, Item::First));
   |                                              ^ expected floating-point variable, found integral variable
<std macros>:2:27: 2:58 note: in this expansion of format_args!
<std macros>:3:1: 3:54 note: in this expansion of print! (defined in <std macros>)
test.rs:16:5: 16:63 note: in this expansion of println! (defined in <std macros>)
   |
   = note: expected type `{float}`
   = note:    found type `{integer}`
 
error[E0308]: mismatched types
  --> test.rs:17:46
   |
17 |     println!("2nd item = {}", select_item(x, y, Item::Second));
   |                                              ^ expected floating-point variable, found integral variable
<std macros>:2:27: 2:58 note: in this expansion of format_args!
<std macros>:3:1: 3:54 note: in this expansion of print! (defined in <std macros>)
test.rs:17:5: 17:64 note: in this expansion of println! (defined in <std macros>)
   |
   = note: expected type `{float}`
   = note:    found type `{integer}`
 
error[E0308]: mismatched types
  --> test.rs:19:17
   |
19 |     let z:f32 = 10;
   |                 ^^ expected f32, found integral variable
   |
   = note: expected type `f32`
   = note:    found type `{integer}`
 
error[E0308]: mismatched types
  --> test.rs:21:46
   |
21 |     println!("1st item = {}", select_item(z, w, Item::First));
   |                                              ^ expected f32, found i32
<std macros>:2:27: 2:58 note: in this expansion of format_args!
<std macros>:3:1: 3:54 note: in this expansion of print! (defined in <std macros>)
test.rs:21:5: 21:63 note: in this expansion of println! (defined in <std macros>)
 
error[E0308]: mismatched types
  --> test.rs:22:46
   |
22 |     println!("2nd item = {}", select_item(z, w, Item::Second));
   |                                              ^ expected f32, found i32
<std macros>:2:27: 2:58 note: in this expansion of format_args!
<std macros>:3:1: 3:54 note: in this expansion of print! (defined in <std macros>)
test.rs:22:5: 22:64 note: in this expansion of println! (defined in <std macros>)
 
error[E0308]: mismatched types
  --> test.rs:26:46
   |
26 |     println!("1st item = {}", select_item(a, b, Item::First));
   |                                              ^ expected integral variable, found bool
<std macros>:2:27: 2:58 note: in this expansion of format_args!
<std macros>:3:1: 3:54 note: in this expansion of print! (defined in <std macros>)
test.rs:26:5: 26:63 note: in this expansion of println! (defined in <std macros>)
   |
   = note: expected type `{integer}`
   = note:    found type `bool`
 
error[E0308]: mismatched types
  --> test.rs:27:46
   |
27 |     println!("2nd item = {}", select_item(a, b, Item::Second));
   |                                              ^ expected integral variable, found bool
<std macros>:2:27: 2:58 note: in this expansion of format_args!
<std macros>:3:1: 3:54 note: in this expansion of print! (defined in <std macros>)
test.rs:27:5: 27:64 note: in this expansion of println! (defined in <std macros>)
   |
   = note: expected type `{integer}`
   = note:    found type `bool`
 
error: aborting due to 7 previous errors

6. Tisk hodnoty komplexního čísla s volitelným typem reálné a imaginární složky

Pokud budeme chtít vytisknout hodnotu komplexního čísla, jehož typy složek jsou parametrizovatelné, narazíme brzy na malý problém související s expanzí makra println! (toto makro potřebuje znát typy tisknutých hodnot). Řešením je implementace traitu nazvaného Debug pro složky komplexních čísel a implementace funkce print, která v případě komplexních čísel může vypadat následovně. Důležitý je zde formátovací řetězec {:?}, který odpovídá právě traitu Debug, pokud se uvede pouze {}, odpovídá to traitu Display:

fn print(&self) {
    println!("complex number: {:?}+{:?}i", self.real, self.imag);
}

Trait Debug nalezneme v modulu std::fmt, takže první řádek našeho programu bude vypadat takto:

use std::fmt::Debug;

Trait Debug může být pro složky komplexních čísel implementován stylem (určujeme zde, že typ T implementuje zvolený trait):

impl<T: Debug> Complex<T> {
 
    fn new(real: T, imag: T) -> Complex<T> {
        Complex{real:real, imag:imag}
    }
 
    fn print(&self) {
        println!("complex number: {:?}+{:?}i", self.real, self.imag);
    }
 
}

Zápis typu <T: Debug> Complex<T> použijeme i v dalších demonstračních příkladech, s nimiž se setkáme v navazujících článcích.

7. Trait „Debug“

Úplný zdrojový kód, v němž je použit trait Debug je vypsán pod tímto odstavcem:

use std::fmt::Debug;
 
struct Complex<T: Debug> {
    real: T,
    imag: T,
}
 
impl<T: Debug> Complex<T> {
 
    fn new(real: T, imag: T) -> Complex<T> {
        Complex{real:real, imag:imag}
    }
 
    fn print(&self) {
        println!("complex number: {:?}+{:?}i", self.real, self.imag);
    }
 
}
 
fn main() {
    let c1 = Complex::new(3, 4);
    let c2 = Complex::new(3.3f32, 4.4f32);
    let c3 = Complex::new(5.0f64, 6.0f64);
    c1.print();
    c2.print();
    c3.print();
}

8. Přetížení operátoru + pro komplexní čísla

Ve druhé části článku se budeme zabývat problematikou přetěžování operátorů, protože uvidíme, že i v této oblasti se využijí generické datové typy. Začneme operátorem +, který si přetížíme pro komplexní čísla. Přetížení operátoru v kontextu programovacího jazyka Rust znamená, že implementujeme trait Add z modulu std::ops. V tomto traitu je předepsána funkce add s poněkud zvláštní hlavičkou, z níž nepřímo plyne, že typ druhého operandu + (RHS = right-hand side) je volitelný (a potenciálně odlišný od typu operandu prvního) a dokonce že i návratový typ může být odlišný (u součtu to může vypadat podivně, ale řekněme, že u přetížíme operátor * pro skalární součin dvou vektorů):

fn add(self, rhs: RHS) -> Self::Output;

Zápis Self::Output odkazuje na atribut traitu nazvaný Output, který musíme taktéž nastavit. V našem konkrétním případě jsou oba operandy operátoru + stejného typu a navíc odpovídají výsledku výrazu s +. Implementace může vypadat následovně:

impl Add for Complex {
 
    type Output = Complex;
 
    fn add(self, right: Complex) -> Self::Output {
        Complex{real: self.real + right.real,
                imag: self.imag + right.imag}
    }
 
}

Povšimněte si, že skutečně deklarujeme výstupní typ jako Complex a stejný je i typ pravého operandu +. Navíc stojí za připomenutí fakt, že sémantika operátoru + je taková, že vrací novou hodnotu, což přesně odpovídá kódu funkce. Nesnažíme se tedy modifikovat stávající komplexní číslo, to ostatně ani není u neměnného typu možné:

Úplný demonstrační příklad vypadá takto:

use std::ops::Add;
 
struct Complex {
    real: f32,
    imag: f32,
}
 
impl Complex {
 
    fn new(real: f32, imag: f32) -> Complex {
        Complex{real:real, imag:imag}
    }
 
    fn print(&self) {
        println!("complex number: {:}+{:}i", self.real, self.imag);
    }
 
}
 
impl Add for Complex {
 
    type Output = Complex;
 
    fn add(self, right: Complex) -> Self::Output {
        Complex{real: self.real + right.real,
                imag: self.imag + right.imag}
    }
 
}
 
fn main() {
    let c1 = Complex::new(1.0, 1.0);
    let c2 = Complex::new(3.0, 4.0);
    c1.print();
    c2.print();
    let c3 = c1 + c2;
    c3.print();
}

Alternativně samozřejmě můžeme ve funkci add využít konstruktor:

use std::ops::Add;
 
struct Complex {
    real: f32,
    imag: f32,
}
 
impl Complex {
 
    fn new(real: f32, imag: f32) -> Complex {
        Complex{real:real, imag:imag}
    }
 
    fn print(&self) {
        println!("complex number: {:}+{:}i", self.real, self.imag);
    }
 
}
 
impl Add for Complex {
 
    type Output = Complex;
 
    fn add(self, right: Complex) -> Self::Output {
        Complex::new(self.real + right.real,
                     self.imag + right.imag)
    }
}
 
fn main() {
    let c1 = Complex::new(1.0, 1.0);
    let c2 = Complex::new(3.0, 4.0);
    c1.print();
    c2.print();
    let c3 = c1 + c2;
    c3.print();
}

9. Změna operátoru + podle typu druhého operandu

Ve skutečnosti může být chování operátoru +, nebo samozřejmě jakéhokoli jiného přetíženého operátoru, odlišné podle toho, jaký je typ druhého operandu (první operand má typ stejný – odpovídá typu, pro nějž implementujeme trait Add). Zkusme nyní změnit náš příklad takovým způsobem, aby bylo možné sečíst dvě komplexní čísla nebo ke komplexnímu číslu přičíst reálnou konstantu (výsledkem bude nové komplexní číslo). Zde se již musí použít generický zápis, nikoli však pro datový typ či funkci, ale pro celý trait. První implementaci traitu již známe, musíme ovšem přidat specifikaci typu (druhého operandu):

impl Add<Complex> for Complex {
 
    type Output = Complex;
 
    fn add(self, right: Complex) -> Self::Output {
        Complex::new(self.real + right.real,
                     self.imag + right.imag)
    }
}

Druhý trait se uplatní při součtu komplexní číslo+reálné číslo:

impl Add<f32> for Complex {
 
    type Output = Complex;
 
    fn add(self, right: f32) -> Self::Output {
        Complex::new(self.real + right,
                     self.imag)
    }
}

Úplný demonstrační příklad vypadá následovně (ve funkci main se sčítají dvě komplexní čísla i komplexní číslo s číslem reálným):

use std::ops::Add;
 
struct Complex {
    real: f32,
    imag: f32,
}
 
impl Complex {
 
    fn new(real: f32, imag: f32) -> Complex {
        Complex{real:real, imag:imag}
    }
 
    fn print(&self) {
        println!("complex number: {:}+{:}i", self.real, self.imag);
    }
 
}
 
impl Add<Complex> for Complex {
 
    type Output = Complex;
 
    fn add(self, right: Complex) -> Self::Output {
        Complex::new(self.real + right.real,
                     self.imag + right.imag)
    }
}
 
impl Add<f32> for Complex {
 
    type Output = Complex;
 
    fn add(self, right: f32) -> Self::Output {
        Complex::new(self.real + right,
                     self.imag)
    }
}
 
fn main() {
    let c1 = Complex::new(1.0, 1.0);
    let c2 = Complex::new(3.0, 4.0);
    c1.print();
    c2.print();
    let c3 = c1 + c2;
    c3.print();
    let c4 = c3 + 1000.;
    c4.print();
}

10. Porovnání dvou komplexních čísel

Další operátor, který se poměrně často přetěžuje, je operátor pro porovnání dvou hodnot. Tento operátor není pro uživatelem definované datové struktury předepsán, o čemž se ostatně lze velmi snadno přesvědčit při pokusu o překlad zdrojového kódu, v němž se budou porovnávat komplexní čísla:

use std::ops::Add;
 
struct Complex {
    real: f32,
    imag: f32,
}
 
impl Complex {
 
    fn new(real: f32, imag: f32) -> Complex {
        Complex{real:real, imag:imag}
    }
 
    fn print(&self) {
        println!("complex number: {:}+{:}i", self.real, self.imag);
    }
 
}
 
impl Add<Complex> for Complex {
 
    type Output = Complex;
 
    fn add(self, right: Complex) -> Self::Output {
        Complex::new(self.real + right.real,
                     self.imag + right.imag)
    }
}
 
impl Add<f32> for Complex {
 
    type Output = Complex;
 
    fn add(self, right: f32) -> Self::Output {
        Complex::new(self.real + right,
                     self.imag)
    }
}
 
fn main() {
    let c1 = Complex::new(1.0, 1.0);
    let c2 = Complex::new(3.0, 4.0);
    c1.print();
    c2.print();
    let c3 = c1 + c2;
    let c4 = Complex::new(4.0, 5.0);
    c3.print();
    c4.print();
    println!("c3 == c4? {}", (if c3==c4 { "yes"} else {"no"}));
}

Překladač při pokusu o kompilaci pouze vypíše následující chybovou zprávu:

error[E0369]: binary operation `==` cannot be applied to type `Complex`
  --> test.rs:49:34
   |
49 |     println!("c3 == c4? {}", (if c3==c4 { "yes"} else {"no"}));
   |                                  ^^
<std macros>:2:27: 2:58 note: in this expansion of format_args!
<std macros>:3:1: 3:54 note: in this expansion of print! (defined in <std macros>)
test.rs:49:5: 49:64 note: in this expansion of println! (defined in <std macros>)
   |
note: an implementation of `std::cmp::PartialEq` might be missing for `Complex`
  --> test.rs:49:34
   |
49 |     println!("c3 == c4? {}", (if c3==c4 { "yes"} else {"no"}));
   |                                  ^^
<std macros>:2:27: 2:58 note: in this expansion of format_args!
<std macros>:3:1: 3:54 note: in this expansion of print! (defined in <std macros>)
test.rs:49:5: 49:64 note: in this expansion of println! (defined in <std macros>)
 
error: aborting due to previous error

11. Trait specifikující chování operátoru == (porovnání)

Chybové hlášení překladače, které jsme viděli v předchozí kapitole, poměrně přesně naznačovalo, co je zapotřebí udělat proto, aby bylo možné dvě komplexní čísla porovnat. Je nutné implementovat buď trait Eq nebo PartialEq. Tyto dva traity se od sebe odlišují svou sémantikou, protože u traitu Eq je nutné zajistit test skutečné (relace) ekvivalence (ta je reflexivní, symetrická a tranzitivní), kdežto u traitu PartialEq se zajišťuje jen symetričnost a tranzitivita. Co to znamená v praxi? Typickým příkladem jsou hodnoty s plovoucí řádovou čárkou, které (pokud jsou implementovány podle normy IEEE 754), nezajišťují reflexivitu pro NaN, protože platí NaN != NaN.

Implementace traitu PartialEq pro komplexní čísla není příliš složitá, postačuje jen implementovat funkci eq vracející pro dvě komplexní čísla pravdivostní hodnotu (ve skutečnosti nemusí být pravým operandem komplexní číslo, což se podobá výše zmíněnému příkladu s operandem +):

impl PartialEq for Complex {
 
    fn eq(&self, right: &Complex) -> bool {
        self.real == right.real && self.imag == right.imag
    }
}

Implementovaný trait lze snadno otestovat:

use std::ops::Add;
use std::cmp::PartialEq;
 
struct Complex {
    real: f32,
    imag: f32,
}
 
impl Complex {
 
    fn new(real: f32, imag: f32) -> Complex {
        Complex{real:real, imag:imag}
    }
 
    fn print(&self) {
        println!("complex number: {:}+{:}i", self.real, self.imag);
    }
 
}
 
impl Add<Complex> for Complex {
 
    type Output = Complex;
 
    fn add(self, right: Complex) -> Self::Output {
        Complex::new(self.real + right.real,
                     self.imag + right.imag)
    }
}
 
impl Add<f32> for Complex {
 
    type Output = Complex;
 
    fn add(self, right: f32) -> Self::Output {
        Complex::new(self.real + right,
                     self.imag)
    }
}
 
impl PartialEq for Complex {
 
    fn eq(&self, right: &Complex) -> bool {
        self.real == right.real && self.imag == right.imag
    }
}
 
fn main() {
    let c1 = Complex::new(1.0, 1.0);
    let c2 = Complex::new(3.0, 4.0);
    c1.print();
    c2.print();
    let c3 = c1 + c2;
    let c4 = Complex::new(4.0, 5.0);
    c3.print();
    c4.print();
    println!("c3 == c4? {}", (if c3==c4 { "yes"} else {"no"}));
}

Povšimněte si, že konstrukci if-else lze použít i ve výrazu, pouze se musí uzavřít do kulatých závorek.

12. Obsah další části seriálu

V další části seriálu si nejprve popíšeme způsob přetížení dalších typů operátorů. Zajímavé budou především operátory typu +=, -= atd., protože u těchto operátorů se předpokládá, že dojde ke změně (modifikaci) levého operandu. To v Rustu znamená, že operand musí být měnitelný (mutable), což se samozřejmě bude týkat i naší implementace komplexních čísel. Ve druhé části článku se budeme zabývat způsobem alokace objektů na haldě (heap) a tím, jak lze zajistit automatickou správu paměti bez toho, aby bylo nutné používat komplikované garbage collectory.

13. Repositář s demonstračními příklady

Všechny dnes popisované demonstrační příklady byly, podobně jako v předchozích částech tohoto seriálu, uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/pre­sentations. Příklady si můžete v případě potřeby stáhnout i jednotlivě bez nutnosti klonovat celý (dnes již poněkud objemný) repositář:

14. Odkazy na Internetu

  1. Operators and Overloading
    https://doc.rust-lang.org/book/operators-and-overloading.html
  2. Module std::ops
    https://doc.rust-lang.org/std/ops/index.html
  3. Module std::cmp
    https://doc.rust-lang.org/std/cmp/index.html
  4. Trait std::ops::Add
    https://doc.rust-lang.org/stable/std/ops/trait.Add.html
  5. Trait std::ops::AddAssign
    https://doc.rust-lang.org/std/ops/trait.AddAssign.html
  6. Trait std::ops::Drop
    https://doc.rust-lang.org/std/ops/trait.Drop.html
  7. Trait std::cmp::Eq
    https://doc.rust-lang.org/std/cmp/trait.Eq.html
  8. Struct std::boxed::Box
    https://doc.rust-lang.org/std/boxed/struct.Box.html
  9. Explore the ownership system in Rust
    https://nercury.github.io/rus­t/guide/2015/01/19/ownership­.html
  10. Rust's ownership and move semantic
    http://www.slideshare.net/sa­neyuki/rusts-ownership-and-move-semantics
  11. Trait std::marker::Copy
    https://doc.rust-lang.org/stable/std/marker/tra­it.Copy.html
  12. Trait std::clone::Clone
    https://doc.rust-lang.org/stable/std/clone/tra­it.Clone.html
  13. The Stack and the Heap
    https://doc.rust-lang.org/book/the-stack-and-the-heap.html
  14. Rust Compare: Pointers & References
    http://www.rust-compare.com/site/pointers.html
  15. Rust Compare: Parameters
    http://www.rust-compare.com/site/params.html
  16. Why does this compile? Automatic dereferencing?
    https://users.rust-lang.org/t/why-does-this-compile-automatic-dereferencing/2183
  17. Understanding Pointers, Ownership, and Lifetimes in Rust
    http://koerbitz.me/posts/Understanding-Pointers-Ownership-and-Lifetimes-in-Rust.html
  18. Rust lang series episode #25 — pointers (#rust-series)
    https://steemit.com/rust-series/@jimmco/rust-lang-series-episode-25-pointers-rust-series
  19. Rust – home page
    https://www.rust-lang.org/en-US/
  20. Rust – Frequently Asked Questions
    https://www.rust-lang.org/en-US/faq.html
  21. Destructuring and Pattern Matching
    https://pzol.github.io/get­ting_rusty/posts/20140417_des­tructuring_in_rust/
  22. The Rust Programming Language
    https://doc.rust-lang.org/book/
  23. Rust (programming language)
    https://en.wikipedia.org/wi­ki/Rust_%28programming_lan­guage%29
  24. Go – home page
    https://golang.org/
  25. Stack Overflow – Most Loved, Dreaded, and Wanted language
    https://stackoverflow.com/re­search/developer-survey-2016#technology-most-loved-dreaded-and-wanted
  26. Rust vs Go (dva roky staré hodnocení, od té doby došlo k posunům v obou jazycích)
    http://jaredforsyth.com/2014/03/22/rust-vs-go/
  27. Rust vs Go: My experience
    https://www.reddit.com/r/go­lang/comments/21m6jq/rust_vs_go_my_ex­perience/
  28. Friends of Rust (Organizations running Rust in production)
    https://www.rust-lang.org/en-US/friends.html
  29. Rust programs versus C++ g++
    https://benchmarksgame.ali­oth.debian.org/u64q/compa­re.php?lang=rust&lang2=gpp
  30. Další benchmarky (nejedná se o reálné příklady „ze života“)
    https://github.com/kostya/benchmarks
  31. Go na Redditu
    https://www.reddit.com/r/golang/
  32. Rust vs. Go
    http://vschart.com/compare/rust/vs/go-language
  33. Abstraction without overhead: traits in Rust
    https://blog.rust-lang.org/2015/05/11/traits.html
  34. Method Syntax
    https://doc.rust-lang.org/book/method-syntax.html
  35. Traits in Rust
    https://doc.rust-lang.org/book/traits.html
  36. Functional Programming in Rust – Part 1 : Function Abstraction
    http://blog.madhukaraphatak­.com/functional-programming-in-rust-part-1/
  37. Of the emerging systems languages Rust, D, Go and Nim, which is the strongest language and why?
    https://www.quora.com/Of-the-emerging-systems-languages-Rust-D-Go-and-Nim-which-is-the-strongest-language-and-why
  38. Chytré ukazatele (moderní verze jazyka C++) [MSDN]
    https://msdn.microsoft.com/cs-cz/library/hh279674.aspx
  39. UTF-8 Everywhere
    http://utf8everywhere.org/
  40. Rust by Example
    http://rustbyexample.com/
  41. Rust oficiálně ve Fedoře
    https://mojefedora.cz/rust-oficialne-ve-fedore/
  42. Resource acquisition is initialization
    https://en.wikipedia.org/wi­ki/Resource_acquisition_is_i­nitialization
  43. TIOBE index (October 2016)
    http://www.tiobe.com/tiobe-index/
  44. Porovnání Go, D a Rustu na OpenHubu:
    https://www.openhub.net/lan­guages/compare?language_na­me[]=-1&language_name[]=-1&language_name[]=dmd&lan­guage_name[]=golang&langu­age_name[]=rust&language_na­me[]=-1&measure=commits
  45. String Types in Rust
    http://www.suspectsemantic­s.com/blog/2016/03/27/str­ing-types-in-rust/
  46. Trait (computer programming)
    https://en.wikipedia.org/wi­ki/Trait_%28computer_program­ming%29
  47. Type inference
    https://en.wikipedia.org/wi­ki/Type_inference
Našli jste v článku chybu?