Hlavní navigace

Rust: předávání parametrů referencí, elegantní způsob práce se sekvencemi

22. 11. 2016
Doba čtení: 23 minut

Sdílet

 Autor: Rust project
Třetí část seriálu o programovacím jazyce Rust je věnována dvěma tématům. Nejdříve si řekneme, jak je možné předávat parametry do funkcí přes reference a následně si ukážeme „funkcionální“ způsob práce se sekvencemi.

Obsah

1. Předání parametrů do funkcí hodnotou

2. Předání parametrů referencí

3. Změna hodnoty proměnné předané referencí

4. Měnitelný parametr předaný hodnotou

5. Iterátory – malé zopakování z předchozích dílů

6. Délka sekvence

7. Průchod sekvencí se získáním indexu každého prvku

8. Otočení sekvence

9. Funkcionální přístup při zpracování sekvencí – funkce vyššího řádu

10. Metoda map: aplikace vybrané funkce na každý prvek sekvence

11. Metoda filter: výběr pouze některých prvků s použitím filtru

12. Metody take a take_while: získání pouze zvoleného počtu prvků ze sekvence

13. Nekonečné sekvence a práce s nimi

14. Metoda fold: postupné zpracování prvků sekvence

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

16. Odkazy na Internetu

1. Předání parametrů do funkcí hodnotou

První problematikou, kterou se dnes budeme zabývat, je způsob předávání parametrů funkcím přes takzvané reference. Již na tomto místě je vhodné upozornit na to, že pod názvem reference se v programovacím jazyku Rust skrývá koncept, který je v některých ohledech odlišný od referencí známých z Javy či z C++. Nejprve si však připomeňme, jak vypadá funkce, do které se předávají parametry hodnotou (value). Taková funkce může vypadat následovně:

fn print_values(arg1:i32, arg2:i32) {
    println!("function print_values()");
    println!("Variable1: {}", arg1);
    println!("Variable2: {}", arg2);
}

Při volání této funkce jí můžeme předat jak proměnné, tak i hodnoty. U proměnných nezávisí na tom, zda jsou měnitelné či neměnitelné, ovšem musí mít správný typ, což je striktně hlídáno:

fn main() {
    let variable1 = 1;
    let mut variable2 = 1;
 
    print_values(variable1, variable2);
    print_values(100, 200);
}

2. Předání parametrů referencí

V některých případech však není nutné, aby se parametr funkci musel předávat hodnotou, protože předání hodnotou vlastně znamená, že se vytvoří kopie parametru, což je mnohdy zbytečné (můžeme například chtít předat rozsáhlou strukturu). V takovém případě lze předat pouze referenci na hodnotu. Datový typ „reference na hodnotu typu TYP_X“ se zapisuje &TYP_X. Funkce akceptující referenci má z tohoto důvodu nepatrně odlišnou deklaraci datových typů parametrů. Uvnitř funkce je nutné reference takzvaně dereferencovat, na což se používá unární operátor * zapisovaný před argument či proměnnou typu reference:

fn pass_by_reference(arg1:&i32, arg2:&i32) {
    println!("function pass_by_reference()");
    println!("Variable1: {}", *arg1);
    println!("Variable2: {}", *arg2);
}

Funkce se bude volat následovně (povšimněte si použití & pro získání reference na proměnné):

fn main() {
    let variable1 = 1;
    let mut variable2 = 2;
 
    print_variables(variable1, variable2);
    pass_by_reference(&variable1, &variable2);
}

Již zde můžeme vidět odlišnost od referencí používaných v Javě i C++. Javu si dovolím z dalšího popisu vynechat, protože v ní se striktně rozlišuje mezi primitivními datovými typy (předávané vždy hodnotou) a referenčními (objektovými) typy (předávané naopak vždy referencí); což mj. vede ke zjednodušení syntaxe a k některým sémantickým problémům. V C++ je možné použít skutečné reference, ovšem s nimi se uvnitř funkce pracuje odlišně – nemusí se například provádět explicitní dereferencování. Reference jsou v obou jazycích vždy inicializované. Naopak v C++ není možné použít následující zápis, který je naopak v Rustu zcela legální:

pass_by_reference(&100, &200);

Celý zdrojový kód příkladu, v němž je deklarována funkce akceptující parametry předávané hodnotou a funkce akceptující reference, vypadá následovně:

fn print_variables(arg1:i32, arg2:i32) {
    println!("function print_variables()");
    println!("Variable1: {}", arg1);
    println!("Variable2: {}", arg2);
}
 
fn pass_by_reference(arg1:&i32, arg2:&i32) {
    println!("function pass_by_reference()");
    println!("Variable1: {}", *arg1);
    println!("Variable2: {}", *arg2);
}
 
fn main() {
    let variable1 = 1;
    let mut variable2 = 2;
 
    print_variables(variable1, variable2);
    pass_by_reference(&variable1, &variable2);
}

Po spuštění by se měly dvakrát po sobě vypsat shodné hodnoty:

function print_variables()
Variable1: 1
Variable2: 2
function pass_by_reference()
Variable1: 1
Variable2: 2

