Wpis z mikrobloga

Rust dla (t)opornych

Część 1. Po kiego chu… grzyba, czyli co wyróżnia Rusta z pośród reszty.

Już jakiś czas temu napisałem wpis na Mirko gdzie porównywałem Rusta i parę innych "nowoczesnych" języków, tutaj się trochę rozpiszę i przy okazji dam małe wprowadzenie co i jak.

Rozdział 1. Niezmienność jest domyślna

W językach takich jak C, C++, C#, Java niezmienność jest wyborem. Wszystkie zmienne są w domyśle mutowalne. Ma to swoje zalety jako, że nasz mózg przyzwyczajony jest do tego iż zmienną można zmieniać od momentu jej powstania. Jest parę języków, które tego nie wspierają i są to głównie języki funkcyjne (z wyjątkiem - Scalą). W Ruscie to, że zmienna ma być mutowalna jest kwestią wyboru, nie na odwrót.

Przykład:

let a = 10;
a = 20; // błąd

let mut a = 10; // absolutnie legalne w Ruscie, można przesłonić zmienną w tym samym bloku
a = 20; // OK

Takie założenie pozwala w teorii kompilatorowi na trochę większą optymalizację kodu, a dodatkowo wymusza lepsze przemyślenie programu przez programistę.

Rozdział 2. Bezpieczeństwo pamięci

Do dnia dzisiejszego były 2 wybory jeśli chodziło o zarządzanie pamięcią w programie (jeśli się mylę to niech ktoś mnie oświeci a odszczekam):

+ ręczne - uciążliwe z wiadomych powodów: łatwo o wyciek, słabo współpracuje z wątkami i wyjątkami, dość niebezpieczne (patrz GHOST/Heartbleed)
+ GC - wygodne, ale problemy z wydajnością (global lock przy załączeniu się czyszczenia) czy wielowątkowością (jak uruchomić GC na wielowątkowym procesie)

Świętym Graalem hakerów było jednoczesne uzyskanie wydajności i "niskopoziomowych" zabawek przy jednoczesnym zapewnieniu bezpieczeństwa pamięci. C++11 jako tako próbuje sobie z tym poradzić przy pomocy nowych *inteligentnych*
(inaczej) wskaźników jednak ma to ten problem, że wymaga dalej uwagi programisty (trzeba ręcznie pozmieniać wszystkie wskaźniki na te wynalazki) oraz słabo współpracuje z istniejącymi bibliotekami.

Rust wpadł na rozwiązanie: lifetime. Jest to dodatkowy atrybut przypinany do każdej referencji, który określa kiedy kończy się "czas życia" zmiennej. Ten system pozwala na określenie w czasie kompilacji czy istnieje możliwość, by dana referencja wskazywała na zwolnioną pamięć.

Zagadka: Czy dany program w C++ jest poprawny?

#include
#include

int main() {
std::vector vec{};
vec.pushback(42);

int& ref = vec[0];

vec.push
back(69);

std::cout << ref;
}

Odpowiedź i wyjaśnienie (o ile nie będzie w komentarzach) przeczytacie w kolejnej części, gdzie postaram się przybliżyć lifetime i resztę zarządzania pamięcią z Rusta.

Rozdział 3. Typowanie

Typy w językach kompilowanych są dość istotną sprawą - w końcu zapewniają nam bezpieczeństwo kodu. Nie inaczej jest w Ruscie. Z pewnymi wyjątkami.

Wyjątek 1 - naprawdę silne typowanie.

C czy C++ mimo iż mają namiastki silnego typowania to są absolutnie szczęśliwe w przypadku takiego kodu (nie ma nawet upomnienia):

#include

int main() {
int a = 10;
unsigned int b = a;

printf("%d\n", b);

return 0;
}

Czemu brak upomnienia jest tutaj zły? Bo ani C ani C++ nie określają w jaki sposób obsłużyć moment kiedy a < 0. Możliwe, że większość z was teraz mi powie, że U2, ale standard nigdzie nie zapewnia, że liczby mają być kodowane w U2. Mogą
być zakodowane w dowolny sposób. Rust nie dopuści do takiej profanacji, co więcej, nie dopuści nawet do sytuacji, która w teorii nie jest szkodliwa:

fn main() {
let a: i64 = 10;
let b: i32 = a; // błąd kompilacji, mimo iż w teorii nie tracimy żadnych
// danych (standard zapewnia kodowanie U2)

println!("{}", b);
}

Wyjątek 2 - system typów Henleya-Milnera.

