Rust – kiedy warto?

Żyjemy w erze, kiedy nowe języki, frameworki, miodne biblioteki pojawiają się jak grzyby po deszczu. Nauka nowego języka wymaga inwestycji czasu. Jak w kontekście powyższego przedstawia się RUST? Czy i kiedy warto po niego sięgnąć?

Co jest istotne, gdy sięgamy po nowy język?

Zanim przystąpimy do analizy RUSTa, spróbujmy odpowiedzieć sobie na pytanie: czego potrzebujemy, by sięgnąć po nowy język:

  • Nauczyć się czegoś nowego np: nowego konceptu
  • Zrozumieć, o co chodzi z tym nowym hype’m
  • Rozwiązać problem, jaki mam w innym języku, bo w Javie, C# drażnią mnie te ograniczenia.

Ja, gdy zastanawiam się, czy warto sięgnąć po nowy język, najbardziej skupiam się na tym trzecim podpunkcie i szukam, jakie możliwości daje mi nowy język. To jest moja krótka checklista, którą przygotowałem specjalnie na potrzeby tego artykułu:

  • Informacji o zakresie przeznaczenia języka, czyli do jakich zastosowań będzie on dobrym rozwiązaniem?
  • W jakich paradygmatach można w nim programować?
  • Czy jest dobra dokumentacja wraz z przykładami?
  • Jakie jest wsparcie Community?
  • Jakie są dostępne narzędzia wspomagające pisanie kodu?
  • Jakie są dostępne biblioteki?
  • Czy język jest stabilny?

Przyjrzyjmy się, jak spisuje się RUST.

Co obiecuje RUST?

RUST jest systemowym językiem programowania. Reklamowany jest jako język szybki, zapobiegający naruszeniom pamięci, oraz jako język, który umożliwia bezpieczne operowanie wątkami.

Przyjrzyjmy się bliżej, co to tak naprawdę dla nas oznacza.

Rust jest językiem wieloparadygmatowym umożliwiającym programowanie funkcyjne, kompilowanym do natywnego kodu maszynowego ze statycznym typowaniem, co oznacza, że typy są znane i precyzyjnie określone na poziomie kompilacji. Kompilator nie jest tu tylko tłem, a bardzo ważnym pomocnikiem (o czym wspomnę później).

To co obiecuje Rust oznacza, że:

  • nie powinno być błędów związanych z zarządzaniem pamięcią
  • nie powinno być błędów z wyścigami wątków
  • rozszerzanie już istniejących bibliotek czy też tworzenie szybkich wielowątkowych aplikacji powinno być proste.