Ve skutečnosti není nutné dereferenci zapisovat ve chvíli, kdy si překladač dokáže odvodit, že přistupujeme k referencované hodnotě. Stručně řečeno – pokud z funkce pass_by_reference odstraníte hvězdičky (operátor dereference), proběhne překlad v pořádku. Totéž platí i při volání funkce, kdy je možné zapsat znak & kolikrát chcete. Je to zvláštní a užitečný důsledek silné typové kontroly a typové inference.

3. Změna hodnoty proměnné předané referencí

Proměnné, které jsou do nějaké funkce předané referencí, lze uvnitř této funkce změnit, ovšem je nutné dodržet několik podmínek. Nejprve se podívejme na demonstrační příklad, který vychází z částečně mylné domněnky, že se s referencemi v Rustu zachází stejně jako s ukazateli (zápis dereference se skutečně práci s ukazateli podobá, ale jen syntakticky, nikoli sémanticky). V příkladu je deklarována funkce nazvaná mutate_variable, v níž se přes referenci mění proměnná předaná v argumentu arg2:

fn mutate_variable(arg1:i32, arg2:&i32) {
    println!("mutation ...");
    let x = arg1 * 2;
    *arg2 = x;
}
 
fn print_variables(arg1:i32, arg2:i32) {
    println!("function print_variables()");
    println!("Variable1: {}", arg1);
    println!("Variable2: {}", arg2);
}
 
fn pass_by_reference(arg1:&i32, arg2:&i32) {
    println!("function pass_by_reference()");
    println!("Variable1: {}", *arg1);
    println!("Variable2: {}", *arg2);
}
 
fn main() {
    let variable1 = 1;
    let mut variable2 = 1;
 
    print_variables(variable1, variable2);
    pass_by_reference(&variable1, &variable2);
 
    mutate_variable(variable1, &variable2);
 
    print_variables(variable1, variable2);
    pass_by_reference(&variable1, &variable2);
}

Tento příklad se ovšem nepodaří přeložit, a to z toho důvodu, že volané funkci sice skutečně předáváme měnitelnou proměnnou, ovšem typ &i32 označuje neměnitelnou referenci:

error: cannot assign to immutable borrowed content `*arg2`
 --> 29_func_params.rs:4:5
  |
4 |     *arg2 = x;
  |     ^^^^^^^^^
 
error: aborting due to previous error

Pokud skutečně potřebujeme změnit hodnotu proměnné předané referencí, je nutné pozměnit typ druhého parametru funkce na &mut i32, což je typ znamenající „referenci, přes níž je možné změnit hodnotu referencovaného objektu“:

fn mutate_variable(arg1:i32, arg2:&mut i32) {
    println!("mutation ...");
    let x = arg1 * 2;
    *arg2 = x;
}
 
fn print_variables(arg1:i32, arg2:i32) {
    println!("function print_variables()");
    println!("Variable1: {}", arg1);
    println!("Variable2: {}", arg2);
}
 
fn pass_by_reference(arg1:&i32, arg2:&i32) {
    println!("function pass_by_reference()");
    println!("Variable1: {}", *arg1);
    println!("Variable2: {}", *arg2);
}
 
fn main() {
    let variable1 = 1;
    let mut variable2 = 1;
 
    print_variables(variable1, variable2);
    pass_by_reference(&variable1, &variable2);
 
    mutate_variable(variable1, &mut variable2);
 
    print_variables(variable1, variable2);
    pass_by_reference(&variable1, &variable2);
}

Po této nepatrné změně se program přeloží a bude pracovat podle požadavků – funkce mutate_variable skutečně změní hodnotu druhé proměnné:

function print_variables()
Variable1: 1
Variable2: 1
function pass_by_reference()
Variable1: 1
Variable2: 1
mutation ...
function print_variables()
Variable1: 1
Variable2: 2
function pass_by_reference()
Variable1: 1
Variable2: 2

4. Měnitelný parametr předaný hodnotou

To, že je reference měnitelná, však ještě neznamená, že budeme moci změnit hodnotu neměnitelné (immutable) proměnné. Překladač jazyka Rust si tuto skutečnost ohlídá. Pokud například změníme deklaraci druhé proměnné tak, že se bude jednat o proměnnou neměnitelnou:

    let variable1 = 1;
    let variable2 = 1;

Dojde při překladu k chybě:

error: cannot borrow immutable local variable `variable2` as mutable
  --> 29_func_params.rs:26:37
   |
21 |     let variable2 = 1;
   |         --------- use `mut variable2` here to make mutable
...
26 |     mutate_variable(variable1, &mut variable2);
   |                                     ^^^^^^^^^ cannot borrow mutably
 
error: aborting due to previous error

Nepomůže nám ani pokus o obelstění překladače tím, že při volání funkce vynecháme klíčové slovo mut:

    let variable1 = 1;
    let variable2 = 1;
    mutate_variable(variable1, &variable2);
error[E0308]: mismatched types
  --> 29_func_params.rs:26:32
   |
26 |     mutate_variable(variable1, &variable2);
   |                                ^^^^^^^^^^ values differ in mutability
   |
   = note: expected type `&mut i32`
   = note:    found type `&i32`
 