Jest to taki sam system typów jak w np. Haskellu. Czym on się różni od auto (C++) lub var (C#/Scala)? Ano tym, że w przypadku tamtych języków typ jest definiowany w miejscu, tzn. że w momencie deklaracji cały typ zmiennej musi być znany.
Przykład (tym razem C#):

var list = new List();
list.Add(10);
list.Add(20);

Gdzie T musi być znane w momencie wywołania tej linii. Kompilator nie potrafi sam "wydedukować" typu zmiennej.

Przykład w Ruscie:

let mut vec = Vec::new(); // nie określamy typu zmiennych w wektorze
vec.push(10); // tutaj jest on określany jako typ całkowity
vec.push(20);

println!("{:?}", vec); // => [10, 20]

// z racji, że nie ma dokładnego określenia jaki to ma być typ zmiennej całkowitej
// to kompilator przyjmuje, że jest to isize czyli domyślny typ całkowitoliczbowy
// dla danej platformy

Typy zmiennych są dedukowane w czasie kompilacji na bazie całego życia zmiennej. W ten sposób programista nie musi się martwić wyjątkiem 1. (w większości przypadków).

Rozdział 4. Brak dziedziczenia i system traitsów

Rust nie wspiera dziedziczenia. Jakiegokolwiek. W dowolnej postaci. Kropka.
Zawsze można użyć kompozycji.

W zamian mamy system traitsów, który jest formą interfejsów (i działa podobnie jak w Scali). By się nie rozpisywać, przykładzik:

struct Foo(u32); // Struktura zawierająca tylko 1 anonimowe pole o typie u32
impl Add for Foo { // operator +
type Output = Foo;

fn add(self, other: Foo) -> Foo {
let Foo(this) = self;
let Foo(other) = other;

Foo(this + other) // (prawie) każde wyrażenie w Ruscie zwraca wartość, brak ; na końcu funkcji oznacza, że to jest wartość zwracana
}
}

println!("{:?}", Foo(10) + Foo(20)); // => Foo(30)

Rozdział 5. Pattern matching, typy wyliczeniowe, monady i obsługa błędów

Jeśli ktoś z was bawił się kiedyś językami funkcyjnymi to jest świadomy opcji pattern matchingu w takowych językach. Jest to swoistego rodzaju odpowiednik bloku switch dla ludzi z C/C++. Przykład

enum Foo {
Bar,
Baz(u32) // w Ruscie typy wiliczeniowe mogą nieść wartość (mogą być monadami)
}

let a = Foo::Baz(10);

match a {
Foo::Bar => println!("Take me to the Gay Bar"),
Foo::Baz(val) => println!("Nuke Bar with {} nukes", val)
};

Z racji, że Rust nie posiada wyjątków obsługa błędów jest obsługiwana przez monadę Result:

let num = -1.0;

let sqrt = match num.sqrt() {
Ok(val) => val,
Err(err) => panic!("{}", err)
}

Epilog części 1.
To by było chyba na tyle. Najważniejsze różnice zostały przedstawione. Oczywiście, z racji, że Rust jest mocno funkcyjny, nie mogło w nim zabraknąć lambd, ale to omówię innym razem (najprawdopodobniej a propos wielowątkowości).

#programowanie #rustlang #naukaprogramowania

Ciąg dalszy będzie pod tagiem #haulethuczyrusta

I wołam zainteresowanych @Acrene, @siepet, @yonah, @kuhar, @pp555, @sosnnaa, @kao3991, @tytyryty, @Aysorth, @Felonious_Gru, @Existanza, @kornik20082, @GlenPL, @notauser, @Analityk, @mpisz, @Seima, @Pietrek558, @gress

Jak kogoś zapomniałem to trudno.
  • 10
Zagadka: Czy dany program w C++ jest poprawny?


@Hauleth: implementation defined ;)

Bo ani C ani C++ nie określają w jaki sposób obsłużyć moment kiedy a < 0. Możliwe, że większość z was teraz mi powie, że U2, ale standard nigdzie nie zapewnia, że liczby mają być kodowane w U2. Mogą

być zakodowane w dowolny sposób.


@Hauleth: Mówią, cytatem rzucę za kilka minut.

Rust nie wspiera dziedziczenia. Jakiegokolwiek. W dowolnej
@Hauleth: N3797, §4.7/2 [conv.integral]:

If the destination type is unsigned, the resulting value is the least unsigned integer congruent to the source integer (modulo 2^n where n is the number of bits used to represent the unsigned type). [ Note: In a two’s complement representation, this conversion is conceptual and there is no change in the bit pattern (if there is no truncation). —end note ]

tl;dr: dodajesz lub odejmujesz
KrzaQ2 - @Hauleth: N3797, §4.7/2 [conv.integral]:
 If the destination type is unsigne...

źródło: comment_C8PnNdVCot3APj1U2mmCTb4Kr5mzTr8q.jpg

Pobierz
@KrzaQ2: Co do zagadki to zła odpowiedź.

Co do liczb, to rzeczywiście, zwracam honor. C++ musiał jak zwykle #!$%@?ć.

O "braku dziedziczenia" rozpiszę się później i wtedy dasz znać czy to taka wada. Bo rozwiązane jest to na tyle sprytnie, że ciężko to uznać za wadę ;) Chyba, że opiszesz, czemu to aż taka wada.
@Hauleth: Hm, serio? Byłem przekonany, ƶe standard nie nakłada ograniczeń na mnoƶnik alokacji wektora. Jutro sobie poczytam konkretniej.

@​dziedziczenie: jutro opiszę mój use-case, moƶe źle na to patrzę i moƶna to ładniej zrobić (inna sprawa, ƶe nie wiem jak to wygląda w ruście, więc moƶe faktycznie jest dostatecznie sprytne)
@KrzaQ2: Nie ma narzuconego mnoznika przy zwiekszaniu rozmiaru wektora (poza takim, ze k>1). Jezeli dobrze pamietam, VS uzywa k=1.5, gcc k=2. Natomiast co do domyslnego rozmiaru poczatkowego vektora, to nie spotkalem sie z innym niz 1 po pierwszym push_backu.