Rust: funkce, lambda výrazy a rozhodovací konstrukce match

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

Sdílet

 Autor: Rust project
Dnes se budeme věnovat třem důležitým jazykovým konstrukcím. Jedná se o funkce, lambda výrazy tvořící základ pro uzávěry a o konstrukci match nahrazující switch-case.

Obsah

1. Programovací jazyk Rust – funkce, lambda výrazy a rozhodovací konstrukce match

2. Deklarace funkce bez parametrů i bez návratové hodnoty

3. Deklarace funkce s parametry, ale bez návratové hodnoty

4. Návratové hodnoty funkce

5. Funkce bez návratového bodu

6. Funkce jako plnohodnotný datový typ

7. Funkce vyššího řádu

8. Lambda výrazy

9. Typy parametrů v lambda výrazech

10. Lambda výrazy přiřazené do proměnné

11. Rozhodovací konstrukce match

12. Konstrukce match ve funkci výrazu (ne příkazu)

13. Jednoduché vzory v konstrukci match

14. Specifikace rozsahu

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

16. Odkazy na Internetu

1. Programovací jazyk Rust – funkce, lambda výrazy a rozhodovací konstrukce match

„I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.“
C.A.R. Hoare, InfoQ Null References: The Billion Dollar Mistake

V úvodní části seriálu o programovacím jazyku Rust jsme si, prozatím ovšem bez větších podrobností, řekli, z jakého důvodu tento relativně mladý jazyk vlastně vznikl a pro které typy aplikací může být výhodné ho použít. Připomeňme si jen krátce, že se jedná o jazyk určený pro překlad do (nativního) strojového kódu s využitím LLVM backendu, přičemž cílem je vytvořit takový výsledný strojový kód, který by byl jak z hlediska spotřeby paměti, tak i z hlediska výkonu srovnatelný s výsledkem produkovaným překladači jazyků C či C++. Programovací jazyk Rust je však zároveň navržen s ohledem na tvorbu bezpečnějších aplikací i s důrazem na použití paralelismu. Dnes se poněkud podrobněji seznámíme se třemi rysy tohoto programovacího jazyka – s funkcemi tvořícími základ modulárních aplikací, s lambda výrazy (v tento okamžik ovšem bez vysvětlení uzávěrů) a taktéž s rozhodovací konstrukcí match, která v Rustu nahradila primitivnější a mnohdy nevyhovující konstrukci switch-case.

Poznámka: všechny následující příklady je možné přeložit překladačem nazvaným rustc, který se spouští klasicky z příkazové řádky. Žádné další podpůrné nástroje (cargo apod.) prozatím nebudete potřebovat. Chybová hlášení, s nimiž se taktéž setkáme, se mohou nepatrně lišit v závislosti na aktuálně nainstalované verzi Rustu.

Poznámka2: v této části se některým konstrukcím věnuje poměrně velké množství prostoru, včetně ukázky typických chyb, s nimiž se může programátor přecházející na Rust setkat. Prosím čtenáře o vyjádření v diskuzi, jestli je lepší se v jednom článku věnovat více vlastnostem (méně podrobně) či zda tato forma vyhovuje.

2. Deklarace funkce bez parametrů i bez návratové hodnoty

Začněme skutečně velmi jednoduchými příklady. V následujícím úryvku kódu můžeme vidět deklaraci dvou funkcí nazvaných foo a main. Deklarace funkce začíná klíčovým slovem fn (takže se neupíšete tak, jako například v JavaScriptu). Za tímto klíčovým slovem následuje jméno funkce, případná specifikace typu u generických funkcí (zapsáno v úhlových závorkách), seznam a typy parametrů umístěných v kulatých závorkách, případná specifikace návratového typu a konečně blok, v němž je zapsáno tělo funkce. Jak je patrné z úryvku kódu, ani jedna z deklarovaných funkcí neakceptuje žádné parametry a dokonce ani nemá žádnou návratovou hodnotu. Z tohoto důvodu je deklarace zjednodušena a v kulatých závorkách se žádná jména a typy parametrů neuvádí, dokonce se neuvádí ani návratový typ funkce:

fn foo() {
    println!("foo");
}
 
fn main() {
    foo();
}

Kulaté závorky nelze vynechat, což hlídá překladač:

fn foo {
    println!("foo");
}
 
fn main() {
    foo();
}
error: expected one of `(` or `<`, found `{`
 --> 15_void_function.rs:1:8
  |