error: aborting due to previous error

Podívejme se ještě na jedno úskalí jazyka Rust. Pokud se pokusíme změnit hodnotu argumentu uvnitř funkce:

fn mutate_variable(arg1:i32, arg2:&mut i32) {
    println!("mutation ...");
    let x = arg1 * 2;
    arg1  = x;
    *arg2 = arg1;
}

Dojde opět k detekci chyby, protože argument je neměnitelný:

error[E0384]: re-assignment of immutable variable `arg1`
 --> 30_func_params.rs:4:5
  |
1 | fn mutate_variable(arg1:i32, arg2:&mut i32) {
  |                    ---- first assignment to `arg1`
...
4 |     arg1  = x;
  |     ^^^^^^^^^ re-assignment of immutable variable
 
error: aborting due to previous error

Tuto chybu můžeme napravit použitím klíčového slova mut před jméno argumentu. Tím ovšem jen povolujeme změnu hodnoty, která vznikla při volání funkce, nikoli změnu původní proměnné (první parametr je předáván hodnotou):

fn mutate_variable(mut arg1:i32, arg2:&mut i32) {
    println!("mutation ...");
    let x = arg1 * 2;
    arg1  = x;
    *arg2 = arg1;
}
 
fn print_variables(arg1:i32, arg2:i32) {
    println!("function print_variables()");
    println!("Variable1: {}", arg1);
    println!("Variable2: {}", arg2);
}
 
fn pass_by_reference(arg1:&i32, arg2:&i32) {
    println!("function pass_by_reference()");
    println!("Variable1: {}", *arg1);
    println!("Variable2: {}", *arg2);
}
 
fn main() {
    let variable1 = 1;
    let mut variable2 = 1;
 
    print_variables(variable1, variable2);
    pass_by_reference(&variable1, &variable2);
 
    mutate_variable(variable1, &mut variable2);
 
    print_variables(variable1, variable2);
    pass_by_reference(&variable1, &variable2);
}

Po spuštění je patrné, že se změna prvního argumentu nijak neprojevila vně funkce:

function print_variables()
Variable1: 1
Variable2: 1
function pass_by_reference()
Variable1: 1
Variable2: 1
mutation ...
function print_variables()
Variable1: 1
Variable2: 2
function pass_by_reference()
Variable1: 1
Variable2: 2

5. Iterátory – malé zopakování z předchozích dílů

Minule jsme se seznámili s objektem typu range, který dokáže generovat určitou sekvenci hodnot. Tento typ objektů je specifickým typem takzvaných iterátorů. Iterátory jsou známé a často používané i v dalších programovacích jazycích; v Rustu je jejich užitečnost ještě zvýšena tím, že iterátory lze použít pro vytváření a zpracování takzvaných „líných sekvencí“, které mohou být v některých případech i nekonečné. Zpracování (potenciálně) nekonečných sekvencí v konečném čase je umožněno právě díky tomu, že se prvky sekvence vyhodnocují „líně“, tj. až v tom okamžiku, kdy jsou skutečně zapotřebí.

V dalších příkladech budeme pro jednoduchost často používat objekty typu range, takže si jen připomeňme, jak se s nimi pracuje:

fn main() {
    // projiti sekvence hodnot od 1 do 10 (vcetne)
    for i in 1..11 {
        println!("{}", i);
    }
}

6. Délka sekvence

Délku sekvence můžeme zjistit několika způsoby. Nejprimitivnější je průchod všemi prvky sekvence v programové smyčce typu for each:

fn main() {
    // promenna pouzita pro vypocet delky sekvence (naivni implementace)
    let mut cnt :u32 = 0;
 
    // projiti sekvence hodnot od 1 do 10 (vcetne)
    for i in 1..11 {
        cnt += 1;
    }
    println!("{}", cnt);
}

Tuto možnost si zde ukazujeme jen z toho důvodu, že namísto jména řídicí proměnné smyčky můžeme použít univerzální vzorek _, což je idiomatický způsob použitý ve chvíli, kdy programátor potřebuje naznačit, že ho hodnota přiřazovaná do řídicí proměnné nezajímá:

fn main() {
    // promenna pouzita pro vypocet delky sekvence (naivni implementace)
    let mut cnt :u32 = 0;
 
    // projiti sekvence hodnot od 1 do 10 (vcetne)
    // ridici promenna se nikdy nepouzije
    for _ in 1..11 {
        cnt += 1;
    }
    println!("{}", cnt);
}

Iterátor je samostatným objektem, což již víme z minula, takže ho lze přiřadit do proměnné:

fn main() {
    // iterator (povsimnete si, ze je nemenny)
    let iter = 1..11;
 
    // promenna pouzita pro vypocet delky sekvence (naivni implementace)
    let mut cnt :u32 = 0;
 
    // projiti sekvence hodnot od 1 do 10 (vcetne)
    // ridici promenna se nikdy nepouzije
    for _ in iter {
        cnt += 1;
    }
    println!("{}", cnt);
}

Mnohem jednodušší a samozřejmě i rychlejší je však zavolat metodu count vracející počet prvků (pokud se tedy nejedná o nekonečnou sekvenci, tam je ovšem volání této metody ohlídáno):

fn main() {
    // iterator (povsimnete si, ze je nemenny)
    let iter = 1..11;
 
    // promenna pouzita pro vypocet delky sekvence (typ musi byt nastaven na usize)
    let cnt :usize;
 
    cnt = iter.count();
 
    println!("count = {}", cnt);
}

7. Průchod sekvencí se získáním indexu každého prvku

V mnoha případech může být užitečné získat při průchodu sekvence indexy jednotlivých prvků. První řešení spočívá v použití počítané programové smyčky, to je však poměrně nešikovné (viz Java a její rozhraní ListIterator). Lepší je následující způsob založený na metodě enumerate, která vrací nový iterátor. Tento iterátor generuje dvojici index+prvek a jeho použití je velmi jednoduché:

fn main() {
    // iterator (povsimnete si, ze je nemenny)
    let iter = 1..11;
 
    // pruchod sekvenci se ziskanim indexu a hodnoty kazdeho prvku
    for (index,item) in iter.enumerate() {
    println!("item[{}] = {}", index, item);
    }
}

Můžeme zde vidět i novou sémantiku (index,item) použitelnou i v jiných částech jazyka.

8. Otočení sekvence

V některých případech je zapotřebí sekvenci otočit. K tomuto účelu lze využít metodu nazvanou rev (od slova „reverse“). Její použití je jednoduché, jak je ostatně patrné z následujícího demonstračního příkladu:

fn main() {
    // iterator (povsimnete si, ze je nemenny)
    let iter1 = 1..11;
    let iter2 = iter1.rev();
 
    // pruchod sekvenci se ziskanim indexu a hodnoty kazdeho prvku
    for (index,item) in iter2.enumerate() {
    println!("item[{}] = {}", index, item);
    }
}

Výsledek by nás v tomto případě neměl překvapit:

item[0] = 10
item[1] = 9
item[2] = 8
item[3] = 7
item[4] = 6
item[5] = 5
item[6] = 4
item[7] = 3
item[8] = 2
item[9] = 1

O některých důsledcích plynoucích z použití této metody si více řekneme v části o nekonečných sekvencích.

9. Funkcionální přístup při zpracování sekvencí – funkce vyššího řádu

V programovacím jazyce Rust nalezneme několik metod použitelných při zpracování sekvencí vytvářených pomocí iterátorů. Tyto metody mají jména i sémantiku převzatou z funkcionálních jazyků (poprvé se objevily v LISPu, který sice není čistě funkcionální, ale nalezneme v něm mnoho prvků, které se do FP jazyků posléze dostaly). Mezi zmíněné metody – které by byly ve funkcionálních jazycích implementovány funkcemi vyššího řádu – patří zejména map, filter, take, take_while a fold (někde se nazývá reduce).

10. Metoda map: aplikace vybrané funkce na každý prvek sekvence

S funkcí či možná přesněji řečeno s metodou vyššího řádu nazvanou map jsme se již setkali minule, takže si jen krátce připomeňme, že map postupně aplikuje jí předanou funkci na jednotlivé prvky nějaké sekvence a vytváří tak sekvenci novou (modifikovanou). V prvním příkladu se funkce map bude postupně volat neanonymní funkci square:

fn square(x :i32) -> i32 {
    x * x
}
 
fn main() {
    // iterator (povsimnete si, ze je nemenny)
    let iter1 = 1..11;
    let iter2 = iter1.map(square);
 
    // pruchod sekvenci se ziskanim indexu a hodnoty kazdeho prvku
    for (index,item) in iter2.enumerate() {
    println!("item[{}] = {}", index, item);
    }
}

Po překladu získáme na standardním výstupu tyto zprávy:

item[0] = 1
item[1] = 4
item[2] = 9
item[3] = 16
item[4] = 25
item[5] = 36
item[6] = 49
item[7] = 64
item[8] = 81
item[9] = 100

Můžeme samozřejmě použít i anonymní funkci, což je více idiomatické:

fn main() {
    // iterator (povsimnete si, ze je nemenny)
    let iter1 = 1..11;
    let iter2 = iter1.map(|x| x*x);

    // pruchod sekvenci se ziskanim indexu a hodnoty kazdeho prvku
    for (index,item) in iter2.enumerate() {
        println!("item[{}] = {}", index, item);
    }
}

Jen si připomeňme, že do anonymní funkce lze bez problémů zapisovat bloky, protože poslední výraz v bloku určuje i hodnotu celého bloku (blok je výraz, ne příkaz). To stejné platí i pro rozhodovací konstrukce if-then či match, takže následující zápis je zcela korektní a funkční:

fn main() {
    // iterator (povsimnete si, ze je nemenny)
    let iter1 = 1..11;
    let iter2 = iter1.map(|x| {if x<5 {x*x} else {10/x}});

    // pruchod sekvenci se ziskanim indexu a hodnoty kazdeho prvku
    for (index,item) in iter2.enumerate() {
        println!("item[{}] = {}", index, item);
    }
}

Po spuštění takto upraveného příkladu získáme na standardním výstupu těchto deset řádků:

item[0] = 1
item[1] = 4
item[2] = 9
item[3] = 16
item[4] = 2
item[5] = 1
item[6] = 1
item[7] = 1
item[8] = 1
item[9] = 1

11. Metoda filter: výběr pouze některých prvků s použitím filtru

Užitečná je v mnoha případech i metoda nazvaná filter, která ze sekvence vybere pouze ty prvky, které odpovídají zadanému predikátu. Predikátem je libovolná funkce vracející pravdivostní hodnotu true či false. Zkusme vytvořit a použít predikát, kterým bude funkce testující, zda je parametr sudé číslo. Povšimněte si však jedné maličkosti – anonymní funkci uvedené v metodě filter se postupně předává reference na zpracovávaný prvek, tudíž je nutné před voláním našeho predikátu even použít dereferenci *x:

fn even(x :u32) -> bool {
    x % 2 == 0
}
 
fn main() {
    // iterator (povsimnete si, ze je nemenny)
    let iter1 = 1..11;
    let iter2 = iter1.filter(|x| even(*x));
 
    // pruchod sekvenci se ziskanim indexu a hodnoty kazdeho prvku
    for (index,item) in iter2.enumerate() {
    println!("item[{}] = {}", index, item);
    }
}

Výsledek je předvídatelný:

item[0] = 2
item[1] = 4
item[2] = 6
item[3] = 8
item[4] = 10

Zajímavé je, že anonymní funkci volající náš predikát lze zapsat i jinak – již parametr anonymní funkce je označen jako reference, takže kód je čitelnější (dereferenci si překladač doplní automaticky):

fn even(x :u32) -> bool {
    x % 2 == 0
}
 
fn main() {
    // iterator (povsimnete si, ze je nemenny)
    let iter1 = 1..11;
    let iter2 = iter1.filter(|&x| even(x));
 
    // pruchod sekvenci se ziskanim indexu a hodnoty kazdeho prvku
    for (index,item) in iter2.enumerate() {
    println!("item[{}] = {}", index, item);
    }
}

Výsledek překladu bude (bitově) naprosto shodný, stejně jako bude stejný obsah standardního výstupu:

item[0] = 2
item[1] = 4
item[2] = 6
item[3] = 8
item[4] = 10

Nejlepší je samozřejmě vložit predikát přímo do anonymní funkce předané metodě filter. S následujícím kódem se pravděpodobně setkáte nejčastěji:

fn main() {
    // iterator (povsimnete si, ze je nemenny)
    let iter1 = 1..11;
    let iter2 = iter1.filter(|x| x % 2 == 0);
 
    // pruchod sekvenci se ziskanim indexu a hodnoty kazdeho prvku
    for (index,item) in iter2.enumerate() {
    println!("item[{}] = {}", index, item);
    }
}

Výsledek je podle očekávání stále stejný:

item[0] = 2
item[1] = 4
item[2] = 6
item[3] = 8
item[4] = 10

12. Metody take a take_while: získání pouze zvoleného počtu prvků ze sekvence

Ve chvíli, kdy je nutné z nějaké sekvence získat pouze určitý počet prvků, je možné zavolat metodu nazvanou take. Tato metoda vrátí pouze prvních n prvků ze sekvence, nezávisle na tom, zda se jedná o sekvenci konečnou či nekonečnou. V následujícím programu se nejprve vytvoří objekt typu range se sto prvky, následně se s využitím filtru vyberou pouze sudé prvky a v posledním kroku se z vyfiltrovaných prvků (je jich celkem padesát) vytvoří sekvence o pouhých deseti prvcích:

fn main() {
    // iterator (povsimnete si, ze je nemenny)
    let iter1 = 1..101;
    let iter2 = iter1.filter(|x| x % 2 == 0);
    let iter3 = iter2.take(10);
 
    // pruchod sekvenci se ziskanim indexu a hodnoty kazdeho prvku
    for (index,item) in iter3.enumerate() {
    println!("item[{}] = {}", index, item);
    }
}

Po překladu a spouštění se na standardní výstup vypíšou následující zprávy:

item[0] = 2
item[1] = 4
item[2] = 6
item[3] = 8
item[4] = 10
item[5] = 12
item[6] = 14
item[7] = 16
item[8] = 18
item[9] = 20

Funkce/metoda take bude pracovat správně i ve chvíli, kdy požadujeme větší počet prvků, než máme k dispozici. K žádnému překročení mezí nedojde:

fn main() {
    // iterator (povsimnete si, ze je nemenny)
    let iter1 = 1..101;
    let iter2 = iter1.filter(|x| x % 2 == 0);
    let iter3 = iter2.take(1000);

    // pruchod sekvenci se ziskanim indexu a hodnoty kazdeho prvku
    for (index,item) in iter3.enumerate() {
        println!("item[{}] = {}", index, item);
    }
}
item[0] = 2
item[1] = 4
item[2] = 6
item[3] = 8
item[4] = 10
item[5] = 12
item[6] = 14
...
...
...
item[41] = 84
item[42] = 86
item[43] = 88
item[44] = 90
item[45] = 92
item[46] = 94
item[47] = 96
item[48] = 98
item[49] = 100

Další zajímavou a užitečnou funkcí (metodou?) vyššího řádu, kterou lze použít pro zpracování sekvencí, je funkce take-while. Tato funkce se částečně podobá funkci take, která z předané sekvence získá prvních n prvků, ovšem funkce take-while namísto hodnoty n očekává predikát. Dokud predikát vrací hodnotu true, budou se prvky ze vstupní sekvence předávat do sekvence výstupní, při první hodnotě false je funkce ukončena. Podívejme se na jednoduchý příklad:

fn main() {
    let iter1 = 1..999;
    let iter2 = iter1.filter(|x| x % 2 == 0);
    let iter3 = iter2.take_while(|x| x*x < 200);
 
    // pruchod sekvenci se ziskanim indexu a hodnoty kazdeho prvku
    for (index,item) in iter3.enumerate() {
        println!("item[{}] = {}", index, item);
    }
}
item[0] = 2
item[1] = 4
item[2] = 6
item[3] = 8
item[4] = 10
item[5] = 12
item[6] = 14

13. Nekonečné sekvence a práce s nimi

V některých funkcionálních jazycích se setkáme s termíny nekonečná sekvence popř. „líná“ sekvence (lazy sekvence). Tyto termíny spolu úzce souvisí. Připomeňme si, že sekvencí je v programovacím jazyku Rust myšlen objekt typu „iterátor“, v němž je implementována metoda next, po jejímž zavolání se buď vrátí další prvek nebo se, pokud další prvek neexistuje, vrátí hodnota None (ve skutečnosti metoda next vrací hodnotu typu Option<?>, což však bude téma probírané příště). Pokud iterátor postupně vrací prvky z nějaké sekvence, znamená to, že po vrácení posledního prvku je sekvence ukončena. Ovšem může také existovat iterátor – a je to poměrně časté – který neustále vrací další a další hodnoty. Jeden takový iterátor můžeme získat snadno, protože i objekt typu range nemusí mít uvedenou horní mez. Zápis je sice poněkud zvláštní, ale plně funkční:

fn main() {
    let iter1 = 1..;
 
    // pruchod sekvenci se ziskanim indexu a hodnoty kazdeho prvku
    for (index,item) in iter1.enumerate() {
    println!("item[{}] = {}", index, item);
    }
}

Sami si vyzkoušejte příklad přeložit a spustit. Program se ukončí pomocí Ctrl+C nebo oblíbeným příkazem kill.

Nekonečné sekvence již tedy dokážeme vytvořit, ale co se s nimi dá dělat, když při pokusu o jejich zpracování dostaneme nekonečný počet hodnot? Ve skutečnosti tak tomu být nemusí, protože metody, které se sekvencemi pracují (map, filter atd.), ve skutečnosti nemusí volat jim předané (anonymní) funkce ihned, ale až ve chvíli, kdy je skutečně nutné vyčíslit hodnoty prvků. Lze tedy zpracovávat i nekonečné sekvence, ovšem za předpokladu, že se v řetězci zpracování, například na jeho konci, počet prvků nějakým způsobem omezí. Podívejme se na příklad:

fn main() {
    let iter1 = 1..;
    let iter2 = iter1.filter(|x| x % 2 == 0);
    let iter3 = iter2.take(10);
 
    // pruchod sekvenci se ziskanim indexu a hodnoty kazdeho prvku
    for (index,item) in iter3.enumerate() {
    println!("item[{}] = {}", index, item);
    }
}

V tomto příkladu vytvoříme nekonečnou a současně i „línou“ sekvenci představovanou objektem typu range. Všechny (!) prvky této sekvence jsou následně profiltrovány a z výsledné nekonečné „líné“ sekvence je vybráno prvních deset prvků. Na první pohled by se mělo jednat o nekonečný program, protože anonymní funkci pro vyfiltrování sudých hodnot zdánlivě aplikujeme na nekonečný počet prvků, ovšem vzhledem k tomu, že je vyhodnocení „líné“, je ve skutečnosti zmíněná anonymní funkce zavolána jen dvacetkrát (zastavení zajišťuje funkce/metoda take):

item[0] = 2
item[1] = 4
item[2] = 6
item[3] = 8
item[4] = 10
item[5] = 12
item[6] = 14
item[7] = 16
item[8] = 18
item[9] = 20

Samozřejmě lze použít i metodu take_while:

fn main() {
    let iter1 = 1..;
    let iter2 = iter1.filter(|x| x % 2 == 0);
    let iter3 = iter2.take_while(|x| x*x < 256);
 
    // pruchod sekvenci se ziskanim indexu a hodnoty kazdeho prvku
    for (index,item) in iter3.enumerate() {
    println!("item[{}] = {}", index, item);
    }
}

S následujícím výstupem:

item[0] = 2
item[1] = 4
item[2] = 6
item[3] = 8
item[4] = 10
item[5] = 12
item[6] = 14

Možné jsou i další kombinace, které ukazují, že i při zpracování nekonečných sekvencí nepotřebujeme ani nekonečnou paměť ani nekonečný čas:

    let iter1 = 1..;
    let iter2 = iter1.filter(|x| x % 2 == 0);
    let iter3 = iter2.map(|x| x * x );
    let iter4 = iter3.take(20);

    // pruchod sekvenci se ziskanim indexu a hodnoty kazdeho prvku
    for (index,item) in iter4.enumerate() {
        println!("item[{}] = {}", index, item);
    }
}

