Obsah
1. Reakce na chyby v programovacím jazyku Rust
2. Když nelze vrátit žádnou rozumnou hodnotu: datový typ Option
3. Oznámení o chybě formou návratové hodnoty: datový typ Result
4. Příklad použití datového typu Result
5. Predikáty is_ok() a is_err()
6. Datový typ Result a pattern matching
7. Kontrola, zda jsou pokryty obě větve v konstrukci match
8. Znak „_“ použitý v konstrukci match
10. Funkce vyššího řádu map_err
11. Více variant chybových hlášení
12. Použití výčtu pro chybová hlášení
13. Repositář s demonstračními příklady
1. Reakce na chyby v programovacím jazyku Rust
V předchozí části seriálu o programovacím jazyku Rust jsme se seznámili s užitečným datovým typem Option, který se velmi často používá v těch situacích, v nichž je zapotřebí vrátit (či naopak předat či alespoň reprezentovat) nějakou speciální hodnotu. Příklad použití jsme si vysvětlili na velmi jednoduché funkci, která vydělí dvě celá čísla a vrátí celočíselný podíl „zabalený“ právě v datovém typu Option. Ovšem ve chvíli, kdy je druhé číslo nulové, vrací se namísto zabaleného výsledku speciální hodnota None, takže volající funkce může a někdy dokonce musí kdykoli zjistit, že namísto skutečného podílu získala tuto speciální hodnotu.
2. Když nelze vrátit žádnou rozumnou hodnotu: datový typ Option
Funkce pro celočíselné dělení vypadala následovně:
fn div(x: i32, y: i32) -> Option<i32> { if y != 0 { Some(x/y) } else { None } }
Jednou z největších předností datového typu Option je fakt, že jeho používání je v programovacím jazyku Rust do značné míry standardní a navíc idiomatické, takže programátoři nemusí hledat, která „magická konstanta“ je pro danou funkci použita. Dále je zaručeno, že pokud budeme chtít získat zabalenou hodnotu přes pattern matching, bude nutné explicitně použít i druhou větev pracující s None. Pro hodnoty typu Option je navíc možné volat různé více či méně užitečné metody, například is_none(), is_some(), expect(), unwrap(), and_then(), or_else() a různé varianty funkce vyššího řádu map(). Mimochodem – tato struktura se používá i v případě, že potřebujeme pracovat s referencemi, které v některých situacích nemusí existovat (což nám jinak Rust nedovolí).
3. Oznámení o chybě formou návratové hodnoty: datový typ Result
V mnoha případech však nemusí být použití datového typu Option tím nejlepším řešením. Pro příklad nemusíme chodit daleko – předpokládejme, že budeme chtít, aby naše funkce pro dělení celých čísel vracela v případě pokusu o dělení nulou chybové hlášení a nikoli nicneříkající hodnotu None. K tomuto účelu se v programovacím jazyku Rust používá datová struktura nazvaná příhodně Result. Tato datová struktura se podobá Option, ovšem s tím rozdílem, že obaluje buď výsledek (třeba návratovou hodnotu volané funkce) nebo informaci o chybě. Deklarace struktury Result vypadá následovně:
enum Result<T, E> { Ok(T), Err(E), }
což se liší od deklarace typu Option:
enum Option<T> { None, Some(T), }
Povšimněte si, že se u datové struktury Result specifikují dva typy – typ návratové hodnoty T a typ hodnoty reprezentující chybu E. To je poměrně užitečná vlastnost, protože se programátoři mohou sami rozhodnout, jakým způsobem budou reprezentovat chybu – zda se bude jednat o jednoduchý řetězec či o složitější datovou strukturu, v ní může být například uloženo jméno otevíraného souboru a současně chybové hlášení systému při pokusu o jeho otevření.
4. Příklad použití datového typu Result
Zkusme si nyní upravit naši funkci určenou pro dělení dvou celých čísel takovým způsobem, aby se v případě dělení nulou namísto hodnoty None vracelo plnohodnotné chybové hlášení ve formě řetězce. Úprava je velmi snadná a může vypadat následovně:
fn div(x: i32, y: i32) -> Result<i32, &'static str> { if y != 0 { Ok(x/y) } else { Err("Divide by zero!") } }
Prozatím si vypočtené hodnoty vytiskneme jednoduše makrem println!() a formátovacím příkazem „?:“:
fn main() { let z1 = div(2, 1); println!("{:?}", z1); let z2 = div(2, 0); println!("{:?}", z2); }
Po spuštění tohoto příkladu se na prvním řádku vypíše vypočtená hodnota obalená do „Ok“ a na řádku druhém pak chybové hlášení, tentokrát obalené do „Err“:
Ok(2) Err("Divide by zero!")
Poznámka: v tomto případě se do Err uloží reference na statický řetězec, který je neměnitelný a který je součástí výsledného spustitelného binárního souboru. Konkrétně se tento řetězec nachází v sekci nazvané .rodata, která má nastavená práva Exec a Read, nikoli však Write (ostatně i proto se jedná o neměnitelný řetězec).
5. Predikáty is_ok() a is_err()
Zatímco u struktury Option bylo možné zjistit typ uložené hodnoty, tedy rozlišit mezi Some a None, pomocí predikátů is_some() a is_none(), používají se u datové struktury Result predikáty nazvané is_ok() a is_err(). Jejich použití je velmi jednoduché, jak je ostatně ukázáno na dalších dvou příkladech.
Použití predikátu is_ok()
fn div(x: i32, y: i32) -> Result<i32, &'static str> { if y != 0 { Ok(x/y) } else { Err("Divide by zero!") } } fn print_div_result(result: Result<i32, &'static str>) { if result.is_ok() { println!("Result is correct"); } else { println!("Result is incorrect"); } } fn main() { let z1 = div(2, 1); print_div_result(z1); let z2 = div(2, 0); print_div_result(z2); }
Po spuštění tohoto příkladu se vypíše:
Result is correct Result is incorrect
Použití predikátu is_err()
fn div(x: i32, y: i32) -> Result<i32, &'static str> { if y != 0 { Ok(x/y) } else { Err("Divide by zero!") } } fn print_div_result(result: Result<i32, &'static str>) { if result.is_err() { println!("Result is incorrect"); } else { println!("Result is correct"); } } fn main() { let z1 = div(2, 1); print_div_result(z1); let z2 = div(2, 0); print_div_result(z2); }
Po spuštění tohoto příkladu se vypíše:
Result is correct Result is incorrect
6. Datový typ Result a pattern matching
Ve skutečnosti se často namísto predikátů a čtení zabalené hodnoty či chybové zprávy používá pattern matching. Další příklad se nápadně podobá příkladu, který již známe z předchozí části seriálu:
fn div(x: i32, y: i32) -> Result<i32, &'static str> { if y != 0 { Ok(x/y) } else { Err("Divide by zero!") } } fn print_div_result(result: Result<i32, &'static str>) { match result { Ok(value) => println!("value: {}", value), Err(error) => println!("error: {}", error) } } fn main() { let z1 = div(2, 1); print_div_result(z1); let z2 = div(2, 0); print_div_result(z2); }
Vidíme, že je díky pattern matchingu v konstrukci match možné získat jak správnou hodnotu (pokud existuje), tak i hodnotu reprezentující chybu.
Výsledek běhu tohoto příkladu:
value: 2 error: Divide by zero!
Pro připomenutí a porovnání si ukažme, jak byl pattern matching využit společně s datovým typem Option:
fn div(x: i32, y: i32) -> Option<i32> { if y != 0 { Some(x/y) } else { None } } fn div_and_print(x: i32, y :i32) { let result = div(x, y); println!("{:?}", result); match result { None => println!("Divide by zero"), Some(val) => println!("{} / {} = {}", x, y, val), } println!(""); } fn main() { div_and_print(2, 1); div_and_print(2, 0); }
7. Kontrola, zda jsou pokryty obě větve v konstrukci match
O tom, že při pattern matchingu je v konstrukci match nutné pokrýt všechny možnosti (větve) jsme se již několikrát zmiňovali, takže si jen rychle zkontrolujme, zda to platí i pro strukturu Result. V následujícím příkladu naschvál vynecháme jednu z větví a zkontrolujeme průběh překladu:
fn div(x: i32, y: i32) -> Result<i32, &'static str> { if y != 0 { Ok(x/y) } else { Err("Divide by zero!") } } fn print_div_result1(result: Result<i32, &'static str>) { match result { //Ok(value) => println!("value: {}", value), Err(error) => println!("error: {}", error) } } fn print_div_result2(result: Result<i32, &'static str>) { match result { Ok(value) => println!("value: {}", value), //Err(error) => println!("error: {}", error) } } fn main() { let z1 = div(2, 1); print_div_result1(z1); let z2 = div(2, 0); print_div_result2(z2); }
Překladač podle očekávání obě chyby snadno nalezne a vypíše přehledná chybová hlášení:
error[E0004]: non-exhaustive patterns: `Ok(_)` not covered --> 215_result_patern_matching_coverage.rs:10:11 | 10 | match result { | ^^^^^^ pattern `Ok(_)` not covered error[E0004]: non-exhaustive patterns: `Err(_)` not covered --> 215_result_patern_matching_coverage.rs:17:11 | 17 | match result { | ^^^^^^ pattern `Err(_)` not covered error: aborting due to 2 previous errors
8. Znak „_“ použitý v konstrukci match
V konstrukci match je možné namísto vzoru použít i znak „_“, kterým je možné implementovat obdobu větve default známou z céčkových jazyků, ale i z Javy či JavaScriptu (konstrukce switch/case). Pokud tedy například nepotřebujeme získat přesnou informaci o chybě, která vznikla, můžeme náš skript upravit následujícím způsobem:
fn div(x: i32, y: i32) -> Result<i32, &'static str> { if y != 0 { Ok(x/y) } else { Err("Divide by zero!") } } fn print_div_result(result: Result<i32, &'static str>) { match result { Ok(value) => println!("value: {}", value), _ => println!("some error") } } fn main() { let z1 = div(2, 1); print_div_result(z1); let z2 = div(2, 0); print_div_result(z2); }
Výsledek běhu tohoto příkladu:
value: 2 some error
Pokud se vám pattern matching zalíbil (skutečně se jedná o velmi silnou část Rustu), je možné upravit i funkci div, například následovně:
fn div(x: i32, y: i32) -> Result<i32, &'static str> { match y { 0 => Err("Divide by zero!"), _ => Ok(x / y) } } fn print_div_result(result: Result<i32, &'static str>) { match result { Ok(value) => println!("value: {}", value), _ => println!("some error") } } fn main() { let z1 = div(2, 1); print_div_result(z1); let z2 = div(2, 0); print_div_result(z2); }
9. Funkce vyššího řádu map
Ve všech předchozích demonstračních příkladech jsme museli se strukturou Result pracovat dosti nešikovným způsobem – při každém přístupu bylo nutné kontrolovat, jestli je výsledek korektní (Ok) nebo chybový (Err), což by však v reálných programech bylo dosti nečitelné. Ve skutečnosti nám však struktura Result nabízí i mnohé další možnosti. Velmi užitečná je funkce map, která se aplikuje na výsledek, ovšem jen ve chvíli, kdy je výsledek korektní (Ok). Díky tomu se obejdeme bez nutnosti použití rozhodovací konstrukce či pattern matchingu. V následujícím příkladu vypíšeme výsledek dělení i výsledek zvýšený o jedničku. Povšimněte si, že do funkce map předáváme funkci akceptující celé číslo a vracející taktéž celé číslo (o rozbalení a zabalení výsledků se nemusíme vůbec starat):
fn div(x: i32, y: i32) -> Result<i32, &'static str> { if y != 0 { Ok(x/y) } else { Err("Divide by zero!") } } fn print_div_result(result: Result<i32, &'static str>) { match result { Ok(value) => println!("value: {}", value), Err(error) => println!("error: {}", error) } } fn inc(x: i32) -> i32 { x+1 } fn main() { let z0 = div(0, 1); print_div_result(z0); print_div_result(z0.map(inc)); let z1 = div(2, 1); print_div_result(z1); print_div_result(z1.map(inc)); let z2 = div(2, 0); print_div_result(z2); print_div_result(z2.map(inc)); }
Program bude plně funkční, a to i když použijeme map(inc) na výsledek dělení nulou:
value: 0 value: 1 value: 2 value: 3 error: Divide by zero! error: Divide by zero!
10. Funkce vyššího řádu map_err
Obdobou funkce map je funkce map_err. Asi jste již uhodli, že tato funkce vyššího řádu bude použita ve chvíli, kdy je ve struktuře Result uložena chybová hodnota, nikoli hodnota korektní. V následujícím příkladu budeme přes map_err překládat chybová hlášení do češtiny (tento příklad je poněkud umělý). Základem bude funkce translate transformující anglické řetězce:
fn div(x: i32, y: i32) -> Result<i32, &'static str> { if y != 0 { Ok(x/y) } else { Err("Divide by zero!") } } fn print_div_result(result: Result<i32, &'static str>) { match result { Ok(value) => println!("value: {}", value), Err(error) => println!("error: {}", error) } } fn translate(s: &'static str) -> &'static str { if s == "Divide by zero!" { "Deleni nulou" } else { "Neznama chyba" } } fn main() { let z0 = div(0, 1); print_div_result(z0); print_div_result(z0.map_err(translate)); let z1 = div(2, 1); print_div_result(z1); print_div_result(z1.map_err(translate)); let z2 = div(2, 0); print_div_result(z2); print_div_result(z2.map_err(translate)); }
Výsledek běhu tohoto programu:
value: 0 value: 0 value: 2 value: 2 error: Divide by zero! error: Deleni nulou
11. Více variant chybových hlášení
Díky tomu, že ve struktuře Result může být chybová hodnota reprezentována libovolným datovým typem (na rozdíl od Option), je možné pracovat s větším množstvím chybových hlášení. Můžeme si představit funkci pro otevření souboru, načtení jeho obsahu a následné zavření souboru – zde může dojít k celé řadě různých chyb (soubor nelze otevřít, lze ho otevřít, ale ne přečíst, při čtení může dojít k chybě/odpojení disku, soubor nemusí jít zavřít atd. atd.). Zkusme si nepatrně upravit naši funkci pro dělení tak, aby rozpoznala speciální případ 0/0. Úprava se dotkne i funkce translate:
fn div(x: i32, y: i32) -> Result<i32, &'static str> { if y != 0 { Ok(x/y) } else if x != 0 { Err("Divide by zero!") } else { Err("Zero/zero!") } } fn print_div_result(result: Result<i32, &'static str>) { match result { Ok(value) => println!("value: {}", value), Err(error) => println!("error: {}", error) } } fn translate(s: &'static str) -> &'static str { match s { "Divide by zero!" => "Deleni nulou", "Zero/zero!" => "Nula delena nulou", _ => "Neznama chyba" } } fn main() { let z0 = div(0, 1); print_div_result(z0); print_div_result(z0.map_err(translate)); let z1 = div(2, 1); print_div_result(z1); print_div_result(z1.map_err(translate)); let z2 = div(2, 0); print_div_result(z2); print_div_result(z2.map_err(translate)); let z3 = div(0, 0); print_div_result(z3); print_div_result(z3.map_err(translate)); }
Podívejme se na výsledek spuštění tohoto programu:
value: 0 value: 0 value: 2 value: 2 error: Divide by zero! error: Deleni nulou error: Zero/zero! error: Nula delena nulou
12. Použití výčtu pro chybová hlášení
Velmi často se můžeme setkat s programy, v nichž se pro reprezentaci chybové hodnoty používá namísto pouhých řetězců výčet (enum). Předností je snadnější práce s hodnotami typu výčet a možnost tisku jmen jednotlivých prvků výčtu v případě, že se použije #[derive(Debug)]. Ostatně podívejme se na příklad, v němž nejprve deklarujeme výčtový typ s dvojicí prvků (hodnot) a následně ho použijeme pro reprezentaci chybových stavů. Povšimněte si, že k jednotlivým prvkům/hodnotám se přistupuje s využitím operátoru „čtyřtečky“:
#[derive(Debug)] enum DivError { DivideByZero, DivideZeroByZero } fn div(x: i32, y: i32) -> Result<i32, DivError> { if y != 0 { Ok(x/y) } else if x != 0 { Err(DivError::DivideByZero) } else { Err(DivError::DivideZeroByZero) } } fn print_div_result(result: Result<i32, DivError>) { match result { Ok(value) => println!("value: {}", value), Err(error) => println!("error: {:?}", error) } } fn main() { let z0 = div(0, 1); print_div_result(z0); let z1 = div(2, 1); print_div_result(z1); let z2 = div(2, 0); print_div_result(z2); let z3 = div(0, 0); print_div_result(z3); }
Výsledek běhu tohoto programu:
value: 0 value: 2 error: DivideByZero error: DivideZeroByZero
13. Repositář s demonstračními příklady
Všechny dnes popisované demonstrační příklady byly, podobně jako ve všech předchozích částech tohoto seriálu, uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/presentations. Příklady si můžete v případě potřeby stáhnout i jednotlivě bez nutnosti klonovat celý repositář:
14. Odkazy na Internetu
- Enum std::result::Result
https://doc.rust-lang.org/std/result/enum.Result.html - Module std::result
https://doc.rust-lang.org/std/result/ - Result
http://rustbyexample.com/std/result.html - Rust stdlib: Option
https://doc.rust-lang.org/std/option/enum.Option.html - Module std::option
https://doc.rust-lang.org/std/option/index.html - Rust by example: option
http://rustbyexample.com/std/option.html - Rust by example: if-let
http://rustbyexample.com/flow_control/if_let.html - Rust by example: while let
http://rustbyexample.com/flow_control/while_let.html - Rust by example: Option<i32>
http://rustbyexample.com/std/option.html - An Overview of Macros in Rust
http://words.steveklabnik.com/an-overview-of-macros-in-rust - A Practical Intro to Macros in Rust 1.0
https://danielkeep.github.io/practical-intro-to-macros.html - The Rust Programming Language: macros
https://doc.rust-lang.org/beta/book/macros.html - Rust by example: 15 macro_rules!
http://rustbyexample.com/macros.html - Module std::vec
https://doc.rust-lang.org/nightly/std/vec/index.html - Primitive Type isize
https://doc.rust-lang.org/nightly/std/primitive.isize.html - Primitive Type usize
https://doc.rust-lang.org/nightly/std/primitive.usize.html - Primitive Type array
https://doc.rust-lang.org/nightly/std/primitive.array.html - Module std::slice
https://doc.rust-lang.org/nightly/std/slice/ - Rust by Example: 2.3 Arrays and Slices
http://rustbyexample.com/primitives/array.html - What is the difference between Slice and Array (stackoverflow)
http://stackoverflow.com/questions/30794235/what-is-the-difference-between-slice-and-array - Learning Rust With Entirely Too Many Linked Lists
http://cglab.ca/~abeinges/blah/too-many-lists/book/ - Testcase: linked list
http://rustbyexample.com/custom_types/enum/testcase_linked_list.html - Operators and Overloading
https://doc.rust-lang.org/book/operators-and-overloading.html - Module std::ops
https://doc.rust-lang.org/std/ops/index.html - Module std::cmp
https://doc.rust-lang.org/std/cmp/index.html - Trait std::ops::Add
https://doc.rust-lang.org/stable/std/ops/trait.Add.html - Trait std::ops::AddAssign
https://doc.rust-lang.org/std/ops/trait.AddAssign.html - Trait std::ops::Drop
https://doc.rust-lang.org/std/ops/trait.Drop.html - Trait std::cmp::Eq
https://doc.rust-lang.org/std/cmp/trait.Eq.html - Struct std::boxed::Box
https://doc.rust-lang.org/std/boxed/struct.Box.html - Explore the ownership system in Rust
https://nercury.github.io/rust/guide/2015/01/19/ownership.html - Rust's ownership and move semantic
http://www.slideshare.net/saneyuki/rusts-ownership-and-move-semantics - Trait std::marker::Copy
https://doc.rust-lang.org/stable/std/marker/trait.Copy.html - Trait std::clone::Clone
https://doc.rust-lang.org/stable/std/clone/trait.Clone.html - The Stack and the Heap
https://doc.rust-lang.org/book/the-stack-and-the-heap.html - Rust Compare: Pointers & References
http://www.rust-compare.com/site/pointers.html - Rust Compare: Parameters
http://www.rust-compare.com/site/params.html - Why does this compile? Automatic dereferencing?
https://users.rust-lang.org/t/why-does-this-compile-automatic-dereferencing/2183 - Understanding Pointers, Ownership, and Lifetimes in Rust
http://koerbitz.me/posts/Understanding-Pointers-Ownership-and-Lifetimes-in-Rust.html - Rust lang series episode #25 — pointers (#rust-series)
https://steemit.com/rust-series/@jimmco/rust-lang-series-episode-25-pointers-rust-series - Rust – home page
https://www.rust-lang.org/en-US/ - Rust – Frequently Asked Questions
https://www.rust-lang.org/en-US/faq.html - Destructuring and Pattern Matching
https://pzol.github.io/getting_rusty/posts/20140417_destructuring_in_rust/ - The Rust Programming Language
https://doc.rust-lang.org/book/ - Rust (programming language)
https://en.wikipedia.org/wiki/Rust_%28programming_language%29 - Go – home page
https://golang.org/ - Stack Overflow – Most Loved, Dreaded, and Wanted language
https://stackoverflow.com/research/developer-survey-2016#technology-most-loved-dreaded-and-wanted - 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/ - Rust vs Go: My experience
https://www.reddit.com/r/golang/comments/21m6jq/rust_vs_go_my_experience/ - Friends of Rust (Organizations running Rust in production)
https://www.rust-lang.org/en-US/friends.html - Rust programs versus C++ g++
https://benchmarksgame.alioth.debian.org/u64q/compare.php?lang=rust&lang2=gpp - Další benchmarky (nejedná se o reálné příklady „ze života“)
https://github.com/kostya/benchmarks - Go na Redditu
https://www.reddit.com/r/golang/ - Rust vs. Go
http://vschart.com/compare/rust/vs/go-language - Abstraction without overhead: traits in Rust
https://blog.rust-lang.org/2015/05/11/traits.html - Method Syntax
https://doc.rust-lang.org/book/method-syntax.html - Traits in Rust
https://doc.rust-lang.org/book/traits.html - Functional Programming in Rust – Part 1 : Function Abstraction
http://blog.madhukaraphatak.com/functional-programming-in-rust-part-1/ - 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 - Chytré ukazatele (moderní verze jazyka C++) [MSDN]
https://msdn.microsoft.com/cs-cz/library/hh279674.aspx - UTF-8 Everywhere
http://utf8everywhere.org/ - Rust by Example
http://rustbyexample.com/ - Rust oficiálně ve Fedoře
https://mojefedora.cz/rust-oficialne-ve-fedore/ - Resource acquisition is initialization
https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization - TIOBE index (October 2016)
http://www.tiobe.com/tiobe-index/ - Porovnání Go, D a Rustu na OpenHubu:
https://www.openhub.net/languages/compare?language_name[]=-1&language_name[]=-1&language_name[]=dmd&language_name[]=golang&language_name[]=rust&language_name[]=-1&measure=commits - String Types in Rust
http://www.suspectsemantics.com/blog/2016/03/27/string-types-in-rust/ - Trait (computer programming)
https://en.wikipedia.org/wiki/Trait_%28computer_programming%29 - Type inference
https://en.wikipedia.org/wiki/Type_inference