Obsah
1. Datový typ Option v programovacím jazyku Rust
2. Základní použití – funkce vracející hodnotu typu Option
6. Idiomatický kód – použití pattern matchingu
7. Použití řídicí struktury if let
8. Řídicí struktura if let ve výrazu
10. Řetězení operací – metoda Option.or()
11. Řetězení operací – metoda Option.and_then()
14. Repositář s demonstračními příklady
1. Datový typ Option v programovacím jazyku Rust
V programovacím jazyku Rust se poměrně často používá datový typ Option, a to ve chvílích, kdy je zapotřebí reprezentovat neznámou hodnotu, chybovou hodnotu (ovšem bez vyvolání výjimky), vytvořit funkci s volitelnými parametry či vytvořit typově bezpečnou obdobu odkazu typu null. Dnes si ukážeme některé možnosti, které tento datový typ programátorům nabízí (podobný typ ovšem nalezneme i v mnoha dalších programovacích jazycích).
Deklarace datového typu Option je ve skutečnosti velmi přímočará:
enum Option<T> { None, Some(T), }
Vidíme, že se jedná o výčtový typ s pouhými dvěma hodnotami None a Some, přičemž Some „obaluje“ vlastní hodnotu typu T, se kterou chceme pracovat.
Základní význam datového typu Option si můžeme ukázat na zcela typickém příkladu, který je mimochodem v poněkud upravené podobě použit i v dokumentaci programovacího jazyka Rust. V tomto příkladu je definovaná funkce div akceptující dva celočíselné parametry a vracející jejich podíl. Naivní implementace takové funkce by mohla vypadat následovně:
fn div(x: i32, y: i32) -> i32 { x/y }
Problém spočívá v tom, že tato funkce není dobře definována pro hodnotu y==0, což si lze snadno ověřit:
fn main() { println!("{}", div(2, 1)); println!("{}", div(2, 0)); }
S běhovou chybou:
2 thread 'main' panicked at 'attempt to divide by zero', test.rs:2 note: Run with `RUST_BACKTRACE=1` for a backtrace.
Výše uvedený problém je možné řešit různými způsoby, například vrácením nějaké „speciální“ předem vybrané celočíselné hodnoty nebo vyhozením výjimky. U speciální celočíselné hodnoty je však nutné dobře zdokumentovat, jak ji použít a samozřejmě tuto hodnotu nějak zvolit (-1?, MAX_INT?, MIN_INT?), popř. použít typ s větší šířkou (viz getc() v céčku). Výsledkem bude program plný podmínek testujících tuto speciální hodnotu.
2. Základní použití – funkce vracející hodnotu typu Option
Výhodnější je použít datový typ Option, neboť jeho použitím čtenářům našeho zdrojového kódu jasně naznačíme, že funkce v některých případech nemusí nic vracet (na tento datový typ se tedy můžeme dívat i jako na určitou formu samodokumentujícího se kódu). Funkci upravíme tak, že nebude vracet i32 (celé číslo), ale Option<i32>. V této struktuře může být buď ono celé číslo zabaleno (zavoláním Some(…) nebo je možné vrátit speciální hodnotu None:
fn div(x: i32, y: i32) -> Option<i32> { if y != 0 { Some(x/y) } else { None } }
Nejjednodušší způsob volání vypadá následovně (je nutné použít {:?}):
fn main() { let z1 = div(2, 1); println!("{:?}", z1); let z2 = div(2, 0); println!("{:?}", z2); }
S následujícím výsledkem:
Some(2) None
3. Metoda Option.unwrap()
V předchozím příkladu jsme si trošku zjednodušili práci, protože pro přečtení celočíselné hodnoty zabalené do typu Option jsme použili makro println!. Pokud však potřebujeme explicitně přistoupit k hodnotě uvnitř Option, je možné použít metodu Option.unwrap(), ovšem jen tehdy, pokud jsme si jisti, že nepracujeme s None (v případě, že by se zavolalo None.unwrap(), vyhodila by se výjimka, což si můžete snadno odzkoušet. Druhý příklad již explicitně rozlišuje mezi None a Some(i32), i když způsobem, který se příliš nepoužívá:
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); if result == None { println!("Divide by zero"); } else { println!("{} / {} = {}", x, y, result.unwrap()); } println!(""); } fn main() { div_and_print(2, 1); div_and_print(2, 0); }
Povšimněte si, že zde již můžeme celočíselnou hodnotu typu i32 vytisknout přímo formátovacím řetězcem {}:
println!("{} / {} = {}", x, y, result.unwrap());
Výsledek běhu programu:
Some(2) 2 / 1 = 2 None Divide by zero
4. Metoda Option.is_none()
Namísto testu proměnné či parametru typu Option na hodnotu None je možné použít přímo metodu Option.is_none() vracející pravdivostní hodnotu true ve chvíli, kdy je metoda skutečné volána s None; v opačném případě se vrací pravdivostní hodnota false. Příklad lze tedy nepatrně upravit takto:
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); if result.is_none() { println!("Divide by zero"); } else { println!("{} / {} = {}", x, y, result.unwrap()); } println!(""); } fn main() { div_and_print(2, 1); div_and_print(2, 0); }
Poznámka: tento zápis je jen o jediný znak kratší, než přímé porovnání a opět se nejedná o příliš idiomatický kód. Ten si představíme později.
5. Metoda Option.is_some()
Opakem metody Option.is_none() je podle očekávání metoda Option.is_some(), vracející true ve chvíli, kdy Option obaluje reálnou hodnotu a false ve chvíli, kdy se metoda volá nad None. Podmínku tedy můžeme velmi snadno otočit:
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); if result.is_some() { println!("{} / {} = {}", x, y, result.unwrap()); } else { println!("Divide by zero"); } println!(""); } fn main() { div_and_print(2, 1); div_and_print(2, 0); }
6. Idiomatický kód – použití pattern matchingu
V předchozích kapitolách jsem se několikrát zmínil o tom, že program, v němž se mezi Some(T) a None rozhodujeme pomocí řídicí struktury typu if-then-else, není v programovacím jazyku Rust považován za příliš idiomatický, a to i z toho důvodu, že se k obalené hodnotě musí přistupovat pomocí metody Option.unwrap(). Namísto toho se preferuje využití pattern matchingu umožňujícímu dostat se k obalené hodnotě vlastně zadarmo. Rozhodovací konstrukce může vypadat takto:
match proměnná_typu_Option { None => println!("Divide by zero"), Some(val) => println!("{} / {} = {}", x, y, val), }
Povšimněte si, že ve druhé větvi můžeme přímo pracovat s hodnotou val, aniž by se explicitně volala metoda Option.unwrap.
Použití v našem příkladu s dělením celých čísel může vypadat následovně:
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); }
Výsledek běhu programu:
Some(2) 2 / 1 = 2 None Divide by zero
7. Použití řídicí struktury if let
Poměrně často se můžeme setkat i s řídicí strukturou if let, která vlastně kombinuje podmínku s přiřazením, ovšem pouze při splnění této podmínky. Tuto strukturu lze doplnit i o blok else. Jedná se o speciální typ pattern matchingu, jenž lze v našem příkladu uplatnit následovně:
if let Some(val) = result { println!("{} / {} = {}", x, y, val); } else { println!("Divide by zero"); }
Pokud je podmínka splněna, je v prvním bloku možné používat hodnotu val, která byla původně obalena v proměnné result typu Option.
Celý příklad můžeme upravit následujícím způsobem:
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); if let Some(val) = result { println!("{} / {} = {}", x, y, val); } else { println!("Divide by zero"); } println!(""); } fn main() { div_and_print(2, 1); div_and_print(2, 0); }
8. Řídicí struktura if let ve výrazu
V programovacím jazyku Rust se ve skutečnosti může většina řídicích struktur použít i ve funkci výrazu, tj. ve chvíli, kdy očekáváme výslednou hodnotu. Podívejme se na poněkud upravený příklad, v němž funkce div_message() vydělí dvě čísla a následně vrátí řetězec (konkrétně typu String) obsahující buď informaci o obou vstupních operandech a výsledku podílu nebo hlášení o dělení nulou. Povšimněte si, že if let je skutečně výrazem – každá větev obsahuje jediný výraz (bez středníku na konci), jehož výsledná hodnota se stane návratovou hodnotou celé funkce:
fn div(x: i32, y: i32) -> Option<i32> { if y != 0 { Some(x/y) } else { None } } fn div_message(x: i32, y :i32) -> String { let result = div(x, y); if let Some(val) = result { format!("{} / {} = {}", x, y, val) } else { String::from("Divide by zero") } } fn main() { println!("{}", div_message(2, 1)); println!("{}", div_message(2, 0)); }
9. Řídicí struktura while let
Podobným způsobem je možné použít strukturu while let, kterou lze implementovat programovou smyčku, jež se zastaví ve chvíli, když se detekuje hodnota None. Poněkud umělý příklad by mohl vypadat takto:
fn div(x: i32, y: i32) -> Option<i32> { if y != 0 { Some(x/y) } else { None } } fn main() { let array = [div(2,1), div(3,2), div(1,0), div(1,1)]; let mut i = 0; while let Some(value) = array[i] { println!("{}", value); i = i + 1; } }
Častěji se setkáme s tímto příkladem, protože metoda Vec.pop() vrací (samozřejmě nikoli náhodou) právě typ Option s hodnotou None ve chvíli, kdy je již vektor prázdný:
fn main() { let mut v = vec![1, 2, 3, 4, 5]; while let Some(x) = v.pop() { println!("{}", x); } }
10. Řetězení operací – metoda Option.or()
V některých případech je výhodné namísto neustálého testování, zda nějaká proměnná či parametr Option náhodou neobsahuje hodnotu None, provést tento test implicitně s tím, že se v případě neúspěchu vrátí jakákoli jiná programátorem zvolená hodnota. Přesně k tomuto účelu slouží metoda Option.or(zvolená_hodnota), která vrací buď původní proměnnou/parametr nebo zvolenou hodnotu ve chvíli, kdy je původní proměnná/parametr rovna None. Nesmíme ovšem zapomenout na to, aby nová hodnota byla správného typu, což je kontrolováno již při překladu.
Jeden z příkladů použití (ne příliš dobrý!) zajišťuje, že nezávisle na tom, jak dopadlo dělení, bude možné vypočítat druhou mocninu výsledku dělení (s tím, že při dělení nulou bude i druhá mocnina nulová):
fn div(x: i32, y: i32) -> Option<i32> { if y != 0 { Some(x/y) } else { None } } fn sqr(val: Option<i32>) -> Option<i32> { let x = val.unwrap(); Some(x*x) } fn div_square(x: i32, y :i32) -> String { let result = sqr(div(x, y).or(Some(0))); if let Some(val) = result { format!("({} / {}) ^ 2 = {}", x, y, val) } else { String::from("Divide by zero") } } fn main() { println!("{}", div_square(2, 1)); println!("{}", div_square(2, 0)); }
11. Řetězení operací – metoda Option.and_then()
Mnohem elegantnější je použití metody Option.and_then(), které se předá nějaká funkce či anonymní funkce. Tato funkce se zavolá pouze ve chvíli, kdy platí hodnota != None. Pokud je původní hodnota naopak rovna None, funkce se nezavolá a pouze se vrátí None. Díky tomu, že volaná funkce vždy musí vracet typ Option, lze metody and_then() bez problémů řetězit stylem:
proměnná.and_then(fce1).and_then(fce2).and_then(fce3)
Podívejme se na příklad použití – počítá se zde druhá mocnina podílu dvou celočíselných hodnot, ovšem ve chvíli, kdy dojde k dělení nulou, druhá mocnina se vůbec nepočítá, což se posléze otestuje v podmínce (pattern match):
fn div(x: i32, y: i32) -> Option<i32> { if y != 0 { Some(x/y) } else { None } } fn sqr(x: i32) -> Option<i32> { Some(x*x) } fn div_square(x: i32, y :i32) -> String { let result = div(x, y).and_then(sqr); if let Some(val) = result { format!("({} / {}) ^ 2 = {}", x, y, val) } else { String::from("Divide by zero") } } fn main() { println!("{}", div_square(2, 1)); println!("{}", div_square(2, 0)); }
Tento program po svém spuštění vypíše následující dva řádky textu:
(2 / 1) ^ 2 = 4 Divide by zero
12. Metoda Option.unwrap_or()
Datový typ Option nám nabízí i další zajímavé a někdy i užitečné metody, například metodu Option.unwrap_or(). Jedná se vlastně o kombinaci metody Option.or() (vrácení zvolené, ovšem obalené hodnoty) a metody Option.unwrap(). To nám může ušetřit volání dvou metod – Some(…) a unwrap, ostatně podívejme se opět na příklad:
fn div(x: i32, y: i32) -> Option<i32> { if y != 0 { Some(x/y) } else { None } } fn div_print(x: i32, y :i32) -> String { let result = div(x, y).unwrap_or(0); format!("{} / {} = {}", x, y, result) } fn main() { println!("{}", div_print(2, 1)); println!("{}", div_print(2, 0)); }
13. Odkazy null v Rustu?
Programovací jazyk Rust sice umožňuje vytvářet odkazy na objekty umístěné na haldě, ovšem tyto odkazy nikdy nemohou obsahovat hodnotu null, nil atd. známou z jiných programovacích jazyků. To většinou nepředstavuje žádný větší problém (právě naopak to mnoho problémů řeší!), ovšem ve chvíli, kdy nějaká funkce za určitých podmínek nemůže vrátit odkaz, je zapotřebí mít možnost oznámit volajícímu kódu „teď namísto odkazu nic nevracím“. Řešením je samozřejmě opět použití datového typu Option, konkrétně pak:
Option<Box<T>> Option<Rc<T>> Option<Arc<T>>
Potom můžeme psát:
let x: Option<Box<i32>> = None;
14. 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ář:
15. Odkazy na Internetu
- 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