Warto uczyć się języka Rust? Devdebata
Czy program napisany w Rust jest porównywalny wydajnościowo do tego napisanego w C/C++? Czym różnią się makra Rust’a od makr preprocesora z C/C++ i czy powielają te same problemy? Czy pisząc w Rust, z każdą nową funkcjonalnością jesteśmy zmuszeni wymyślać koło na nowo? Jak wygląda dostępność bibliotek dla języka Rust? Te pytania zadaliśmy w pierwszej części devdebaty pt. Czy Rust jest następcą C++?
Dziś przedstawiamy drugą część wypowiedzi zaproszonych seniorów, którzy podzielili się spostrzeżeniami na temat Rust’a i C++. Devdebatę przygotował Damian Babula, Software Developer w Telestream.
W devdebacie udział wzięli:
- Bartłomiej Kuras. Senior Rust Developer w Luxoft, a po godzinach współorganizator Rust Wrocław i konferencji Rusty Days. Zajmuje się implementacją rdzenia sieci 5g. W przeszłości programista C++, dzisiaj cały swój wysiłek wkłada w promowanie idiomów Rustowych i samego języka. Żywo zainteresowany poznawaniem nowych technologii twierdzi, żę nowy jezyk zawsze warto poznać choćby po to, żeby zobaczyć inny punkt widzenia.
- Paweł Romanowski. Senior backend engineer w Sauce Labs. Zajmuje się usprawnianiem i rozszerzaniem centralnych części platformy testowej Sauce. Programista Pythona z rozległym doświadczeniem w wielu projektach. Miłośnik Rusta i pasjonat programowania niskopoziomowego, o wysokiej wydajności. Promotor dobrych praktyk inżynierii oprogramowania.
- Michał Papierski. Programuje w języku Rust i cały swój wysiłek wkłada w rozwój projektów Open Source opartych na w/w technologii. W przeszłości fanatyk C++ który zdobywał doświadczenie w dużych korporacjach takich jak Samsung i Amazon. Aktualnie rozwija swoją własną firmę konsultingową IT gdzie również promuje Rusta. W wolnych chwilach śledzi rozwój swojego ulubionego języka i jest zafascynowany jego dynamizmem i kierunkiem w którym on podąża.
Spis treści
4. Czym różni się podejście do obsługi błędów w Rust? Dlaczego zrezygnowano w nim z koncepcji wyjątków oraz popularnych bloków „try-catch”?
Odpowiada Bartłomiej Kuras, Rust SW Developer in Metaswitch via Luxoft:
Skupianie się na składni w tym kontekście jest zdradliwe, mam bowiem dobrą wiadomość dla jej fanów – try jest słówkiem zarezerwowanym w aktualnej edycji Rusta, a prace nad wprowadzeniem składni try… catch trwają.
Odpowiadając jednak do rzeczy – różni się fundamentalnie sposób obsługi błędów w Ruscie od tego znanego z C++ czy innych językach. Zacznę od rozwiania pewnego mitu – w Ruscie są wyjątki. Panic jest niczym innym jak wyjątkiem w C++ – powoduje rozwinięcie stosu i zawołanie panic handlera, który domyślnie crashuje aplikację. Sęk w tym, że w Ruscie jest to zarezerwowane dla sytuacji wyjątkowych (np. wejście w niezaimplementowany branch w wydaniu debugowym – makro unimplemented!, wejście w branch, który z definicji jest nieosiągalny – makro unreachable!, czy brak pamięci).
Większość jednak błędów obsługuje się bezpośrednio w miejscu wystąpienia, a ich wystąpienie jest nie tyle nie wyjątkowe, co spodziewane (np. brak możliwości otwarcia pliku o nazwie podanej przez użytkownika jest więcej niż prawdopodobna). Używanie do takich celów wyjątków nie tylko zaciemnia kod (co jest z resztą opinią dyskusyjną), ale przede wszystkim jest niewydajne – pamiętajmy, że o ile zwykle w miejscu napisania “try” nie mamy żadnego kosztu, o tyle “throw/catch” są słówkami mocno impaktującymi performance.
Rust podjął decyzję, żeby powszechne błędy obsługiwać, zwracając z funkcji coś w rodzaju ulepszonej uni (nazywanych w różnych językach “tagged union”, czy “algebraic type” – w Ruscie to są po prostu enumy), która jest albo zwróconą wartością, albo wartością reprezentującą błąd, oraz dodatkowego taga, który określa co siedzi w środku (w pewnych sytuacjach tag może być pominięty, ale to już są szczegóły optymalizatora).
Dwie podstawowe zalety to:
- takiego błędu nie można zignorować, gdyż typ Result jest oznaczony jako
must_use
(zignorowanie go powoduje ostrzeżenie kompilatora), - dokładnie jasny jest przebieg błędu – jeśli operacja może spowodować błąd, trzeba na miejscu zdecydować, jak będzie obsłużony.
Wydawać by się mogło, że przez takie ograniczenie, obsługa błędów jest bardzo problematyczna, co jest jednak dalekie od prawdy – API typu Result jest bardzo bogate, ale i intuicyjne, możemy więc łatwo zdecydować, czy umiemy ten błąd obsłużyć (funkcje or_else, unwrap_or_else), czy może chcemy go zamienić w wyjątek (i spowodować crash aplikacji – funkcje unwrap, expect), a może przekazać dalej (operatorem?, być może mapując błąd na bardziej ogólny funkcją map_err) lub nawet explicite zignorować (funkcja ok).
Dodatkową zaletą jest fakt, że nie ma wojny o to, czy kompilować z, czy bez wyjątków, a obsługa błędów we wszystkich bibliotekach jest mniej lub bardziej jednolita. Na sam koniec – warto mieć świadomość, co się stanie w C++, jeśli zdecydujemy się na kompilację bez wyjątków, a wyjątek jednak poleci (a wyłączyć ich w bibliotece standardowej się nie da) – tym czytelnikom, którzy nie są tego pewni, polecam temat zbadać.
Odpowiada Paweł Romanowski, Senior Backend Engineer w Sauce Labs:
Obsługa błędów w Rust jest jednym z powodów, dla których język tytułuje się “bezpiecznym” i “niezawodnym”. Rust wyjątki rezerwuje dla “wyjątkowych” sytuacji (tzn “panikowania”), nie dla ordynarnych błędów. Moim zdaniem jest to dobra decyzja. Wyjątki są problematyczne, bo zmieniają ścieżki wykonania programu oraz mogą być zwyczajnie nie obsłużone w kodzie, nie mówiąc już o narzucie wydajnościowym.
Rust błędy traktuje jak dane, tj. każda funkcja, która może zwrócić błąd musi to zadeklarować w swojej sygnaturze. Wartości opcjonalne (Some / None) przekazuje się generycznym typem Option, a wartości lub błędy – generycznym typem Result (OK / Error). Oba te typy są enumami (“tagged union”) i jakakolwiek interakcja z nimi wymaga świadomej, jawnej obsługi obu możliwości przez programistę. Nie jest więc możliwe zignorowanie błędu (można iść na skróty i poinstruować program by panikował w przypadku błędu – jednak musi to być jawne, tzn. metody .unwrap()
lub .expect()
).
Nie oznacza to wszystko jednak, że obsługa błędów w Ruscie jest wyjątkowo uciążliwa. Jest specjalny operator “?”, który znacznie upraszcza obsługę błędów. Do pisania własnych struktur błędów i ich konwersji są dobre biblioteki (np. anyhow, snafu, thiserror), a sam język ciągle ewoluuje w kierunku ulepszenia ergonomii i dodania usprawnień w zakresie obsługi błędów do biblioteki standardowej.
Odpowiada Michał Papierski, Founder w DELTA Solutions:
To prawda, że Rust nie posiada wyjątków (no dobrze, z wyjątkiem “panic”, który tak naprawdę nie jest wyjątkiem…). Prawdą jest również brak bloków try/catch. Mimo tego podejście do kontroli błędów jest bardzo dobrze zaprojektowanie.
Możliwe błędy zwracane są poprzez wartości. Wyróżnić tu można dwa standardowe typy, które w tym pomagają, a które przyjmują dwa różne stany – Option<T>, który może zawierać wariant “Some(…)” lub “None”, i Result<T, E>, który ma warianty “Ok(…)” i “Err(…)”. Dodatkowo, dla funkcji, które zwracają te typy, jak również używają innych funkcji wewnątrz, które zwracają te dwa wymienione typy dostajemy do dyspozycji opertor, który propaguje wyżej wartość None (dla wartości Option) i wartość z wariantu Err(…) typu Result.
Same błędy mogą być implementowane dowolnie, jako struktury, enumeracje, lub nawet typ “unit”, który reprezentowany jest poprzez 0 bajtów (taka ciekawostka: coś co w C++ nie jest możliwe), to sam wariant Ok/Err lub Some/None ma znaczenie, konkretna wartość danego wariantu to tylko informacja, czego dany błąd dotyczy. Rust również dostarcza trait “std::error::Error”, dzięki któremu nasz obiekt to “błąd” i posiada szereg standardowych dodatkowych metod
Dodatkowym mechanizmem mechanizmem jest również “panic!”, który raz wyrzucony zamyka działanie programu. W prawdzie jest możliwość złapania takiego błędu i zamienienia go na typ Result, aczkolwiek nie jest to generalnie zalecane.
5. Jak wygląda podejście Rust’a do programowania współbieżnego/wielowątkowego?
Odpowiada Bartłomiej Kuras, Rust SW Developer in Metaswitch via Luxoft:
Co do zasady podobnie do C++, z tym że ze wszystkimi zaletami jakie daje nam Rust. Biblioteka standardowa języka daje nam możliwość tworzenia wątków systemowych razem z podstawowymi operacjami (jak czekanie, aż wątek zakończy pracę).
Wspomniałem jednak coś o zaletach jakie daje nowy język, warto więc jest je uwypuklić – otóż Rust gwarantuje, że dopóki nie używamy jego niebezpiecznej warstwy (której używa się niezmiernie rzadko) gwarantuje całkowitą gwarancję braku wyścigów danych – prawdopodobnie najtrudniejszego do wykrycia błędu związanego z wątkami. I muszę przyznać, że jeśli ktoś przestał w tym miejscu czytać, to ja go rozumiem – ja też nie mogłem w to uwierzyć i twierdziłem, że ktoś bajki opowiada. Warto jednak zrozumieć mechanizm, który dostarcza nam język, na przykładzie najprostszego prymitywu synchronizacyjnego – mutexa.
W C++ (i wielu innych językach) to tylko taka wajcha, którą można przestawić i umownie w trakcie, kiedy jest przestawiona można odczytywać i zmieniać pilnowane przez nią zasoby. Do kompletu jest jeszcze coś, co się nazywa lock_guard
– takie dodatkowe urządzonko, które jak się zorientuje, że wyszliśmy z pokoju, samo przestawia wajchę z powrotem na odblokowaną pozycję, żeby można było jej znów użyć, a żebyśmy sami nie musieli o tym pamiętać.
Jeśli jednak chcielibyśmy odczytać, lub nawet zmodyfikować zasoby bez zablokowania mutexa, nie ma nic co stanie na przeszkodzie – może poza zdrowym rozsądkiem i niezawodną pamięcią, które lubią zawodzić w obliczu błędów zgłaszanych przez klienta.
W Ruscie mutex (i analogiczne prymitywy) są zorganizowane inaczej. Ich semantyka jest podobna bardziej do inteligentnego wskaźnika, który nie pozwala w ogóle dostać się do zasobu. Aby to zrobić, należy najpierw zawołać metodę “lock”, która zwróci nam obiekt typu “MutexGuard”. Ten znów zachowuje się jak inteligentny wskaźnik, ale taki, który daje nam pełen dostęp do zasobu. Oczywiście metoda “lock” dba o to, żeby jednocześnie na wszystkich wątkach mógł istnieć tylko jeden “MutexGuard” dla każdego mutexa. Kiedy zaś strażnik zostanie zniszczony (czyli mniej więcej wtedy, kiedy w C++ zawołano by destruktor), mutex zgodnie z RAII zostaje odblokowany.
Oprócz tego, Rust daje nam w bibliotece standardowej dodatkowe prymitywy synchronizacyjne, których nie ma w bibliotece standardowej C++ – kanały. Kanały to takie obiekty, które mają dwa końce – jeden, na który można wysyłać obiekty i drugi, który te obiekty odbierze. Koniec wysyłający można kopiować, a więc na jednym kanale może być wielu producentów, ale jeden odbiorca (stąd jego nazwa – mpsc, “multiple producers, single consumer”). Wszystkie końce kanałów mogą znajdować się na zupełnie różnych wątkach, a synchronizacja jest zaimplementowana poprzez struktury nieblokujące.
O czym warto też w tym miejscu wspomnieć, to wsparcie Rust’a nie tylko do programowania w oparciu o fizyczne wątki, ale niedawno wydany feature języka pozwalający na łatwe programowanie asynchroniczne. Nie zacząłem od tego, ponieważ sam język nie daje nam możliwości rozdysponowania asynchronicznych zadań na wątki, ale biblioteka tokio jest całkowitym standardem w tym języku, a dostarcza ona oprócz prymitywów do asynchronicznej obsługi różnego typu wejścia i wyjścia, bardzo wydajny egzekutor implementujący kradzież zadań.
Można powiedzieć, że tokio jest w Ruscie tym, czym asio w C++, z tym że potężna biblioteka futures i wygodny cukier składniowy pozwalający na wygodne pisanie korutyn pozwala na pisaniu asynchronicznego kodu dużo wygodniej niż w odpowiedniku z C++.
Na zakończenie chciałbym wspomnieć, że Rust daje nam jeszcze jedną gwarancje – pozwala na poziomie języka kontrolować, czy obiekty danych typów mogą być między wątkami przesyłane i/lub współdzielone. Dzięki temu jeśli jakaś biblioteka z różnych powodów powinna zawsze być używana na jednym wątku, język daje nam narzędzie do wyrażenia tego w składni języka, a nie tylko w dokumentacji – nie będę jednak tego tematu rozwijał, bo jest to raczej bardziej zaawansowana część języka.
Odpowiada Paweł Romanowski, Senior Backend Engineer w Sauce Labs:
Wspomniałem już trochę o tym porównując wydajność Rusta do C++. Filozofia jest prosta: wszystko co powinno być zrównoleglone, może być zrównoleglone bez strachu. Jest też drugi filar filozofii Rusta, który trochę pokrywa się z tą w C++: w Ruscie jeśli chcemy, to mamy pełną kontrolę nad wątkami, bo Rust (z zasady) nic przed nami nie ukrywa i dokumentuje koszt związany z abstrakcjami. Rust faworyzuje abstrakcje o zerowym koszcie, tj. które kompilują się zupełnie do zera albo minimalnej możliwej reprezentacji aby wymusić gwarancje bezpieczeństwa. Zerowy koszt oznacza też to, że nieużywanie abstrakcji oznacza brak ponoszenia jakiegokolwiek kosztu z nią związanego. Jeśli bardzo chcemy, możemy zrezygnować z gwarancji bezpieczeństwa na rzecz wydajności w blokach “unsafe”.
Borrow checker dba o zachowanie inwariantów Rusta i bezpieczną wymianę danych między wątkami. Faworyzowane jest przekazywanie danych poprzez referencje lub transfer własności (zbliżone do “move” w C++, jednak każde przeniesienie powoduje że obiekt “znika” z miejsca z którego został przeniesiony dalej). Nie dzieje się to jednak samo: Rust wymaga nauki i po części zmiany modelu postrzegania “świata” tj. architektury oprogramowania i sposobów przekazywania i dzielenia danych między częściami programu. Wymaga też cierpliwości, bo kompilator często wytyka nam błędy nawet w najprostszych programach, które na pierwszy rzut oka nie powinny przejawiać problemów.
Nie powinno to nas jednak zniechęcać: kompilator nagradza naszą cierpliwość, prowadząc nas za rękę czytelnymi błędami i sugestiami. A kod, który już się skompiluje jest szybki i bezpieczny (gwarantowany brak problemów klasy memory-safety i thread-safety). Dodatkowym efektem ubocznym jest też (subiektywnie) lepsza struktura kodu. Poświęcamy więc czas na rozwiązanie problemów zawczasu, tzn. zanim program przysporzy problemów działając na produkcji. Jak dla mnie, jest to bardzo dobre podejście w niszy “systemów”, w którą celuje Rust, ale też w programowaniu w ogóle. Oprogramowanie w dzisiejszych czasach ma często złą sławę wśród użytkowników końcowych i Rust przedstawia kilka dobrych rozwiązań by tę sytuację nieco poprawić.
Przy okazji warto też napomknąć to, jak zostało zaimplementowane async/await: biblioteka standardowa języka zapewnia abstrakcje do budowania asynchronicznych programów (np. Future), jednak nie zapewnia “runtime”, czyli elementu wykonującego. Te możemy przebierać i wybierać, instalując bibliotekę która zapewni środowisko wywołania dla naszego asynchronicznego programu. Możemy dobrać taką, która spełnia wymagania naszego konkretnego use case. Możemy też używać wielu paradygmatów programowania współbieżnego w jednym programie, np. mieć thread pool do operacji na plikach i event loop do operacji sieciowych. Jest to niesamowita wolność i Rust jest jednym z niewielu języków które dają taką możliwość, przy zachowaniu dobrej ergonomii.
Odpowiada Michał Papierski, Founder w DELTA Solutions:
Siłą Rusta w programowaniu współbieżnym są jego gwarancje czasu kompilacji. Typy mogą, lub nie być bezpiecznie udostępniane do wewnątrz wątków (“Sync”) i te bezpiecznie przekazywane do wątków (“Send”). Wiele klas problemów jest już rozwiązywanych na tym etapie.
Przykładowo: jeśli potrzebujesz żeby twój obiekt miał zliczanie referencji, możesz użyć typu “Rc”, ale niestety nie możesz przekazać go do wątku, bo skończy się to błędem kompilacji – w tym celu możesz użyć typu “Arc”.
Jest również szereg “cegiełek” w bibliotece standardowej do programowania współbieżnego doskonale znane z innych języków (Mutex, CondVar, itp), ale również dobrze znane z Golanga kanały (“channels”) do wysyłania i odbierania informacji między wątkami.
Alternatywnie do moich przedmówców chciałbym również zwrócić uwagę na funkcjonalność async/await, który jest potężnym narzędziem języka, a która została ustabilizowana dość niedawno. Specjalne słówko kluczowe języka “async fn” sprawia, że dana funkcja “pod spodem” zostaje zamieniona w blok z funkcjami, które zwracają typ ”Future”, a mimo to cały czas kod wygląda jak synchroniczny. Do wykorzystania tej funkcjonalności potrzebujemy jeszcze executor, który zadba o wykonanie zadanych asynchronicznych bloków kodu na osobnych wątkach – jak np. async-std, lub tokio.
6. Warto uczyć się języka Rust?
Odpowiada Bartłomiej Kuras, Rust SW Developer in Metaswitch via Luxoft:
To jest pytanie, na który każdy musi sobie odpowiedzieć sam. Większość programistów uczy się nowych technologii przede wszystkim po to, żeby znaleźć w niej pracę, a nie ma co ukrywać – rynek Rusta jest w tym momencie niewielki – choć ostatnio się to bardzo zmienia, na korzyść programistów.
Warto przy tym pytaniu zwrócić jednak uwagę, że Rust uczy bardzo dobrych nawyków dotyczących zarządzania pamięcią, stałości obiektów, czy pokazuje jak implementować funkcyjne wzorce w języku imperatywnym (Rust nie jest żadną miarą językiem funkcyjnym!). Fakt, że w Ruscie nie ma typowej obiektowości nie wynika z widzimisię twórców tylko z tego, że od dawna widać w programowaniu trend rezygnowania z dziedziczenia na korzyść enkapsulacji, czy oddzielenie danych od zachowań z nimi związanymi (podczas gdy tradycyjne OOP silnie łączy typy, z zachowaniami jakie można na nich wykonać, widać w nowoczesnym programowaniu silne wpływy funkcyjne które zdają się ten trend odwracać).
Jeśli więc można mieć wątpliwości co do tego, czy nauka Rusta się wypłaci, uważam że sam fakt, że po nauce Rusta pozostaje nam w głowie borrow checker i nowe wzorce, poszerzy to nam zdecydowanie perspektywę i wpłynie tylko pozytywnie na jakość kodu pisanego w innych językach – także w C++. Jeśli więc mamy tylko na to czas i chęci, uważam że przynajmniej pod tym względem warto się tego języka uczyć.
Odpowiada Paweł Romanowski, Senior Backend Engineer w Sauce Labs:
Zdecydowanie warto! Przez wiele lat byłem sfrustrowany brakiem “świeżego powiewu” w świecie języków systemowych/kompilowanych. Mimo iż mam dużo doświadczenia w języku Python, programowanie na niższym poziomie zawsze było moją pasją i po prostu lubię wykorzystywać hardware do szczytu możliwości. W porównaniu do Pythona, pisanie C czy C++ nie należało do zbyt przyjemnych zajęć. Go był na celowniku przez chwilę, ale nie spełnił moich nadziei z różnych powodów.
Rusta obserwowałem od dawna, zanim jeszcze osiągnął wersję 1.0. Język we wczesnych fazach rozwoju przeszedł wiele iteracji i fundamentalnych zmian i zastanawiałem się, gdzie w końcu wyląduje. A wylądował dokładnie tam, gdzie powinien. Blisko metalu, jako konkurencja dla C++. Jest językiem, który rzeczywiście wnosi coś nowego, jednocześnie zapożyczając dużo dobrego od innych. Mamy więc elementy funkcyjne, generyki, pattern matching, tagged unions (Rust zwie to “enum”) i tak dalej. Całość tworzy bardzo spójny core języka, który jest mały, ale niesamowicie ekspresywny.
Jest to język, który jest zdecydowanie “współczesny” tzn. ma bardzo dobry tooling i mocno stawia na “developer experience”. Jedna komenda do zarządzania projektami Rusta, cargo, ma w zasadzie wszystko co developer zapragnie, plus można rozszerzać ją pluginami. Z jednego miejsca uruchamiamy testy (które są wbudowane w język), benchmarki, lintery, budujemy dokumentację czy publikujemy swoją paczkę. Jako że Rust jest tak młody, autorzy mieli szansę nauczyć się na błędach innych projektów i stworzyć ekosystem z którym aż chce się pracować.
Więc tak, warto. Polecam Rusta każdemu programiście z ambicjami.
Odpowiada Michał Papierski, Founder w DELTA Solutions:
Zdecydowanie tak!
Można śmiało powiedzieć, że język, jak i społeczność, która go używa są bardzo dojrzałe. Sam język również przez ostatnie lata był wielokrotnie ”odchudzany” i upraszczany tak, aby ułatwić adopcję. Jest również cały czas rozwijany przez Mozillę, jak i przez zaangażowaną społeczność.
Mimo sceptycyzmu już po paru tygodniach używania Rusta przywykłem do jego rozbudowanego ekosystemu narzędzi (cargo, clippy, rustfmt, …), i gwarancje, które otrzymuję zdecydowanie sprawiają że nie mam ochoty pisać w innym języku.
Jedną z najciekawszych funkcjonalności języka, którą bardzo doceniłem jest system typowania Hindleya-MIlnera, który sprawia, że typy są dopasowywane dopiero “na końcu” w momencie użycia – możemy stworzyć nowy wektor nie podając mu żadnego typu, i dopiero w momencie dodania do niego jakiegoś elementu kompilator nada mu konkretny typ. Sprawia to, że dobrze napisany kod tak naprawdę nie musi operować na typach, i mając trochę wprawy kod pisze się zaskakująco łatwo.
Dzięki tym wszystkim możliwością, które daje Rust w pewien sposób czuję się ”rozleniwiony” – także gorąco polecam naukę.
7. Postawiłbyś na Rust czy C++ w kolejnym projekcie?
Odpowiada Bartłomiej Kuras, Rust SW Developer in Metaswitch via Luxoft:
Jedyny przypadek, w którym rozważyłbym postawienie na C++ to pisanie na bardzo egzotyczną platformę, dla której nie istnieją kompilatory innych języków – a nawet w tym przypadku prawdopodobnie wybrałbym C (w takich sytuacjach spotykamy się z różnymi ograniczeniami które dla C++ mogą być problematyczne).
W każdym innym przypadku wybrałbym inny język, prawdopodobnie Rust, chociaż nie wykluczone, że w specyficznych sytuacjach byłby to język bardziej dopasowany do konkretnego projektu.
Odpowiada Paweł Romanowski, Senior Backend Engineer w Sauce Labs:
Zgadzam się z przedmówcą . Nie brałbym C++ pod uwagę, chyba że brakowałoby jakiejś kluczowej dla projektu biblioteki. Ale nawet wtedy, C++ nie byłby moim pierwszym wyborem. Koszt adopcji C++ jest ogromny i musi być dobrze uzasadniony. C++ to język, który pozwala osiągnąć maksymalną wydajność, który działa wszędzie i robi wszystko. Ale też język z dużymi historycznymi naleciałościami, niespójnościami, pełen pułapek i zasadzek. To wszystko to koszt. Koszt w tworzeniu oprogramowania, utrzymywaniu go i diagnozowaniu problemów.
Rust to język, który pozwala osiągnąć podobną wydajność, jednak w o wiele bezpieczniejszy sposób. Rust nie popełnia wielu błędów C++. W każdej szanującej się organizacji, gwarancje które czyni Rust powinny rezonować bardzo mocno. Mimo że jest to stosunkowo młody język, to bardzo obiecujący i powinniśmy dać mu szansę wszędzie tam, gdzie potrzeba wydajności, bezpieczeństwa i niezawodności.
Ostatnio miałem okazję brać udział w ogólnofirmowym hackathonie, gdzie w dwa dni naszemu zespołowi udało się napisać projekt w Ruście od zera, używając go po raz pierwszy w Sauce Labs. Osoby, które nie miały do tej pory do czynienia z językiem bardzo sobie chwaliły to doświadczenie. Z małą pomocą bardziej doświadczonych z Rustem członków zespołu, początkujący byli w stanie szybko stać się produktywni i kontrybuować nowe funkcjonalności. Projekt się udał i nawet zajął drugie miejsce!
Odpowiada Michał Papierski, Founder w DELTA Solutions:
Zdecydowanie rozważyłbym użyłcie Rusta w kolejnym projekcie. Być może jest to jeszcze niepopularna opinia, ale uważam, że w roku 2020 Rust, dzięki sile jego dojrzałego ekosystemu i narzędzi wokół niego nadaję się do wielu zastosowań tam gdzie do tej pory mógłby być rozważony C++, Python lub inne.
Rozważyłbym również adopcje Rusta do istniejącego kodu legacy pisanego w C lub C++. Z pomocą narzędzia “c2rust” jesteśmy w stanie przekonwertować istniejący gotowy kod C na Rust – kod wynikowy ma dużo elementów unsafe i bardziej przypomina “C pisane w Rust”, ale również z pomocą tego narzędzia jesteśmy w stanie go automatycznie zrefaktorować. Jeśli konwersja nie wchodzi w grę, możemy go również stopniowo integrować Rusta, poprzez linkowanie kodu legacy, np. poprzez opakowanie go, i stopniowe wymienianie starego kodu.
Organizacje na pewno docenią silne gwarancje, które zapewnia język, jak i wysoką wydajność kodu który jest generowany, i zaawansowane narzędzia które przekładają się na zwiększoną produktywność.
Mimo wsparcia wielu różnych architektur procesorów i systemów operacyjnych, nadal w pewnym stopniu Rust ograniczany jest przez użycie LLVM – i w związku z tym na pewno firmy, które rozwijają oprogramowanie embedded na naprawdę niszowe procesory, które LLVM jeszcze nie wspiera, mogą być zawiedzione.
Również te firmy, które pracują przy wyspecjalizowanych systemach medycznych, które wymagają specjalnych certyfikacji (np. więcej niż jedna implementacja, specyfikacja zachowania) mogą być zawiedzione – niestety, aktualnie nie jest to mocna strona, ale dzięki projektom takim jak Sealed Rust prawdopodobnie w przyszłości będzie można używać tego języka i do takich zastosowań.
8. Jak wygląda społeczność Rusta, a jak C++? Gdzie szybciej znajdę wsparcie w rozwiązaniu mojego problemu?
Odpowiada Bartłomiej Kuras, Rust SW Developer in Metaswitch via Luxoft:
Pytanie jest o tyle trudne, że nie umiem wiele powiedzieć o społeczności C++. Zawodowo zacząłem w nim pracować już 8 lat temu, a naukę języka zacząłem dużo wcześniej. Byłem wtedy nie tylko młody i raczej nieśmiały (więc nie chętny do pytania na forach), ale i sam dostęp do internetu był zupełnie inny niż dzisiaj (w tamtym czasie zasobów o Ruscie nie było w ogóle ;)). Było swego czasu polskie forum coderscity, na którym się aktywnie udzielałem, ale odnoszę wrażenie, że aktywnych tam było kilka osób – z jednej strony jak na forum Polskie wydawać by się mogło dużo, z drugiej mam wrażenie, że na naszym małym wrocławskim slacku rust-wrocław jest więcej ludzi, odpowiadających na różne pytania i aktywnie rozmawiających.
Jeśli chodzi o community Rusta, to jest bardzo przyjazne – dla nowych ludzi mamy forum users.rust-lang.org, gdzie bardzo szybko można znaleźć odpowiedź na swoje pytania, a dla tych, którzy chcieliby porozmawiać więcej o samych detalach kompilatora (np zaproponować RFC), jest forum internals.rust-lang.org. Także lokalni ludzie są bardzo pomocni – bardzo mocno zapraszam na naszego wrocławskiego slacka (także ludzi spoza Wrocławia) i wydarzenia Rust Wrocław (comiesięczne meetupy).
Natomiast każdy, kto chciałby pomóc w rozwoju języka może przygotować merge request zarówno dla kompilatora, co cargo, czy jakiejkolwiek biblioteki w ekosystemie – ludzie są z reguły bardzo pomocni w tym procesie.
Odpowiada Paweł Romanowski, Senior Backend Engineer w Sauce Labs:
Nie mogę się wypowiedzieć o społeczności C++, bo nie mam z nią wielu doświadczeń. Chociaż na jedno mogę zwrócić uwagę: C++ jest językiem sterowanym przez komisję. Przechodzi przez rygorystyczny proces standaryzacji i tempo zmian jest stosunkowo wolne. Nowości w standardach też wymagają czasu aby autorzy kompilatorów je zaimplementowali w pełni.
Rust natomiast jest sterowany całkowicie przez społeczność, a nowa wersja wychodzi co 6 tygodni. Rust zebrał wokół siebie wielu utalentowanych inżynierów oprogramowania, którzy nie boją się zmian nawet w najgłębszych czeluściach biblioteki standardowej. Jednocześnie Rust nie zrywa kompatybilności i jest bardzo stabilny, jak na częstotliwość releasów. Przytoczyłbym tu porównanie “waterfall” kontra “agile”, ale nie wiem czy jest na miejscu.
Co do społeczności Rusta: jest po prostu niesamowita. Jest to społeczność otwarta, pełna entuzjazmu i cierpliwości dla początkujących. Mogę tu przytoczyć własne doświadczenie commitowania do kompilatora Rusta. Obierając istniejący problem oznaczony jako “easy” zostałem przywitany i przydzielono mi mentora do pracy nad Pull Requestem. Osoba ta bardzo cierpliwie przeprowadziła mnie przez problem, sugerując rozwiązania i z uporem maniaka wytykała moje błędy. Sam kompilator Rusta ma dobrą dokumentację “developerską”, która bardzo się przydała. Kiedy już moje zmiany były gotowe i przeszły review a później trafiły do jednego z releasów, lista płac automatycznie powiększyła się o moje imię i nazwisko. W rezultacie nauczyłem się bardzo dużo i było to najlepsze moje doświadczenie z projektem open source do tej pory. Cały proces został dobrze przemyślany i maintainerzy dają bardzo dobry przykład wszystkim dookoła.
Bardzo lubię też Reddit Rusta. Jest to bardzo duża społeczność (105 tys. subskrybentów — dla porównania Reddit C++ ma 144 tys, jest po prostu więcej programistów C++ na rynku). Entuzjaści Rusta dzielą się tam swoimi projektami, newsami, blog postami, pomysłami a nawet okazjonalnie pytają “jak coś zakodować lepiej”. Być może jest to kwestia dobrej moderacji, ale nie spotkałem się z żadnym toksycznym zachowaniem. Warto tam zaglądać jeśli ktoś się interesuje tym językiem!
Rust ma też bardzo obszerną, wysokiej jakości dokumentację oraz materiały edukacyjne dostępne za darmo i wylistowane na swojej oficjalnej stronie. Społeczność odwaliła kawał dobrej roboty w tym zakresie i z Rustem jest bardzo łatwo zacząć, a także zgłębić zaawansowane tematy.
Odpowiada Michał Papierski, Founder w DELTA Solutions:
Patrząc na historyczne użycie obu języków to siłą rzeczy społeczność C++ jest zdecydowanie większa, co przełoży się również na szybsze otrzymanie pomocy w rozwiązywaniu potencjalnych problemów które mamy. Statystycznie nadal łatwiej jest znaleźć dewelopera C++ niż Rusta.
W Rust sprawy mają się trochę inaczej – społeczność jest na pewno mniejsza, aczkolwiek ilość materiałów i ludzie którzy są skupieni w internetowych społecznościach jest na tyle gęsta, że i również bez problemu otrzymamy pomoc, jeśli wiemy gdzie tej pomocy szukać. Warto zajrzeć na oficjalnego Discorda “The Rust Programming Language”, na StackOverflow (gdzie Rust wiele razy zdobył tytuł najbardziej lubianego języka!). Śledzę również inne kanały (np. Discord Tokio, kanały IRCowe), Warto również zadawać pytania wchodząc do repozytorium kodu z którego korzystamy i otwierając nowe zgłoszenie. Maintainerzy otwartych bibliotek są również chętni do pomocy.
NIe można zapomnieć o dokumentacji biblioteki standardowej, która jest bardzo obszerna i prawdopodobnie zawiera odpowiedzi na pytania które mogą się pojawić w trakcie pracy z językiem.
Zdjęcie główne artykułu pochodzi z unsplash.com.