Výsledkem je řada čtverců sudých hodnot:

item[0] = 4
item[1] = 16
item[2] = 36
item[3] = 64
item[4] = 100
item[5] = 144
item[6] = 196
item[7] = 256
item[8] = 324
item[9] = 400
item[10] = 484
item[11] = 576
item[12] = 676
item[13] = 784
item[14] = 900
item[15] = 1024
item[16] = 1156
item[17] = 1296
item[18] = 1444
item[19] = 1600

Ne všechny metody je však možné na nekonečné sekvence aplikovat. Například dříve popsaná metoda rev je aplikovatelná pouze na sekvence konečné, což je hlídáno při překladu:

fn main() {
    let iter1 = 1..;
    let iter2 = iter1.filter(|x| x % 2 == 0);
    let iter3 = iter2.rev();
    let iter4 = iter3.take_while(|x| x*x < 256);
 
    // pruchod sekvenci se ziskanim indexu a hodnoty kazdeho prvku
    for (index,item) in iter3.enumerate() {
        println!("item[{}] = {}", index, item);
    }
}

Překlad se v tomto případe nepodaří, a to z toho důvodu, že sekvencí není možné iterovat pozpátku:

error[E0277]: the trait bound `std::ops::RangeFrom<{integer}>: std::iter::DoubleEndedIterator` is not satisfied
 --> 46_iter16.rs:4:23
  |
