Hlavní navigace

Datový typ Option v programovacím jazyku Rust

Pavel Tišnovský

V dalším článku o jazyku Rust si popíšeme práci s datovým typem Option, který se v programech poměrně často používá, protože zjednodušuje práci s „neexistujícími“ hodnotami, volitelnými parametry funkcí, volitelnými členy struktur atd.

Obsah

1. Datový typ Option v programovacím jazyku Rust

2. Základní použití – funkce vracející hodnotu typu Option

3. Metoda Option.unwrap()

4. Metoda Option.is_none()

5. Metoda Option.is_some()

6. Idiomatický kód – použití pattern matchingu

7. Použití řídicí struktury if let

8. Řídicí struktura if let ve výrazu

9. Řídicí struktura while let

10. Řetězení operací – metoda Option.or()

11. Řetězení operací – metoda Option.and_then()

12. Metoda Option.unwrap_or()

13. Odkazy null v Rustu?

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

15. Odkazy na Internetu

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/pre­sentations. Příklady si můžete v případě potřeby stáhnout i jednotlivě bez nutnosti klonovat celý repositář:

15. Odkazy na Internetu

  1. Rust stdlib: Option
    https://doc.rust-lang.org/std/option/enum.Option.html
  2. Module std::option
    https://doc.rust-lang.org/std/option/index.html
  3. Rust by example: option
    http://rustbyexample.com/std/op­tion.html
  4. Rust by example: if-let
    http://rustbyexample.com/flow_con­trol/if_let.html
  5. Rust by example: while let
    http://rustbyexample.com/flow_con­trol/while_let.html
  6. Rust by example: Option<i32>
    http://rustbyexample.com/std/op­tion.html
  7. An Overview of Macros in Rust
    http://words.steveklabnik.com/an-overview-of-macros-in-rust
  8. A Practical Intro to Macros in Rust 1.0
    https://danielkeep.github.io/practical-intro-to-macros.html
  9. The Rust Programming Language: macros
    https://doc.rust-lang.org/beta/book/macros.html
  10. Rust by example: 15 macro_rules!
    http://rustbyexample.com/macros.html
  11. Module std::vec
    https://doc.rust-lang.org/nightly/std/vec/index.html
  12. Primitive Type isize
    https://doc.rust-lang.org/nightly/std/primi­tive.isize.html
  13. Primitive Type usize
    https://doc.rust-lang.org/nightly/std/primi­tive.usize.html
  14. Primitive Type array
    https://doc.rust-lang.org/nightly/std/primi­tive.array.html
  15. Module std::slice
    https://doc.rust-lang.org/nightly/std/slice/
  16. Rust by Example: 2.3 Arrays and Slices
    http://rustbyexample.com/pri­mitives/array.html
  17. What is the difference between Slice and Array (stackoverflow)
    http://stackoverflow.com/qu­estions/30794235/what-is-the-difference-between-slice-and-array
  18. Learning Rust With Entirely Too Many Linked Lists
    http://cglab.ca/~abeinges/blah/too-many-lists/book/
  19. Testcase: linked list
    http://rustbyexample.com/cus­tom_types/enum/testcase_lin­ked_list.html
  20. Operators and Overloading
    https://doc.rust-lang.org/book/operators-and-overloading.html
  21. Module std::ops
    https://doc.rust-lang.org/std/ops/index.html
  22. Module std::cmp
    https://doc.rust-lang.org/std/cmp/index.html
  23. Trait std::ops::Add
    https://doc.rust-lang.org/stable/std/ops/trait.Add.html
  24. Trait std::ops::AddAssign
    https://doc.rust-lang.org/std/ops/trait.AddAssign.html
  25. Trait std::ops::Drop
    https://doc.rust-lang.org/std/ops/trait.Drop.html
  26. Trait std::cmp::Eq
    https://doc.rust-lang.org/std/cmp/trait.Eq.html
  27. Struct std::boxed::Box
    https://doc.rust-lang.org/std/boxed/struct.Box.html
  28. Explore the ownership system in Rust
    https://nercury.github.io/rus­t/guide/2015/01/19/ownership­.html
  29. Rust's ownership and move semantic
    http://www.slideshare.net/sa­neyuki/rusts-ownership-and-move-semantics
  30. Trait std::marker::Copy
    https://doc.rust-lang.org/stable/std/marker/tra­it.Copy.html
  31. Trait std::clone::Clone
    https://doc.rust-lang.org/stable/std/clone/tra­it.Clone.html
  32. The Stack and the Heap
    https://doc.rust-lang.org/book/the-stack-and-the-heap.html
  33. Rust Compare: Pointers & References
    http://www.rust-compare.com/site/pointers.html
  34. Rust Compare: Parameters
    http://www.rust-compare.com/site/params.html
  35. Why does this compile? Automatic dereferencing?
    https://users.rust-lang.org/t/why-does-this-compile-automatic-dereferencing/2183
  36. Understanding Pointers, Ownership, and Lifetimes in Rust
    http://koerbitz.me/posts/Understanding-Pointers-Ownership-and-Lifetimes-in-Rust.html
  37. Rust lang series episode #25 — pointers (#rust-series)
    https://steemit.com/rust-series/@jimmco/rust-lang-series-episode-25-pointers-rust-series
  38. Rust – home page
    https://www.rust-lang.org/en-US/
  39. Rust – Frequently Asked Questions
    https://www.rust-lang.org/en-US/faq.html
  40. Destructuring and Pattern Matching
    https://pzol.github.io/get­ting_rusty/posts/20140417_des­tructuring_in_rust/
  41. The Rust Programming Language
    https://doc.rust-lang.org/book/
  42. Rust (programming language)
    https://en.wikipedia.org/wi­ki/Rust_%28programming_lan­guage%29
  43. Go – home page
    https://golang.org/
  44. Stack Overflow – Most Loved, Dreaded, and Wanted language
    https://stackoverflow.com/re­search/developer-survey-2016#technology-most-loved-dreaded-and-wanted
  45. 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/
  46. Rust vs Go: My experience
    https://www.reddit.com/r/go­lang/comments/21m6jq/rust_vs_go_my_ex­perience/
  47. Friends of Rust (Organizations running Rust in production)
    https://www.rust-lang.org/en-US/friends.html
  48. Rust programs versus C++ g++
    https://benchmarksgame.ali­oth.debian.org/u64q/compa­re.php?lang=rust&lang2=gpp
  49. Další benchmarky (nejedná se o reálné příklady „ze života“)
    https://github.com/kostya/benchmarks
  50. Go na Redditu
    https://www.reddit.com/r/golang/
  51. Rust vs. Go
    http://vschart.com/compare/rust/vs/go-language
  52. Abstraction without overhead: traits in Rust
    https://blog.rust-lang.org/2015/05/11/traits.html
  53. Method Syntax
    https://doc.rust-lang.org/book/method-syntax.html
  54. Traits in Rust
    https://doc.rust-lang.org/book/traits.html
  55. Functional Programming in Rust – Part 1 : Function Abstraction
    http://blog.madhukaraphatak­.com/functional-programming-in-rust-part-1/
  56. 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
  57. Chytré ukazatele (moderní verze jazyka C++) [MSDN]
    https://msdn.microsoft.com/cs-cz/library/hh279674.aspx
  58. UTF-8 Everywhere
    http://utf8everywhere.org/
  59. Rust by Example
    http://rustbyexample.com/
  60. Rust oficiálně ve Fedoře
    https://mojefedora.cz/rust-oficialne-ve-fedore/
  61. Resource acquisition is initialization
    https://en.wikipedia.org/wi­ki/Resource_acquisition_is_i­nitialization
  62. TIOBE index (October 2016)
    http://www.tiobe.com/tiobe-index/
  63. 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
  64. String Types in Rust
    http://www.suspectsemantic­s.com/blog/2016/03/27/str­ing-types-in-rust/
  65. Trait (computer programming)
    https://en.wikipedia.org/wi­ki/Trait_%28computer_program­ming%29
  66. Type inference
    https://en.wikipedia.org/wi­ki/Type_inference
Našli jste v článku chybu?