TIL: Rust 1. Enumy, matche i wszechobecne znaki zapytania
Cofnijmy się nieco w przeszłość. Uważam, że dobrze jest przypomianć sobie podstawy, kiedy próbuje się dość szybko robić progres. I nie ma znaczenia w jakiej umiejętności.
Więc pogadam sobie o enumach, matchach i operatorze ? w Rust. Dlaczego tak?
Mam takie wrażenie, że warto rozmawiać o tematach które są trudne. A przynajmniej kiedyś były. Bo właśnie te trzy kwestie były jednym z powodów mojego “odbicia się” od tego jeżyka w przeszłości. Jednak ta ściana już nie istnieje. I to jest super. Więc skoro ta ściana podstaw została już zburzona, to pogadajmy o niej. Dla praktyki i przypominania podstaw.
Enumy
Enum to typ wyliczenionyw wróznych językach programowania. Wyliczeniowy, bo pozawal na “wyliczenie” wszystkich dozwolonych wartości. I jak się okazuje jedna z najczęsniej pojawiającyhc się konstrukcji w samym Rust’cie (chociaż nie zawsze jawnie).
Najczęstsze przykłady do konstrukcja Result (oznaczająca wynik operacji która może się nie udać):
enum Result<T,E> {
Ok<T>,
Err<E>,
}oraz Option (stwierdzająca czy dana wartość istnieje czy nie).
enum Option<T> {
Some<T>
None,
}Match
Z kolei match to instrukcja przypominająca switch z bardziej klasycznych języków programowania. Czyli banał - sprawdzamy, czy wartość zmiennej występuje w zdefiniowanej w match liście wartości. I jeżeli tak to program wykonuje na tej wartości jakieś operacje.
Najlepszy przykład, to obsługa błędów z funkcji - czyli połączenie z enumem Result -> tutaj funkcja run która zwraca type Result. To bardoz prosty wzorzec globalnego handlera błędów. Jeżeli funkcja run zwróci Ok, to program zakończy się z kodem 0, a jeżeli Err to wypisze błąd i zakończy się z kodem 1.
fn main() {
let app = run();
match handler {
Ok(app) => exit(0),
Err(e) => {
eprintln!("Error: {e}");
exit(1);
}
}
}Powyższy wzorzec pozwala też na stosowanie operatora znaku zapytania wewnątrz funkcji run(). I właśnie to było miejsce mojego pierwszego odbicia się od Rusta.
Znaki zapytania
Czyli co robi ten cholerny znak zapytania? Występuje co chwila, ale kiedy sam próbowałem go stosować - bład. Próba zmiany - dalej błędy.
Oczywiście można powiedzieć, że “wystarczyłoby przeczytać co mówi kompilator”. I do pewnego stopnia jest to prawda. Ale jeżeli próboujesz przejść z dynamicznie typowanego języka, jakim jest Python (uwielbiam!) do typu statycznego, i w dodatku tak bardzo wymagającego jak Rust, to można się zniechęcić. I nie chodzi tylko o komunikaty kompilatora - które są bardzo szczegółowe i pomocne (chociaż mam wrażenie, że w prostych programach i przy niewielkim odświadczeniu, ten poziom precyzji bardziej przeszkadza niż pomaga).
Dla mnie - większy problem stanowiła zmiana sposobu myślenia o sposobach obsługi błędów.
fn run() {
jakasFunkcjaRzucającaBłędem()?;
Ok()
}
// Błąd kompliacji
Myślenie o błędach w Rust jest prostsze - bład to wartość, która może być zwrócona przez funkcję, i czasem przyjmować inne wartości. Nie ma żadnego niewidocznego mechanizmu, który kieruje program w różne miejsca. I na tym polegał właśnie problem. Ten mechanizm jest tak prosty, że wtedy wydawał się aż zbyt prosty. Na siłę szukałem jakiegoś haczyka - w końcu przywyczajenia z nieustannego łapania wyjątków i ich obsługiwania jeden po drugim w Pythonie robiły swoje.
Wracając do operatora ? - żeby móc go użyć, funkcja musi zwracać typ Result. I to jest właśnie ten haczyk. Jeżeli funkcja zwraca typ Result, operator ? pozwala przekazać błąd do funkcji wywołującej. Jeżeli zwraca Ok(), to program jest kontynuowany.
Czyli konieczna poprawka:
fn run() -> Result<(), AppError> { // Dodany typ zwracany Result
jakasFunkcjaRzucającaBłędem()?;
Ok(()) // Typ pusty zwracany w przypadku sukcesu - jak w definicji
}
// Teraz już działa
I oczywiście - prawdopodobnie te kilka lat temu wystarczyłoby przeczytać komunikat kompilatora. A z doświadczenia wiem, że czasami najprostsze błedy są najtrudniejsze do zdebugowania. I właśnie dlatego warto chociaż od czasu przypomnieć sobie podstawy.