4 |     let iter3 = iter2.rev();
  |                       ^^^
  |
  = note: required because of the requirements on the impl of `std::iter::DoubleEndedIterator` for `std::iter::Filter<std::ops::RangeFrom<{integer}>, [closure@46_iter16.rs:3:30: 3:44]>`

14. Funkce fold: postupné zpracování prvků sekvence

Poslední funkcí vyššího řádu, která nesmí chybět v repertoáru žádné funkcionálně zaměřené knihovny (a tím pádem ani v repertoáru metod iterátorů), je funkce pojmenovaná reduce popř. fold. Názvy těchto funkcí naznačují jejich účel – dochází k postupné redukci prvků uložených v sekvenci, a to (postupnou) aplikací zvolené uživatelské funkce na jednotlivé prvky a po krocích počítaný mezivýsledek. V programovacím jazyku Rust se příslušná metoda, která toto chování zajišťuje, jmenuje fold a akceptuje dva parametry – počáteční hodnotu „akumulátoru“ (mezivýsledku) a uzávěr, což je zjednodušeně řečeno funkce (typicky anonymní), která na sebe má navázánu alespoň jednu volnou proměnnou. Tento uzávěr je postupně volán se dvěma argumenty – aktuální hodnotou akumulátoru (mezivýsledku) a prvkem získaným ze vstupní sekvence. Akumulátor je po každém volání modifikován tak, že obsahuje hodnotou vrácenou uzávěrem.