Co można więcej powiedzieć o RUSTcie porównując go z innymi językami:

  • Jeśli chodzi o szybkość działania, możemy porównywać go z C czy też C++. Można śmiało stwierdzić, że przy odpowiednim podejściu aplikacje napisane w RUSTcie mogą być tak samo szybkie, jak te napisane w C lub C++. W porównaniu z innymi językami dynamicznymi jest on zdecydowanie szybszy. RUST nie jest językiem tak niskopoziomowym jak C++, nie na wszystkich architekturach, typach procesorów będzie optymalizował się do takiego stopnia jak C++. Wszystko zależy od tego, czego potrzebujecie.
  • Rozpoczęcie pisania w tym języku wymaga czasu. Pisanie w tym języku w stopniu płynnym wymaga zmiany sposobu myślenia, zapoznania się z konstrukcjami, które mogą być z początku nieintuicyjne, w szczególności dla osób które pisały kod obiektowy (C++, C#) lub w językach wysokiego poziomu. Należy zacząć myśleć w inny sposób, ale moim zdaniem jest to warte rozważenia.
  • Jeśli tworzysz aplikację, która często alokuje i zwalnia zasoby pamięci i boisz się wycieków, to jest to odpowiedni język dla Ciebie. Kompilator nie stworzy aplikacji, która nie jest bezpieczna.
  • Jeśli boisz się wyścigów między wątkami, to podobnie jak w poprzednim przypadku kompilator zadba o brak występowania wyścigów.
  • Jeśli programujesz w językach takich jak JS, Python i potrzebujesz rozszerzenia do wykonywania obliczeń, gdzie liczy się prędkość, to RUST umożliwia w prosty sposób tworzenie bibliotek, które w prosty sposób można zaimportować.

Przyjrzyjmy się bliżej jego wadom i zaletom:

1. Ownership

To jest mój prywatny faworyt.

Jeśli programujecie w C++ to wiecie, jakie problemy możecie spotkać, gdy zapomnicie usunąć zadeklarowaną pamięć. Jeśli programujecie w C# czy Javie, to macie wspaniałego pomocnika – Garbage Collector – który, gdy popełnicie błąd lub o czymś zapomnicie, to posprząta po Was.

RUST wymusza inne podejście. Wymaga, by programista decydował, kiedy dana zmienna wychodzi z użytku i wtedy będzie kasowana.

Każda zmienna ma swojego “właściciela”, którym jest pewien zakres. Każda zmienna ma swój zakres żywotności (scope), po wyjściu z którego jest “kasowana”, czyli zwalniana jest pamięć.

Są tutaj dwa istotne mechanizmy:

  • Moving, czyli przekazywanie odpowiedzialności za zmienną
  • Borrowing, czyli pożyczanie zmiennej bez zmiany właściciela.

Jeśli programujesz w C++, to wiesz, że odpowiedzialność za kasowanie zaalokowanej pamięci leży na programiście. Musi w kodzie użyć delete’a. Jeśli tego nie zrobi, nic mu nie powie, że o czymś zapomniał. Wiąże się to z tym, że wycieki pamięci mogą wystąpić i będzie to wymagało np: profilowania aplikacji w celu ich znalezienia.

Jeśli programujesz w językach takich jak C# czy Java, mających wbudowany mechanizm garbage collector, który co jakiś czas, jeśli programista zapomni lub świadomie nie usunie zaalokowanego segmentu, usuwa nieużywane zmienne.

Ownership w Rust’cie dba o to na etapie kompilacji, dzięki czemu nie musimy bać się o wycieki pamięci. Dlatego mechanizm taki, jak garbage collector, który działa w runtimie, nie będzie nam tu potrzebny.

2. Ekspresywność

Czymże byłby język bez możliwości tworzenia nowych typów.

Poza typami prostymi, które są już tak ograne, że przez grzeczność je pominę, mamy możliwość tworzenia funkcji, wielolinijkowych wyrażeń lambda, definiowania traitów oraz struktur.

Trait to interfejs definiujący pewne metody, które musi zaimplementować struktura, która go implementuje.

Struktury to typy złożone, które mogą zawierać typy proste jak i inne typy złożone. Dodatkowo mogą implementować wcześniej wspomniane traity.

To czym się różni struktura od klas? Brzmi jak po prostu inna nazwa dla klasy.

Struktury nie mogą dziedziczyć po innych strukturach. Nie ma sposobu, aby zdefiniować strukturę, która dziedziczy pola i implementację metod struktury nadrzędnej.

Dzięki temu Rust uniemożliwia nam wielodziedziczenie, udostępnienia kodu, którego klasa nie potrzebuje do życia.

Bardzo użytecznym aspektem jest możliwość rozszerzania kodu w dowolnym miejscu przez to, że implementacja struktur metody może być realizowana gdziekolwiek w kodzie, a nie tylko w miejscu definicji klasy.

Jak to można wykorzystać? Wyobraź sobie, że definiujesz nowy trait i chcesz, by struktura go implementowała. Może ją rozszerzyć nie modyfikując biblioteki, w której jest ona zdefiniowana.

Jak każdy język funkcyjny ma dostępne mechanizmy dekonstrukcji obiektu oraz pattern matching.

Ograniczeniem jest, że możesz zdefiniować wewnętrzną implementację dla typu w tej samej paczce, gdzie typ został zdefiniowany.

3.Język bezpośrednio kompilowany do natywnego kodu maszynowego

I tu pojawia się pytanie: ile kosztuje wyżej wspomniana ekspresywność?

RUST jest językiem kompilowanym do natywnego kodu maszynowego. Jest umożliwia optymalizacje pod kątem architektury, na której działa kompilator. Umożliwia także cross-compiling (wykorzystując pod spodem LLVM) dzięki czemu można wyprodukować binarkę pod kątem innej architektury, optymalizowaną na hoście działającym w architekturze x86.

Używane abstrakcje takie jak traity, struktury istnieją tylko na etapie programowania. Są po to, by programiści tworzyli kod przyjemny dla oka i przede wszystkim czytelny dla innych programistów..

Kompilator pozbywa się tych abstrakcji na etapie kompilacji, by wytworzyć kod zoptymalizowany pod kątem prędkości działania. Można traktować abstrakcje, jakby ich koszt z punktu widzenia działania aplikacji był zerowy.

Kompilator zamienia go w kod maszynowy charakterystyczny dla maszyny, na której był kompilowany.

Zaletą takiego podejścia jest to, że tworzone przez nas aplikacje mogą być szybsze, lepiej zoptymalizowane oraz pozbywają się ograniczeń, jakie narzucają nam środowiska uruchomieniowe (JVM czy CLR). Jak ktoś ma ochotę błądzić po pamięci bez ograniczeń, to zapraszam. Jeśli ktoś ma ochotę zniszczyć całą zawartość dysku twardego, też jest to możliwe.

Jak to mówił Wujek Spidermana – z wielkimi możliwościami idzie wielka odpowiedzialność

4. Bezpieczeństwo wątków i tworzenie aplikacji wielowątkowej

Tworzenie aplikacji wielowątkowych może być problematyczne. Współdzielenie pamięci, wyścigi, synchronizacja wątków mogą skutecznie utrudniać życie.

Zacznijmy od najprostszego. RUST od samego początku był projektowany jako język, który ma zapewniać bezpieczeństwo w zakresie wielowątkowość.

Out of the box dostajemy mechanizmy tworzenia nowych wątków wbudowane w język.

Kompilator podobnie (jak w przypadku ownershipu) nie pozwoli, by aplikacja się skomplikowała, jeśli może dojść do niektórych negatywnych zjawisk. W runtimie nadal możliwe jest wystąpienie niektórych negatywnych zjawisk np: deadlockó.

Jeśli będziemy dla przykładu próbowali, by dwa wątki zmodyfikowały ten sam segment pamięci (co może doprowadzić do wcześniej wspomnianego wyścigu), to dostaniemy informacje już na etapie kompilacji.

W zakresie synchronizacji działania wątków możemy korzystać z wbudowanych mechanizmów Sync i Send, lub dodatkowe biblioteki udostępniają takie znane mechanizmy jak Channele, Arc, Mutex.

5. Pomocny kompilator

Kompilator jest tutaj ogromnym pomocnikiem. Pokazuje, gdzie dokładnie jest błąd i na czym on polega. Czasami daje także informację, jak naprawić dany błąd Jeśli tej informacji zabraknie, to dostajemy informacje o błędzie, który jest dość precyzyjnie opisany w dokumentacji.

Moim zdaniem kompilator jest dużą zaletą RUSTa. W sposób zrozumiały na etapie kompilacji pozwala wyeliminować dużą ilość problemów, odsyłając do helpa, który jest dobrze napisany. Nie pozwala stworzyć aplikacji, która nie jest bezpieczna w zakresie korzystania z pamięci, pracy z wątkami. Robi zdecydowanie więcej, niż kompilator z języków z którymi do tej pory się spotkałem (C, C++, C#, Java).

6. Łatwa rozszerzalność dla innych języków.

Dostępne są biblioteki, które umożliwiają aplikacji napisanej w RUSTcie integrację w łatwy sposób z innymi językami np: z JS (Node), Python.

Co zyskujemy dzięki temu? Zyskujemy korzyści obu języków. Dla przykładu: możemy wielowątkowo przetwarzać dane, wizualizując je w ulubionej bibliotece wysokiego poziomu, np. w Pythonie.

Dowód? Proszę uprzejmie:

Poniżej porównanie programu napisane w języku RUST oraz python z wykorzystaniem regex i pętli, którego celem jest przeanalizowanie stringu, by znaleźć ilość 2 segmentowych podciągów, składających się z identycznych znaków. Wyniki pochodzą z bardzo ciekawego artykułu, który porównuje także RUST z innymi dostępnymi rozwiązaniami dostępnymi w języku Python.

7. Wspomaganie wytwarzania

Out of the box dostajemy CARGO – menadżer pakietów – który działa podobnie jak NPM, Nuget, zarządza zależnościami i budujemy aplikację na bazie informacji o projekcie. W przypadku CARGO jego zadaniem jest skompilowanie.

Mamy do dyspozycji pokaźną bibliotekę crates.io. Mechanizm testów jednostkowych oraz bibliotekę standardową. Działa to bardzo wydajnie dzięki wydajnym bindingom do C.

Są to bardzo przyjemne w użytkowaniu i wydajne biblioteki do współbieżności, modelu aktorów, własnego api restowego.

Problemem może być, że większość z nich jeszcze się rozwija. O czym świadczyć może ich wersja (poniżej 1.0.0) oraz zmiany na poziomie interfejsu użytkownika biblioteki.

Co do IDE – ja używam Visual Studio Code wykorzystujące Rust Language Server oraz Racer do uzupełnienia kodu.

Podsumowanie:

RUST jest językiem funkcyjnym, systemowym oraz statycznie typowanym.

Jest stabilny, ale ewoluuje i to moim zdaniem w dobrą stronę.

Ma sporo użytecznych mechanizmów, takich jak: Ownership, Bezpieczeństwo wątków, Traity, Struktury, Pattern Match.

Wymaga też zmiany sposobu myślenia i podejścia oraz wzięcia przez programistę większej odpowiedzialności.

Pomocna może się okazać znajomość wzorców projektowych oraz innych języków funkcyjnych. W moim przypadku to pomogło.

O tym, czy język jest stabilny, mogą świadczyć jego produkcyjne wdrożenia. Można znaleźć go w : Servo (silnik przeglądarki internetowej), kompilator Rusta oraz przeglądarka Firefox,

W dzień Senior Software Developer w firmie Future Processing, w nocy śpi. Ponad 8 lat doświadczenia w zakresie wytwarzania oprogramowania w różnych technologiach oraz domenach, również w takich, w których nikt nie chciał pracować. Zafascynowany rozwojem technologii związanej z przetwarzaniem danych a w szczególności tworzeniem rozwiązań z rodziny Big Data. Prelegent oraz organizator licznych wydarzeń, których głównym celem jest dzielenie się wiedzą oraz krzewienie potrzeby stosowania dobrych praktyk, w celu maksymalizacji jakości wytwarzanego produktu. Współorganizator Wakacyjnych Praktyk w Future Processing oraz prowadzący przedmiot na Politechnice Śląskiej „Tworzenie Oprogramowania w Zmiennym Środowisku Biznesowym”.
PODZIEL SIĘ