1 | fn foo {
  |        ^
 
error: aborting due to previous error

Taktéž je striktně hlídáno, aby se funkci bez parametrů skutečně nic nepředávalo:

fn foo() {
    println!("foo");
}
 
fn main() {
    foo(42);
}
error[E0061]: this function takes 0 parameters but 1 parameter was supplied
 --> 15_void_function.rs:6:5
  |
6 |     foo(42);
  |     ^^^^^^^ expected 0 parameters

error: aborting due to previous error

Poznámka: pozor na to, že v céčku znamená zápis () něco jiného, konkrétně funkci s nespecifikovaným počtem parametrů (zde je Rust blíže k C++):

void foo() {
}
 
int main(void) {
    foo(42);
    return 0;
}

3. Deklarace funkce s parametry, ale bez návratové hodnoty

Nyní se podívejme na nepatrně složitější příklad, v němž jsou deklarovány čtyři funkce. První funkce neakceptuje žádné parametry (opět je to hlídáno překladačem), druhá funkce akceptuje parametr typu celé 32bitové číslo se znaménkem a třetí funkce akceptuje dva parametry stejného typu, tedy opět celé 32bitové číslo se znaménkem. Povšimněte si způsobu zápisu, který se sice liší od specifikace typů v céčku či Javě, na stranu druhou je tento zápis konzistentní s deklarací proměnných apod.

fn foo() {
    println!("foo");
}
 
fn bar(argument:i32) {
    println!("{}", argument);
}
 
fn baz(argument1:i32, argument2:i32) {
    println!("{} {}", argument1, argument2);
}
 
fn main() {
    foo();
    bar(42);
    baz(1, 2);
}

Již v době překladu je hlídáno, zda jsou funkce volány s parametry správných typů. Pokud příklad pozměníme takovým způsobem, že funkce bar a baz budou akceptovat celá čísla bez znaménka a předáme těmto funkcím záporné hodnoty, nebude překlad úspěšný:

fn foo() {
    println!("foo");
}
 
fn bar(argument:u32) {
    println!("{}", argument);
}
 
fn baz(argument1:u32, argument2:u32) {
    println!("{} {}", argument1, argument2);
}
 
fn main() {
    foo();
    bar(-42);
    baz(-1, 2);
}

Při pokusu o překlad takto upraveného příkladu nebude překladač rustc příliš spokojen s předloženým zdrojovým kódem, což dá najevo barevným chybovým hlášením, z kterého je zřejmé, kde chyba nastala (v článku nejsou barvy vidět, v praxi však zvyšují čitelnost a přehlednost):

error[E0080]: constant evaluation error
  --> 16_typed_arguments.rs:15:9
   |
15 |     bar(-42);
   |         ^^^ unary negation of unsigned integer
 
error[E0080]: constant evaluation error
  --> 16_typed_arguments.rs:16:9
   |
16 |     baz(-1, 2);
   |         ^^ unary negation of unsigned integer

error: aborting due to 2 previous errors

Překladač se nenechá zmást ani tehdy, pokud je funkci předávána proměnná nesprávného typu:

fn foo() {
    println!("foo");
}
 
fn bar(argument:u32) {
    println!("{}", argument);
}
 
fn baz(argument1:i32, argument2:i32) {
    println!("{} {}", argument1, argument2);
}
 
fn main() {
    foo();
    let x:i32 = -42;
    bar(x);
    baz(1, 2);
}
error[E0308]: mismatched types
  --> 16_typed_arguments.rs:16:9
   |
16 |     bar(x);
   |         ^ expected u32, found i32
 
error: aborting due to previous error

4. Návratové hodnoty funkce

Jak se deklarují funkce s parametry již víme, takže se ještě musíme seznámit se způsobem deklarace funkce, která vrací nějakou hodnotu. Typ návratové hodnoty se specifikuje za šipkou → zapsanou po seznamu parametrů, ale ještě před blokem obsahujícím tělo funkce:

fn foo() -> i32 {
    42
}
 
fn bar(argument:i32) -> i32 {
    return argument * 2;
}
 
fn baz(argument:i32) -> i32 {
    argument * 2
}
 
fn main() {
    println!("{}", foo());
    println!("{}", bar(21));
    println!("{}", baz(21));
}

Povšimněte si dvou zajímavostí. Funkce, která vrací hodnotu, může být zapsána dvěma způsoby:

  1. Blok tvořící tělo kódu obsahuje jeden příkaz return výraz či více příkazů return výraz, které mohou být umístěny v podmínce atd. Vzhledem k tomu, že se jedná o příkaz, je za ním uveden středník.
  2. Blok může končit výrazem, za kterým se nepíše středník. Celý blok je totiž v jazyce Rust považován za plnohodnotný výraz vracející hodnotu (to je podobné LISPovským jazykům). Tento zápis by měl být preferovaný, konkrétně funkce bar není zapsána idiomaticky.

Opět si otestujme, co se stane v případě, že uděláme chybu při specifikaci typů parametrů a návratového typu funkce. Následující příklad byl nepatrně upraven tak, že funkce bar a baz očekávají 32bitová celá čísla se znaménkem (signed), ovšem typ návratových hodnot je odlišný – taktéž 32bitová celá čísla, ovšem bez znaménka (unsigned):

fn foo() -> i32 {
    42
}
 
fn bar(argument:i32) -> u32 {
    return argument * 2;
}
 
fn baz(argument:i32) -> u32 {
    argument * 2
}
 
fn main() {
    println!("{}", foo());
    println!("{}", bar(21));
    println!("{}", baz(21));
}

V některých jiných jazycích by tato deklarace byla zcela korektní (což zde znamená, že by se chyba projevila až v čase běhu), zatímco překladač Rustu vypíše již v čase překladu chybová hlášení:

error[E0308]: mismatched types
 --> 17_return_value.rs:6:12
  |
6 |     return argument * 2;
  |            ^^^^^^^^^^^^ expected u32, found i32
 
error[E0308]: mismatched types
  --> 17_return_value.rs:10:5
   |
10 |     argument * 2
   |     ^^^^^^^^^^^^ expected u32, found i32

error: aborting due to 2 previous errors

Druhá chyba je méně nápadná a ukazuje, co se stane ve chvíli, kdy zapomeneme, že pokud blok těla funkce končí výrazem, nesmí za ním být středník:

fn foo() -> i32 {
    42;
}
 
fn bar(argument:i32) -> i32 {
    return argument * 2;
}
 
fn baz(argument:i32) -> i32 {
    argument * 2;
}
 
fn main() {
    println!("{}", foo());
    println!("{}", bar(21));
    println!("{}", baz(21));
}

I v tomto případě nás překladač pohlídá:

error[E0269]: not all control paths return a value
 --> 17_return_value.rs:1:1
  |
1 | fn foo() -> i32 {
  | ^
  |
help: consider removing this semicolon:
 --> 17_return_value.rs:2:7
  |
2 |     42;
  |       ^
 
error[E0269]: not all control paths return a value
  --> 17_return_value.rs:9:1
   |
9  | fn baz(argument:i32) -> i32 {
   | ^
   |
help: consider removing this semicolon:
  --> 17_return_value.rs:10:17
   |
10 |     argument * 2;
   |                 ^
 
error: aborting due to 2 previous errors

Zkusme ještě jeden nefunkční příklad, tentokrát kombinující oba způsoby (což de facto znamená, že se poslední výraz nikdy nevyhodnotí):

fn baz(argument:i32) -> i32 {
    return 0;
    argument * 2
}

Nyní nastanou při překladu dvě chyby, přičemž druhá chyba současně způsobila i chybu první:

warning: unused variable: `argument`, #[warn(unused_variables)] on by default
 --> 17_return_value.rs:9:8
  |
9 | fn baz(argument:i32) -> i32 {
  |        ^^^^^^^^
 
warning: unreachable expression, #[warn(unreachable_code)] on by default
  --> 17_return_value.rs:11:5
   |
11 |     argument * 2
   |     ^^^^^^^^^^^^

5. Funkce bez návratového bodu

Určitou specialitou jsou funkce, z nichž se řízení programu již nikdy nevrátí (takže se striktně vzato vlastně o skutečné funkce nejedná). U těchto funkcí, které se v Rustu nazývají diverging functions, se namísto specifikace návratového typu používá zápis → !. Příkladem může být funkce foo, která volá knihovní makro panic!, jenž ukončí právě aktivní vlákno a zde tedy i celou aplikaci:

fn foo() -> ! {
    panic!("This function never returns!");
}
 
fn main() {
    foo();
}

Překlad proběhne v pořádku a při spuštění získáme následující chybové hlášení:

thread 'main' panicked at 'This function never returns!', 18_diverging_function.rs:2
note: Run with `RUST_BACKTRACE=1` for a backtrace.

Pokud budete postupovat podle výše vypsané nápovědy, získáme celý (zde dosti nečitelný) backtrace:

~/ $ export RUST_BACKTRACE=1;./18_diverging_function 
thread 'main' panicked at 'This function never returns!', 18_diverging_function.rs:2
stack backtrace:
   1:     0x7fa0a71100d9 - std::sys::backtrace::tracing::imp::write::hd4b54a4a2078cb15
   2:     0x7fa0a7112c1c - std::panicking::default_hook::_{{closure}}::h51a5ee7ba6a9fcef
   3:     0x7fa0a7112109 - std::panicking::default_hook::hf823fce261e27590
   4:     0x7fa0a7112748 - std::panicking::rust_panic_with_hook::h8d486474663979b9
   5:     0x7fa0a710b616 - std::panicking::begin_panic::h94e6fb50d7864116
   6:     0x7fa0a710b76f - _18_diverging_function::foo::haed231fc6b9e676c
   7:     0x7fa0a710b777 - _18_diverging_function::main::h3672be7d52a51206
   8:     0x7fa0a711a706 - __rust_maybe_catch_panic
   9:     0x7fa0a7111882 - std::rt::lang_start::hca48e539ce72a288
  10:     0x7fa0a710b7a9 - main
  11:     0x7fa0a6505f44 - __libc_start_main
  12:     0x7fa0a710b478 - <unknown>
  13:                0x0 - <unknown>

6. Funkce jako plnohodnotný datový typ

Funkce jsou v programovacím jazyku Rust považovány za plnohodnotný datový typ, podobně jako v mnoha funkcionálních jazycích. Díky tomu je možné funkce předávat jako parametry jiným funkcím, vracet funkce, ukládat funkce do proměnných (resp. přesněji řečeno vytvořit vazbu mezi funkcí a jménem proměnné) atd. Podívejme se prozatím na velmi jednoduchý příklad, v němž opět máme deklarovány nám již známé funkce foo, bar a baz. Tyto funkce jsou uloženy do proměnných f1, f2 a f3 a lze je přes tyto proměnné i zavolat:

fn foo() -> i32 {
    42
}
 
fn bar(argument:i32) -> i32 {
    return argument * 2;
}
 
fn baz(argument:i32) -> i32 {
    argument * 2
}
 
fn main() {
    let f1 = foo;
    let f2 = bar;
    let f3 = baz;
    println!("{}", f1());
    println!("{}", f2(21));
    println!("{}", f3(21));
}

Překlad a spuštění by měly proběhnout bez problémů:

42
42
42

Poznámka: podobné chování sice lze napodobit i v céčku použitím ukazatelů na funkce, ovšem hned v další části tohoto seriálu si ukážeme, že vlastnosti funkcí v Rustu jsou poněkud odlišné, a to mj. i díky možnosti deklarace generických funkcí.

7. Funkce vyššího řádu

Programovací jazyk Rust sice není, na rozdíl od Haskellu a částečně i Clojure, čistě funkcionálním jazykem, nicméně i zde mohou hrát při vývoji aplikací velkou roli takzvané funkce vyššího řádu, tj. funkce, které jako své parametry akceptují jiné funkce popř. dokonce vrací (nové) funkce jako svoji návratovou hodnotu (o tom jsme se zmiňovali již v předchozí kapitole). Mezi dvě základní funkce vyššího řádu, které nalezneme prakticky ve všech jazycích, jež tento koncept nějakým způsobem podporují, patří funkce nazvané map a taktéž filter. Funkce map postupně aplikuje jí předanou funkci na jednotlivé prvky nějaké sekvence a vytváří tak sekvenci novou (modifikovanou).

Pro jednoduchost si ukažme, jak lze použít funkci map společně s objektem typu range, s nímž jsme se již seznámili minule. Nejprve vytvoříme sekvenci celých čísel od 0 do 9 a posléze na každý prvek této sekvence zavoláme funkci square. Výsledkem je nová sekvence, jejíž prvky jsou vypsány v programové smyčce typu for:

fn square(x:u32) -> u32 {
    x*x
}
 
fn main() {
    let sequence = 0..10;
    let squares = sequence.map(square);
    for n in squares {
        println!("{}", n);
    }
}

Po překladu a spuštění by se na standardní výstup měly vypsat prvky nově vypočtené sekvence:

0
1
4
9
16
25
36
49
64
81

Poznámka: sekvencemi se budeme podrobněji zabývat příště, protože se jedná o poměrně vhodný způsob, jak „funkcionálně“ pracovat s homogenními datovými strukturami. Navíc se díky tomu, že funkce map či filter (a několik dalších funkcí) vrací novou sekvenci (či přesněji řečeno iterátor), mohou tyto funkce vyššího řádu řetězit.

8. Lambda výrazy

Kromě pojmenovaných funkcí, které jsme si již představili v předchozích kapitolách, je možné v programovacím jazyku Rust použít i funkce anonymní, tj. funkce, které nejsou navázány na žádné jméno. Anonymní funkce jsou taktéž nazývány lambda výrazy (lambda expression), což má svůj původ v lambda kalkulu (to je však již téma přesahující možnosti dnešního článku :-). Označení „anonymní funkce“ poměrně přesně charakterizuje, o co se jedná – tyto funkce jednoduše nemají jméno. Předností je, že anonymní funkce lze předat ve formě parametru nějaké funkci vyššího řádu. To se hodí zejména ve chvíli, kdy se daná funkce používá jen na jednom místě v programu. Příkladem může být v předchozí kapitole uvedená funkce square, která se použije pouze jedenkrát a současně je její tělo tvořeno jednoduchým výrazem. Tuto funkci můžeme přepsat na funkci anonymní takto:

|x| x*x

V této formě zápisu se mezi znaky || vkládají jména parametrů a popř. jejich typy a za druhým znakem | následuje výraz tvořící tělo anonymní funkce.

Příklad použití odvozený od delšího příkladu z předchozí kapitoly:

fn main() {
    let sequence = 0..10;
    let squares = sequence.map(|x| x*x);
    for n in squares {
        println!("{}", n);
    }
}

Po překladu a spuštění by se na standardní výstup měly vypsat prvky nově vypočtené sekvence:

0
1
4
9
16
25
36
49
64
81

Ve skutečnosti nemusí být tělo anonymní funkce tvořeno jediným výrazem, protože můžeme využít faktu, že blok {} je v jazyku Rust považován za plnohodnotný výraz s nějakou výslednou hodnotou. Můžeme tedy napsat i:

fn main() {
    let sequence = 0..10;
    let squares = sequence.map(|x| {let result=x*x; result});
    for n in squares {
        println!("{}", n);
    }
}

Někdy se delší anonymní funkce rozepisují na více řádků:

fn main() {
    let sequence = 0..10;
    let squares = sequence.map(|x| {let result=x*x;
                                    result
                                   });
    for n in squares {
        println!("{}", n);
    }
}

9. Typy parametrů v lambda výrazech

Pozorného čtenáře asi napadlo, jak je vlastně možné, že anonymní funkce měla uvedeno pouze jméno argumentu a nikoli jeho typ. Totéž platí i o typu návratové hodnoty, ten nebyl dokonce pro jistotu specifikován vůbec. Ve skutečnosti je to (někdy) povoleno, protože se anonymní funkce použila v takovém kontextu, že překladač mohl odvodit, jakého typu parametr i návratová hodnota jsou. Ovšem v případě potřeby (ta ovšem nebude moc častá) lze typ parametru předepsat, a to následujícím způsobem:

fn main() {
    let sequence = -10..10;
    let squares = sequence.map(|x:i32| x*x);
    for n in squares {
        println!("{}", n);
    }
}

Překlad a spuštění proběhnou v pořádku:

100
81
64
49
36
25
16
9
4
1
0
1
4
9
16
25
36
49
64
81

Explicitně zadaný typ parametru/parametrů, pokud je tedy uveden, je samozřejmě hlídán, o čemž se můžeme snadno přesvědčit, když se anonymní funkci očekávající jen přirozená čísla pokusíme předat hodnotu zápornou:

fn main() {
    let sequence = -10..10;
    let squares = sequence.map(|x:u32| x*x);
    for n in squares {
        println!("{}", n);
    }
}

Kontrola je opět provedena již při překladu:

error[E0080]: constant evaluation error
 --> 22_typed_lambda_expression.rs:2:20
  |
2 |     let sequence = -10..10;
  |                    ^^^ unary negation of unsigned integer

error: aborting due to previous error

10. Lambda výrazy přiřazené do proměnné

Následující demonstrační příklad kombinuje některé koncepty, s nimiž jsme se seznámili v předchozích kapitolách. Jedná se o deklaraci anonymní funkce s uvedením typu parametru a o uložení funkce do proměnné (resp. o navázání funkce na jméno proměnné):

fn main() {
    let is_odd  = |x: i32| x & 1 == 1;
    //let is_even = |x: i32| !is_odd(x);
    let square  = |x: i32| x*x;
    for x in 0..10 {
        println!("{}*{}={}, {} is {} number", x, x, square(x), x, if is_odd(x) {"odd"} else {"even"})
    }
}

Tento příklad by měl po svém překladu a spuštění vypsat na standardní výstup následujících deset řádků:

0*0=0, 0 is even number
1*1=1, 1 is odd number
2*2=4, 2 is even number
3*3=9, 3 is odd number
4*4=16, 4 is even number
5*5=25, 5 is odd number
6*6=36, 6 is even number
7*7=49, 7 is odd number
8*8=64, 8 is even number
9*9=81, 9 is odd number

Poznámka: sice by se mohlo zdát, že není žádný rozdíl mezi deklarací pojmenované funkce a anonymní funkcí přiřazenou do proměnné (tím se vlastně funkce „pojmenuje“), ve skutečnosti je však možné svázat anonymní funkci se symbolem vytvořeným vně této funkce – jinými slovy je v Rustu možné tvořit uzávěry (closures).

11. Rozhodovací konstrukce match

Programovací jazyk Rust sice nepodporuje céčkovskou rozhodovací konstrukci typu switch-case (kterou převzala a jen mírně rozšířila Java atd.), což však ve skutečnosti není žádná škoda, protože namísto toho nabízí mnohem robustnější konstrukci match, kterou lze taktéž použít pro volbu některé větve kódu, jenž se má provést. Nejprve se bez dlouhých popisů podívejme na způsob zápisu této konstrukce v její nejjednodušší podobě, tedy při klasickém rozvětvení:

fn main() {
    let x:i32 = 1;

    match x {
        0 => println!("zero"),
        1 => println!("one"),
        2 => println!("two"),
        3 => println!("three"),
        _ => println!("something else"),
    }
}

Vidíme, že v konstrukci match je několik větví, přičemž každá větev začíná porovnávanou hodnotou a výraz či příkaz je uveden za šipkou tvořenou znaky =>. Poslední řádek obsahuje symbol _, který vlastně nahrazuje větev default. Ve skutečnosti je však sémantika tohoto symbolu poněkud odlišná, protože před šipkou ⇒ je uveden takzvaný vzorek (pattern) a nikoli pouhopouhá konstanta.

Ve skutečnosti překladač kontroluje, zda je _ použit, protože je vyžadováno, aby větve v konstrukci match pokryly všechny možné hodnoty testovaného výrazu (mám pocit, že současný překladač obsahuje chybu, protože pouze vyžaduje použití _ vždy, i když to s ohledem na předchozí větve není nutné). Zkusme přeložit následující příklad:

fn main() {
    let x:i32 = 1;

    match x {
        0 => println!("zero"),
        1 => println!("one"),
        2 => println!("two"),
        3 => println!("three"),
    }
}

Podle očekávání skončí pokus o překlad tohoto zdrojového kódu s chybou:

error[E0004]: non-exhaustive patterns: `_` not covered
 --> 24_match.rs:4:11
  |
4 |     match x {
  |           ^ pattern `_` not covered

error: aborting due to previous error

12. Konstrukce match ve funkci výrazu (ne příkazu)

Podobně jako tomu bylo o konstrukce if-else či u programového bloku {}, je i konstrukce match považována za výraz, nikoli za příkaz. To znamená, že match je možné použít v nějakém výpočtu, uvnitř jiného výrazu atd. V dalším příkladu je deklarována funkce nazvaná classify, které se předá celočíselná hodnota a funkce vrátí konstantní řetězec, který tuto hodnotu popisuje (podezřelému zápisu &'static str prosím prozatím věřte, popíšeme si ho později, protože řetězce jsou poměrně komplikované téma). Tato funkce obsahuje ve svém bloku jediný výraz a tím je match (proto ostatně za složenou závorkou není středník):

fn classify(x:i32) -> &'static str {
    match x {
        0 => "zero",
        1 => "one",
        2 => "two",
        3 => "three",
        _ => "something else",
    }
}
 
fn main() {
    for x in 0..10 {
        println!("{}:{}", x, classify(x))
    }
}

Po překladu a spuštění dostaneme na standardním výstupu tyto zprávy:

0:zero
1:one
2:two
3:three
4:something else
5:something else
6:something else
7:something else
8:something else
9:something else

Poznámka: někdy se setkáme s tím, že se match použije namísto konstrukce if-else. Skutečně se může jednat o čitelnější zápis, který vzdáleně připomíná funkci cond z LISPu:

fn factorial(n: u64) -> u64 {
    match n {
        0 => 1,
        _ => n * factorial(n-1)
    }
}

13. Jednoduché vzory v konstrukci match

Již jsme si naznačili, že před šipkou ⇒ se nemusí uvádět pouhé konstanty, s nimiž je porovnáván výraz předaný konstrukci match, ale mohou být použity nějaké obecně složitější vzory (pattern). Asi nejjednodušší vzor se tvoří pomocí znaku |, kterým je možné specifikovat několik hodnot, s nimiž je výsledek výrazu porovnáván. Příklad použití tohoto vzoru osvětlí:

fn classify(x:i32) -> &'static str {
    match x {
        0         => "zero",
        1 | 2     => "one or two",
        3 | 4 | 5 => "from three to five",
        _         => "something else",
    }
}
 
fn main() {
    for x in 0..10 {
        println!("{}:{}", x, classify(x))
    }
}

Po spuštění by se měl vypsat tento text:

0:zero
1:one or two
2:one or two
3:from three to five
4:from three to five
5:from three to five
6:something else
7:something else
8:something else
9:something else

Vidíme, že tímto zápisem můžeme nahradit céčkovskou konstrukci:

switch (x) {
    case 0: return "zero";
    case 1:
    case 2: return "one or two";
    ...
    ...
    ...

14. Specifikace rozsahu

V programové konstrukci match je možné použít i specifikaci rozsahu, tj. testu, zda kontrolovaná hodnota leží v zadaných mezích. Tyto meze se zapisují s využitím trojice teček, což je odlišné od zápisu dvou teček používaných při konstrukci objektu typu range. Dalším podstatným rozdílem mezi zápisem mezí a objektem typu range je fakt, že meze jsou vždy kontrolovány včetně obou krajních hodnot. To konkrétně znamená, že v dalším příkladu se text „from three to five“ vypíše pro vstupní hodnoty 3, 4 a 5:

fn classify(x:i32) -> &'static str {
    match x {
        0         => "zero",
        1 | 2     => "one or two",
        3 ... 5   => "from three to five",
        10 ... 20 => "from ten to twenty",
        _         => "something else",
    }
}
 
fn main() {
    for x in 0..20 {
        println!("{}:{}", x, classify(x))
    }
}

Příklad si po překladu samozřejmě můžeme jednoduše otestovat:

0:zero
1:one or two
2:one or two
3:from three to five
4:from three to five
5:from three to five
6:something else
7:something else
8:something else
9:something else
10:from ten to twenty
11:from ten to twenty
12:from ten to twenty
13:from ten to twenty
14:from ten to twenty
15:from ten to twenty
16:from ten to twenty
17:from ten to twenty
18:from ten to twenty
19:from ten to twenty

Překladač programovacího jazyka Rust kontroluje, zda se omylem namísto trojice teček … (meze) nezapsal konstruktor objektu range dvojicí teček. Následující příklad se tedy nepřeloží korektně kvůli zvýrazněnému řádku:

fn classify(x:i32) -> &'static str {
    match x {
        0         => "zero",
        1 | 2     => "one or two",
        3 ... 5   => "from three to five",
        10 .. 20 => "from ten to twenty",
        _         => "something else",
    }
}
 
fn main() {
    for x in 0..20 {
        println!("{}:{}", x, classify(x))
    }
}

Při pokusu o překlad se vypíše následující chybové hlášení:

ict ve školství 24

error: expected one of `...`, `=>`, `if`, or `|`, found `..`
 --> 27_match_range.rs:6:12
  |
6 |         10 .. 20 => "from ten to twenty",
  |            ^^
 
error: aborting due to previous error

Poznámka: podobná konstrukce sice není dostupná ve standardním céčku, ale lze ji nalézt například v rozšíření překladače GCC (a možná i dalších překladačů). Daní ovšem bude nekompatibilita zdrojového kódu se standardem.

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ář:

16. Odkazy na Internetu

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

Autor článku

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