Podívejme se na příklad, v němž sečteme všechny prvky zvolené sekvence. Tato sekvence obsahuje prvky s hodnotou 2, 4, 6 .. 20. Při postupně prováděném součtu je mezivýsledek nejprve nastaven na nulu, takže poprvé je anonymní funkce/uzávěr |sum, x| sum + x volána s parametry sum=0 a x=2, podruhé s parametry sum=2, x=4 atd.:

fn main() {
    let iter1 = 1..;
    let iter2 = iter1.filter(|x| x % 2 == 0);
    let iter3 = iter2.take(10);
    let suma  = iter3.fold(0, |sum, x| sum + x);
 
    println!("sum = {}", suma);
}

Po spuštění tohoto příkladu by se na standardním výstupu měla objevit jediná hodnota 110:

sum = 110

Nepatrně složitější příklad slouží pro výpočet tabulky faktoriálů. Zde si povšimněte, že počáteční hodnota mezivýsledku (akumulátoru) je nastavena na jedničku. Taktéž stojí za povšimnutí, že při konstrukci objektu typu range můžeme použít proměnné, nikoli nutně konstanty:

CS24_early

fn main() {
    for n in 1..10 {
    let fact = (1..n+1).fold(1, |prod, x| prod * x);
    println!("{}! = {}", n, fact);
    }
}

