Programowanie w Rust: The Good, The Bad and The Ugly
Ten artykuł dotyczy mojego doświadczenia w nauce Rusta przez rozwiązywanie każdego problemu CtCI na żywo na Twitchu - moim niedokończonym projekcie.
Rust to nowoczesny systemowy język programowania, zaprojektowany z myślą o bezpieczeństwie. Zapewnia bezkosztowe abstrakcje, wsparcie dla programowania generycznego i funkcyjnego i wiele więcej. Niedawno podjąłem wysiłek, aby prawidłowo nauczyć się Rusta i chciałem podzielić się kilkoma swoimi przemyśleniami.
Do niedawna napisałem tylko garstkę małych programów w Rust i po przeczytaniu połowy „Programming Rust” wciąż tak naprawdę nie znałem Rusta. Pomyślałem, że dobrym sposobem na poznanie języka było rozwiązanie wszystkich 189 problemów z książki „Cracking the Coding Interview”. Nie tylko rozwiązałem je z Rustem, ale postanowiłem zrobić to na żywo na Twitchu. Nie są mi obce prezentacje techniczne lub kodowanie przed publicznością, ale próba nauczenia się języka programowania i wyjaśnienie tego, co robię - na żywo, na oczach wszystkich - było dla mnie czymś nowym.
Na początku było nieco szorstko: czkawki techniczne, problemy ze streamem, narzędziami i do tego na starcie miałem trudności ze zrozumieniem paradygmatu pamięci. Próbowałem to zrobić, wyjaśniając jednocześnie to, co robię, moim widzom. To było całkiem… trudne.
Zaimplementowanie połączonej listy zajęło mi około 8 godzin: nagrałem swoje dwa 4-godzinne streamy, próbując dowiedzieć się, jak prawidłowo używać Rc, RefCell i Box. Przez chwilę czułem się, jakbym walił w klawiaturę, próbując przypadkowych kombinacji, aż coś się zablokuje. Niesamowite jest to, że ludzie wpadali mnie oglądać. Musiałem coś robić dobrze.
Po przeczytaniu w trybie offline (następnie sięgnięciu po bardzo pomocną książki „Learning Rust with Entirely Too Many Linked Lists”) zacząłem łapać koncepcję. Po zakończeniu implementacji listy połączonej wszystko stało się nieco łatwiejsze.
Jestem teraz w rozdziale 4 książki i czuję, że osiągnąłem odpowiedni poziom. Rust jest naturalny, produktywny i niezwykle satysfakcjonujący po jego skompilowaniu. Jest silnie skupiony na pisaniu i dostarcza doskonałe komunikaty kompilatora. Jeśli uda Ci się zadowolić kompilator, istnieje duża szansa, że Twój kod zadziała - bez żadnych błędów logicznych.
Jedną wspaniałą cechą Rusta jest właśnie to, jak pomocny może być kompilator. Na przykład komunikaty kompilatora C++ są bardzo trudne do rozszyfrowania. Podczas gdy Clang robił ogromne postępy w sferze komunikatów o błędach, kompilator Rusta jest tak pomocny, że wchodzi na zupełnie inny poziom.
Podsumuję niektóre z moich dotychczasowych ustaleń. Opiera się to na moich początkowych doświadczeniach i jestem świadom swojego braku wiedzy na temat Rusta, ale porównanie naszej wiedzy może być interesujące dla innych, aby zobaczyć, jak ich doświadczenie porównuje się z moim. Z żalem przyznaję, że nie zbadałem dogłębnie każdego z poniższych problemów, więc możliwe, że bazuję na nieaktualnych informacjach.
Język: The Good
Przede wszystkim gratulacje dla zespołu Rust i wszystkich, którzy przyczynili się do projektu. To było jedno z moich najciekawszych doświadczeń, jeżeli chodzi o naukę programowania. Nie jestem pewien, czy Rust pozostanie w sercach deweloperów w taki sam sposób, jak niektóre inne języki, ale myślę, że na pewno nie zniknie. Więcej szczegółów:
- Kod Rust jest dość łatwy do odczytania i nie cierpi na trudną do przeanalizowania składnię tak jak C ++ lub Scala. Rust posiada to, czego oczekiwałem, a wyzwaniem jest jedynie ustalenie, którą funkcję wywołać.
- Posiada funkcyjne właściwości, takie jak
map()
,filter()
,find()
, itd jest rozkoszą. Definiowanie funkcji wyższego rzędu i przekazywanie im domknięć jest bardzo proste. Może nie sprawia, że programowanie funkcyjne jest tak proste, jak w Ruby, ale jest blisko. W rzeczywistości jest to całkiem niesamowite, jak łatwo przychodzi to językowi porównywalnemu w działaniu do C/ C++. - Rust zmusza Cię do myślenia nad alokacją pamięci, ponieważ nie masz innego wyboru. Ostatecznie oznacza to, że niechlujny kod jest trudny do napisania, a czysty kod przychodzi z łatwością. Te abstrakcje mapują się również bezpośrednio do pisania bezpiecznego kodu współbieżnego.
- Bezkosztowe abstrakcje Rusta ułatwiają napisanie dobrego kodu bez dodawania dodatkowego narzutu. Cechy (ang. traits) zapewniają nowoczesną abstrakcję bez zaniżania wydajności.
- Kod Rust jest bezpieczny(pod warunkiem, że nie używasz słowa kluczowego
unsafe
lub nie wywołujesz niebezpiecznych bibliotek C). - Funkcje
Result
iOption
zapewniają dobry sposób radzenia sobie z funkcjami, które mogą zwrócić wartość lub zmiennymi, które mogą zawierać wartość. Typowy wzorzec w C, C++, a nawet Javie, polega na tym, że funkcje zwracają pusty wskaźnik, gdy nie ma nic do zwrócenia. W większości przypadków, gdy dzieje się to nieoczekiwanie, potrafi zepsuć zabawę.
Język: The Bad
- Uważam konieczność używania
unwrap ()
,as_ref()
iborrow()
za nieco rozwlekłe. Chciałbym, żeby był jakiś trick składniowy, który sprawiłby, że nie muszę tak często ich łączyć w łańcuchy wywołań w przeróżnych kombinacjach. Często piszę kod podobny dooption.as_ref().unwrap().borrow()
, z czym czuje się dziwnie. - Kompilator musi dokonać pewnych kompromisów, aby móc skompilować kod w rozsądnym czasie. W rezultacie istnieją przypadki, w których
rustc
nie może wywnioskować typu lub potrzebuje mojej pomocy w skompilowaniu kodu. Czasem ciężko było się dowiedzieć, czego potrzebuje kompilator i dlaczego nie może tego sam zrozumieć. - Niektóre rzeczy wymagają zbyt rozwlekłego kodu. Np. konwersja między
str
iString
lub przekazanie referencji zamiast wartości do funkcji, wydaje się czymś, co kompilator powinien rozwiązać. Jestem pewien, że jest dobry powód, dla którego tak jest, ale czasami wydaje się, żerustc
jest zbyt poprawny. - Konieczność obsługi każdego
Result
każdej funkcji jest dobra; oznacza to, że programista musi myśleć o tym, co dzieje się z każdym wywołaniem funkcji. Czasami jest to nudne. Operator?
Może tu pomóc, ale nie ma dobrego ogólnego sposobu obsługi typów powiązanych z błędami. Crate’y, takie jak failure i error-chain, ułatwiają to zadanie, ale nadal musisz wyraźnie zdefiniować osobny przypadek dla każdego rodzaju błędu, który może wystąpić.
Język: The ugly
- Makra: WTF? Makra Rusta wydają się zupełnie nie pasować do języka. Żeby być uczciwym, nie byłem jeszcze w stanie ich w pełni pojąć, a i tak wydają się jakoś nie być na miejscu, jak jakiś dziwny przypięty dodatek inspirowany Perlem, który powstał dopiero po zaprojektowaniu języka. Pewnie któregoś dnia poświęcę trochę czasu, aby dobrze je zrozumieć na przyszłość, ale na ten moment chcę ich unikać jak zarazy.
Narzędzia: The Good
- Rust zapewnia przyzwoite oprzyrządowanie i integruje się z IDE, takimi jak VSCode, poprzez RLS. RLS zapewnia obsługę lintingu, uzupełniania kodu, sprawdzania składni i formatowania w locie.
- Cargo to potężny menedżer pakietów Rusta: prawdopodobnie szybko się z nim zapoznasz, jeśli postanowisz skorzystać z Rust. W większości praca z Cargo to przyjemność. Istnieje na ten moment mnóstwo wtyczek dla Cargo, które zapewniają dodatkowe funkcje, takie jak pokrycie kodu.
- Cargo jest również systemem kompilacji i może być używane do uruchamiania testów jednostkowych i integracyjnych. Konfiguracja kompilacji i zależności jest bardzo prosta, dzięki jej deklaratywnej składni TOML.
- Cargo integruje się z crates.ioktóre jest pełnym źródłem open source dla projektów Rusta. Podobnie jak z PyPi lub RubyGems, znajdziesz prawie wszystkie inne pakiety Rusta na crates.io.
- rustupjest preferowanym narzędziem do zarządzania instalacją Rust. Możesz wybrać kanał stabilny, beta lub nightly i zainstalować którykolwiek ze wszystkich powstałych release'ów. Pozwala także zainstalować komponenty takie jak clippy i rustfmt
- clippyto obowiązkowy linter kodu, jeśli jesteś perfekcjonistą takim jak ja. Pomoże Ci to w nauce Rusta i może wykryć wiele typowych błędów, których bez niego nie zauważysz. Dla mnie clippy był pomocny, gdy wiedziałem, jak coś rozwiązać, ale nie znałem właściwego sposobu.
- rustfmtjest formaterem kodu dla Rusta. Moim zdaniem, korzystanie z takich narzędzi to podstawa. Nie ma sporów o formatowanie kodu, gdy wszystko jest zgodne z tym samym standardem.
- sccache, cache kompilatora, przyspieszy działanie, skracając czas kompilacji. Jednak - uważaj - sccache nie działa z RLS, więc nie możesz go używać ze swoim IDE.
Narzędzia: The Bad
No dobra, zanim przejdę do problemów, powinniśmy pamiętać, że Rust ciągle się rozwija. Oprzyrządowanie Rust zaszło całkiem daleko w bardzo krótkim czasie, ale myślę, że jeszcze długa droga przed nami. Podkreślę kilka rzeczy, które wymagają poprawy:
- Kompilacja jest powolna. Nie tylko powolna. Często zmusza mnie do rekompilacji pakietów. Rozumiem konieczność, ale czasami jest to irytujące. sccache pomaga, ale wciąż kompilacja jest za wolna. Jest kilka sposobów na złagodzenie tego, np.
cargo check
zamiastcargo build
. - RLS używa racerado uzupełnienia kodu i uważam, że w najlepszym wypadku jest średnio skuteczny (przynajmniej w VSCode). Często funkcje, od których oczekiwałbym, że mają podpowiedzi - nie istnieją, a funkcje, które nie istnieją, pojawiają się jako opcje uzupełniania. Nie przeprowadziłem dokładnej analizy, ale sugestie wydają się właściwe tylko w 75% przypadków. Przyczyną tego może być po prostu powolność RLS.
- Brak REPL: to może być niesprawiedliwe, ponieważ nie ma też przyzwoitego REPL dla C++, ale w wielu językach pojawia się obecnie REPL. Jest o tym otwarty issue na GitHubie. REPL nie jest konieczny, ale byłby pomocny.
Narzędzia: Ugly
- RLS jest powolny, zbugowany i często się wywala. W moim przypadku często zmuszony byłem ponownie uruchomić RLS w VSCode. RLS to świetne narzędzie, ale moim zdaniem robi w najlepszym przypadku wrażenie bety. Muszę się zatrzymać i poczekać, aż RLS mnie dogoni, aby się upewnić, że nie piszę złego kodu. Czasami myślę, że lepiej byłoby po prostu wyłączyć RLS, napisać kod, a następnie spróbować skompilować go tak, jak w dawnych czasach, kiedy robiłem całe moje kodowanie w Vimie. Można powiedzieć, że RLS stał się czymś, co rozprasza, zamiast czymś, co pomaga.
Biblioteki: The Good
- Jest zaskakująco duża liczba dostępnych bibliotek w ekosystemie Rust. Wydaje się, że trwa jakaś gorączka złota, mająca na celu wyczerpać i zaimplementować wszystkie biblioteki Rust i wpisać się w jego historię. Można tu znaleźć większość tego, czego można by się spodziewać po crates.io lub GitHubie. Często jestem zaskoczony tym, że każde wyszukiwanie wyświetla 2 lub 3 różne implementacje tego, czego szukam.
- Większość bibliotek, z których korzystałem, działała zgodnie z oczekiwaniami, a wiele z nich przekroczyło oczekiwania. Jest to subtelne i ważne rozróżnienie od alternatywy, czyli niedziałających bibliotek.
Biblioteki: The Bad
- Chociaż istnieje wiele bibliotek, odkryłem, że wiele z nich jest niekompletnych, niedojrzałych lub całkowicie porzuconych. Wygląda na to, że społeczność Rust wciąż jest na wczesnym etapie rozwoju, ale z każdym dniem sytuacja się poprawia.
- Czasami jest… zbyt wiele opcji. Na przykład chciałem użyć biblioteki logowania i odkryłem, że istnieje długa lista opcji do wyboru. Posiadanie wielu opcji jest w porządku, ale w tym konkretnym przypadku po prostu chciałbym, aby mi ktoś powiedział dokładnie, czego mam użyć. Ekosystem Java ma podobny problem z java.util.logging, log4j, logback, log4j2, slf4j i tinylog. Do dzisiaj nie wiem, która biblioteka logowania Javy jest odpowiednia. W Rust postanowiłem użyć
env_logger
, tylko ponieważ był pierwszą opcją na liście. - Chociaż nie jest tak źle, jak z ekosystemem Node.js, lista zależności dla każdej biblioteki stała się dość długa. Napisałem niewielkiego bota dla GitHuba o nazwie LabHub i nadal jestem zaskoczony, jak wiele zależności zostało wciągniętych (181 skrzynek, jeśli się zastanawiasz). Dla mnie sugeruje to fragmentację i duplikację, która mogłaby zostać poprawiona przez powolne przenoszenie niektórych powszechnie potrzebnych funkcji do standardowej biblioteki (coś, co C++ robi bardzo powoli).
Biblioteki: The Ugly
- Zauważyłem, że wraz z długą listą zależności dla stosunkowo prostej aplikacji, kompilowałem różne wersje tych samych bibliotek wielokrotnie. Myślę, że Cargo stara się być sprytny, aby zachować kompatybilność wsteczną; zgaduje, których bibliotek należy użyć w oparciu o wersjonowanie semantyczne. Martwi mnie to, co dzieje się, gdy masz bibliotekę, która zależy od starożytnej, zepsutej wersji innej biblioteki, która również ma lukę. Nie mam wątpliwości, że autorzy Rusta już to rozważali, ale wciąż wydaje się to dziwne. Żeby być uczciwym, radzenie sobie z zależnościami jest bardzo trudnym problemem. Na szczęście istnieje narzędzie drzewa dla Cargo, które może pomóc w uporządkowaniu tych rzeczy, a następnie wymusić zależność, aby uaktualnić swoje zależności.
Podsumowanie
Rust to świetny język. Jeśli programowanie jest czymś, co Cię pasjonuje, wypróbuj go. Mam nadzieję, że Ci się spodoba!