Výsledkem tohoto programu bude tabulka s faktoriály prvních devíti přirozených čísel:

1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
9! = 362880

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

Všechny dnes ukázané demonstrační příklady, resp. přesněji řečeno jejich bezchybné varianty, byly 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ž objemný) repositář:

Příklad Odkaz
29_func_params.rs https://github.com/tisnik/pre­sentations/blob/master/rus­t/29_func_params.rs
30_func_params.rs https://github.com/tisnik/pre­sentations/blob/master/rus­t/30_func_params.rs
31_iter1.rs https://github.com/tisnik/pre­sentations/blob/master/rus­t/31_iter1.rs
32_iter2.rs https://github.com/tisnik/pre­sentations/blob/master/rus­t/32_iter2.rs
33_iter3.rs https://github.com/tisnik/pre­sentations/blob/master/rus­t/33_iter3.rs
34_iter4.rs https://github.com/tisnik/pre­sentations/blob/master/rus­t/34_iter4.rs
35_iter5.rs https://github.com/tisnik/pre­sentations/blob/master/rus­t/35_iter5.rs
36_iter6.rs https://github.com/tisnik/pre­sentations/blob/master/rus­t/36_iter6.rs
37_iter7.rs https://github.com/tisnik/pre­sentations/blob/master/rus­t/37_iter7.rs
38_iter8.rs https://github.com/tisnik/pre­sentations/blob/master/rus­t/38_iter8.rs
39_iter9.rs https://github.com/tisnik/pre­sentations/blob/master/rus­t/39_iter9.rs
40_iter10.rs https://github.com/tisnik/pre­sentations/blob/master/rus­t/40_iter10.rs
41_iter11.rs https://github.com/tisnik/pre­sentations/blob/master/rus­t/41_iter11.rs
42_iter12.rs https://github.com/tisnik/pre­sentations/blob/master/rus­t/42_iter12.rs
43_iter13.rs https://github.com/tisnik/pre­sentations/blob/master/rus­t/43_iter13.rs
44_iter14.rs https://github.com/tisnik/pre­sentations/blob/master/rus­t/44_iter14.rs
45_iter15.rs https://github.com/tisnik/pre­sentations/blob/master/rus­t/45_iter15.rs
46_iter16.rs https://github.com/tisnik/pre­sentations/blob/master/rus­t/46_iter16.rs
47_iter17.rs https://github.com/tisnik/pre­sentations/blob/master/rus­t/47_iter17.rs
48_iter18.rs https://github.com/tisnik/pre­sentations/blob/master/rus­t/48_iter18.rs

16. Odkazy na Internetu

  1. Rust Compare: Pointers & References
    http://www.rust-compare.com/site/pointers.html
  2. Rust Compare: Parameters
    http://www.rust-compare.com/site/params.html
  3. Why does this compile? Automatic dereferencing?
    https://users.rust-lang.org/t/why-does-this-compile-automatic-dereferencing/2183
  4. Understanding Pointers, Ownership, and Lifetimes in Rust
    http://koerbitz.me/posts/Understanding-Pointers-Ownership-and-Lifetimes-in-Rust.html
  5. Rust lang series episode #25 — pointers (#rust-series)
    https://steemit.com/rust-series/@jimmco/rust-lang-series-episode-25-pointers-rust-series
  6. Rust – home page
    https://www.rust-lang.org/en-US/
  7. Rust – Frequently Asked Questions
    https://www.rust-lang.org/en-US/faq.html
  8. The Rust Programming Language
    https://doc.rust-lang.org/book/
  9. Rust (programming language)
    https://en.wikipedia.org/wi­ki/Rust_%28programming_lan­guage%29
  10. Go – home page
    https://golang.org/
  11. Stack Overflow – Most Loved, Dreaded, and Wanted language
    https://stackoverflow.com/re­search/developer-survey-2016#technology-most-loved-dreaded-and-wanted
  12. 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/
  13. Rust vs Go: My experience
    https://www.reddit.com/r/go­lang/comments/21m6jq/rust_vs_go_my_ex­perience/
  14. Friends of Rust (Organizations running Rust in production)
    https://www.rust-lang.org/en-US/friends.html
  15. Rust programs versus C++ g++
    https://benchmarksgame.ali­oth.debian.org/u64q/compa­re.php?lang=rust&lang2=gpp
  16. Další benchmarky (nejedná se o reálné příklady „ze života“)
    https://github.com/kostya/benchmarks
  17. Go na Redditu
    https://www.reddit.com/r/golang/
  18. Rust vs. Go
    http://vschart.com/compare/rust/vs/go-language
  19. Abstraction without overhead: traits in Rust
    https://blog.rust-lang.org/2015/05/11/traits.html
  20. Functional Programming in Rust – Part 1 : Function Abstraction
    http://blog.madhukaraphatak­.com/functional-programming-in-rust-part-1/
  21. 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
  22. Chytré ukazatele (moderní verze jazyka C++) [MSDN]
    https://msdn.microsoft.com/cs-cz/library/hh279674.aspx
  23. UTF-8 Everywhere
    http://utf8everywhere.org/
  24. Rust by Example
    http://rustbyexample.com/
  25. Rust oficiálně ve Fedoře
    https://mojefedora.cz/rust-oficialne-ve-fedore/
  26. Resource acquisition is initialization
    https://en.wikipedia.org/wi­ki/Resource_acquisition_is_i­nitialization
  27. TIOBE index (October 2016)
    http://www.tiobe.com/tiobe-index/
  28. 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
  29. String Types in Rust
    http://www.suspectsemantic­s.com/blog/2016/03/27/str­ing-types-in-rust/
  30. Trait (computer programming)
    https://en.wikipedia.org/wi­ki/Trait_%28computer_program­ming%29
  31. Type inference
    https://en.wikipedia.org/wi­ki/Type_inference

Byl pro vás článek přínosný?

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.