Dlaczego poniższy kod się kompiluje?

use std::thread;

struct Chinchilla(&'static str);

fn main() {
    let chinchilla = Chinchilla("Flora");

    thread::spawn(move || {
        println!("{}", chinchilla.0);
    });
}
Note

Ten artykuł opisuje koncept Rustowych auto traits, by następnie pokazać jak w oparciu o nie można wykonać sztuczkę w zachowaniu przypominającą inny mechanizm tego języka, specjalizację.

Na potrzeby artykułu zakładam, że znasz Rust przynajmniej na tyle, że rozumiesz przytoczony powyżej kod - resztę konceptów staram się opisać w artykule.

Jeśli nie masz ochoty na czytanie wywodów, możesz przeszkoczyć bezpośrednio do ostatniej sekcji - Kod :-)

where T: Send

Patrząc na przytoczony wyżej kod można by prędzej wysunąć pytanie no a dlaczego miałby się nie kompilować??, stąd też by zrozumieć istotę oryginalnego pytania, naszą przygodę powinniśmy zacząć od rzucenia okiem na definicję std::thread::spawn:

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
    F: FnOnce() -> T,
    F: Send + 'static,
    T: Send + 'static,
{
    // ...
}

Wśród całego szumu (ach te typy!), naszą uwagę może przykuć bound: F: Send.

Send jest traitem wykorzystywanym do rozróżniania tych typów, które mogą przekroczyć tzw. thread boundary - innymi słowy: wartości typów implementujących Send mogą być utworzone w wątku A, a następnie bezpiecznie przeniesione (ang. moved) do wątku B i tam zwolnione (ang. dropped).

Większość wartości (np. typu String czy Vec<u8>) może być przenoszona między wątkami bez żadnego problemu - istnieje jednak parę typów, które wymagają dodatkowej uwagi - np. Rc.

Rc udostępnia zachowanie podobne do odśmiecacza (ang. garbage collector): kiedy wywołujemy Rc::clone(), wartość trzymana wewnątrz Rc nie zostaje tak naprawdę sklonowana - zamiast tego, Rc zawiera wewnątrz siebie licznik opisujący liczbę obecnie żywych instancji Rc (gdzie Rc::clone() zwiększa ten licznik o jeden, a Rc::drop() - zmniejsza). Gdy licznik żywych instancji spada poniżej zera (tj. gdy ostatni Rc zostaje zwolniony), wtedy dopiero wartość trzymana wewnątrz Rc zostaje usunięta z pamięci.

Rc, w przeciwieństwie do Arc, wykorzystuje nie-atomowy (ang. non-atomic) licznik - oznacza to, że ten licznik nie może być wykorzystywany (np. czytany bądź zapisywany) z wielu wątków w tej samej chwili; Rust pilnuje tego inwariantu poprzez sprawienie, że Rc nie implementuje Send, dzięki czemu nie jest możliwe wykorzystanie Rc z wielu wątków:

use std::rc::Rc;
use std::thread;

fn main() {
    let value = Rc::new(
        "c-rustacean is a Rust programmer who likes C better"
    );

    // ok: `::clone()` dzieje się na tym samym wątku co `::new()`
    let value2 = Rc::clone(&value);

    thread::spawn(move || {
        drop(value2); // błąd
    });
}

// error[E0277]: `Rc<&str>` cannot be sent between threads safely
//    --> src/main.rs:12:5
//     |
// 12  |       thread::spawn(move || {
//     |  _____^^^^^^^^^^^^^_-
//     | |     |
//     | |     `Rc<&str>` cannot be sent between threads safely
// 13  | |         drop(value2); // błąd
// 14  | |     });
//     | |_____- within this `[closure@src/main.rs:12:19: 14:6]`
//     |
//     = help: within `[closure@src/main.rs:12:19: 14:6]`, the
//             trait `Send` is not implemented for `Rc<&str>`
//     = note: required because it appears within the type
//             `[closure@src/main.rs:12:19: 14:6`

Choć komunikat może być enigmatyczny (zwłaszcza dla osób, które nie miały do czynienia z wielowątkowym Rustem), najistotniejszą jego częścią jest: Rc<&str> cannot be sent between threads safely.

Aby nasz kod zadziałał, powinniśmy wykorzystać Arc<&str> - jest to typ funkcjonalnie podobny do Rc, z tą różnicą, że wykorzystuje pod spodem atomowy licznik (a zatem i implementuje Send).

OIBIT

Jak dotąd przeszliśmy przez rolę traita Send w bibliotece standardowej Rusta, lecz tak właściwie najciekawszą rzeczą (a przynajmniej najciekawszą na potrzeby tego artykułu) jest to, że jest on implementowany automatycznie przez kompilator!

use std::fmt::Display;

struct Chinchilla(&'static str);

fn assert_display(_: impl Display) {
    //
}

fn assert_send(_: impl Send) {
    //
}

fn main() {
    assert_display(Chinchilla("Fauna"));
    assert_send(Chinchilla("Flora"));
}

// error[E0277]: `Chinchilla` doesn't implement `Display`
//   --> src/main.rs:15:20
//    |
// 5  | fn assert_display(_: impl Display) {
//    |                           ------- required by this bound
//    |                                   in `assert_display`
// ...
// 14 |     assert_display(Chinchilla("Flora"));
//    |                    ^^^^^^^^^^^^^^^^^^^

Jako że nie określiliśmy impl Display for Chinchilla, wywołanie assert_display(…​) nie jest możliwe - 1:0 dla kompilatora.

Nie określiliśmy jednak również impl Send for Chinchilla - czy assert_send(…​) nie powinno zatem również zwrócić podobnego błędu?

Jak się okazuje, niektóre z Rustowych traitów są opt-out zamiast opt-in - to jest: niektóre traity są implementowane automatycznie dla wszystkich typów dopóki my (lub kompilator) nie wskażemy wprost impl !Trait for Type (tak, z wykrzyknikiem).

Takie traity są nazwane OIBIT, od opt-in built-in traits - ostatecznie zostały one przemianowane na auto traits, stąd też w dalszej części artykułu będziemy wykorzystywali tę drugą nazwę.

Auto traits

Zwyczajne traity są opt-in, tj. nie mają zastosowania dopóty, dopóki nie określimy impl Trait for Type:

trait Foo {
    //
}

impl Foo for &str {
    //
}

fn test(_: impl Foo) {
    //
}

fn main() {
    test(123); // błąd: the trait bound ... is not satisfied
    test("hi!"); // ok
}

Auto traits, z drugiej strony, są opt-out:

#![feature(auto_traits)]
#![feature(negative_impls)]

auto trait Foo {
    //
}

impl !Foo for &str {
    //
}

fn test(_: impl Foo) {
    //
}

fn main() {
    test(123); // ok
    test("hi!"); // błąd: the trait bound ... is not satisfied
}

Jako że auto traity nie mogą mieć ani metod, ani associated items:

auto trait Foo {
    type Type; // błąd
    fn function(&self); // błąd
}

... pełnią one funkcję tzw. marker traits.

O ile przeznaczeniem zwyczajnych traitów jest określanie zachowań (np. poprzez metody), marker traits służą określaniu właściwości wartości danego typu.

Przykładem marker traitu może być właśnie Send, jako że służy on wyłącznie do określania czy wartość danego typu może być przeniesiona do innego wątku, bez definiowania jakiegokolwiek zachowania samemu w sobie (tj. Send istnieje wyłącznie jako swego rodzaju pomoc dla kompilatora).

Możemy zobaczyć definicję Send w bibliotce standardowej:

pub unsafe auto trait Send {
    // empty.
}

... dodatkowo, w pliku std/alloc/rc.rs znajdziemy:

impl<T: ?Sized> !Send for Rc<T> {}

Jak na dłoni - zero magii!

Aby zakończyć tę część poświęconą auto traitom, przejdźmy jeszcze tylko przez najistotniejszą regułę dotyczącą tego mechanizmu: aby jakiś typ implementował dany auto trait, żadne jego pole nie może być typu, który został impl !, tj.:

#![feature(auto_traits)]
#![feature(negative_impls)]

auto trait Arbitrary {
    //
}

impl !Arbitrary for &str {
    //
}

// implementuje `Arbitrary`
struct Yass;

// implementuje `Arbitrary`
struct Foo {
    value: usize,
}

// nie implementuje `Arbitrary`
struct Bar {
    value: &'static str,
}

// nie implementuje `Arbitrary`, gdyż `value_2` jest typu `Bar`,
// który nie implementuje `Arbitrary`
struct Zar {
    value_1: Foo,
    value_2: Bar,
}

(jak zwykle, wszystko zostało spisane w odpowiednim RFC.)

Specjalizacja

Zapomnijmy na chwilę o całej tej nudnej wiedzy z poprzedniego rozdziału i wyobraźmy sobie, że zamiast tego jesteśmy w Dolinie Krzemowej, rozpoczynając nowy start-up - z całą pewnością pierwszym, co musimy zrobić, jest wynalezienie całkowicie unikalnego formatu danych: jesteśmy too cool na babranie się z XMLem, a i JSON jest już pieśnią przeszłości (No Code, ktoś, coś?).

Otwieramy zatem emacs, pisząc linijki, które zostaną pierwszymi trzema naszego nowego, monolitycznego mikroserwisu:

trait Serialize {
    fn serialize_in_place(&self, buffer: &mut String);
}

Póki mamy chęci, dopiszmy blanket impl dla metody serialize(), dzięki czemu będziemy mogli wszystko łatwo przetestować:

trait Serialize {
    fn serialize_in_place(&self, buffer: &mut String);

    fn serialize(&self) -> String {
        let mut buffer = String::new();
        self.serialize_in_place(&mut buffer);
        buffer
    }
}

Nasi inwestorzy mówią, że będziemy przetwarzać dużo booleanów, więc zacznijmy od implementacji serializatora właśnie dla nich:

impl Serialize for bool {
    fn serialize_in_place(&self, buffer: &mut String) {
        if *self {
            buffer.push_str("b(true)");
        } else {
            buffer.push_str("b(false)");
        }
    }
}

#[test]
fn test_bool() {
    assert_eq!("b(true)", true.serialize());
    assert_eq!("b(false)", false.serialize());
}

Jeden z inwestorów nie mógł przestać rozmawiać o stringach, więc:

impl Serialize for &str {
    fn serialize_in_place(&self, buffer: &mut String) {
        buffer.push_str("s(");
        buffer.push_str(self);
        buffer.push_str(")");
    }
}

#[test]
fn test_str() {
    assert_eq!("s(hummus)", "hummus".serialize());
}

Oczywiście, jako że jesteśmy profesjonalnymi programistami, pojedynczy bool czy &str na nic się nam nie zdadzą - Vec<T> będzie ukryty w każdym zakamarku domeny:

impl<T> Serialize for Vec<T> where T: Serialize {
    fn serialize_in_place(&self, buffer: &mut String) {
        buffer.push_str("v(");

        for (item_idx, item) in self.iter().enumerate() {
            if item_idx > 0 {
                buffer.push_str(", ");
            }

            item.serialize_in_place(buffer);
        }

        buffer.push_str(")");
    }
}

#[test]
fn test_vec() {
    assert_eq!(
        "v(b(true), b(false))",
        vec![true, false].serialize(),
    );

    assert_eq!(
        "v(s(foo), s(bar))",
        vec!["foo", "bar"].serialize(),
    );
}

Najs - nasz kod, choć nieco prymitywny, to już potrafi obsłużyć nieskończoną liczbę typów: bool, &str, Vec<bool>, Vec<&str>, Vec<Vec<…​>> i tak dalej.

Póki pieniądze spływają z niebios, mamy chwilkę na optymaliację naszego formatu poprzez dodanie specjalnej, zwięzłej implementacji dla Vec<bool>.

To jest: zamiast serializować vec![true, true, false, false] do v(b(true), b(true), b(false), b(false)), moglibyśmy wykorzystać bitmaskę: vb(12) (12_dec = 1100_bin).

Nie ma co czekać - dorzućmy nowy impl:

impl Serialize for Vec<bool> {
    fn serialize_in_place(&self, buffer: &mut String) {
        todo!()
    }
}

// error[E0119]: conflicting implementations of trait `Serialize`
//               for type `Vec<bool>`:
//   --> src/lib.rs:45:1
//    |
// 29 | impl<T> Serialize for Vec<T> where T: Serialize {
//    | -------------------------------- first implementation here
// ...
// 45 | impl Serialize for Vec<bool> {
//    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation
//                                   for `Vec<bool>`

... i, chwila, moment, co tu się stało?

Kompilator zauważył, że nasza nowa implementacja Vec<bool> nakłada się na istniejącą już Vec<T>, przez co później nie byłby on w stanie wywnioskować do której metody powinno prowadzić wywołanie .serialize_in_place().

Nightly Rust oferuje rozwiązanie tego problemu: mechanizm nazwany specjalizacją (ang. specialization).

Specjalizacja umożliwia oznaczanie wybranych metod oraz associated items jako default, dzięki czemu możliwe staje się ich późniejsze nadpisanie w innych miejscach kodu - w naszym wypadku powinniśmy mieć:

impl<T> Serialize for Vec<T> where T: Serialize {
    default fn serialize_in_place(&self, buffer: &mut String) {
        // to jest domyślna implementacja dla wszystkich `Vec`-ów
    }
}

impl Serialize for Vec<bool> {
    fn serialize_in_place(&self, buffer: &mut String) {
        // to jest implementacja przeznaczona dla `Vec<bool>`
    }
}

Specjalizacja jest najlepszym wyjściem z tego problemu, choć nie jedynym - podobny efekt możemy osiągnąć z wykorzystaniem omówionych wcześniej auto traits.

Specjalizacja w oparciu o auto traits

Skoro bolączką naszej obecnej implementacji:

impl<T> Serialize for Vec<T> where T: Serialize {
    /* ... */
}

... jest to, iż koliduje ona z Vec<bool> (jako że bool: Serialize), tym, co chcielibyśmy osiągnąć jest mniej-więcej:

impl<T> Serialize for Vec<T> where T: Serialize, T != bool {
    /* ... */
}

Mimo iż Rust nie wspiera operatora != w tej pozycji, podobny efekt możemy osiągnąć za pomocą auto traitów; na początek stwórzmy sobie nowy:

auto trait BlanketVecImpl {
    //
}

... i od-implementujmy go dla bool:

impl !BlanketVecImpl for bool {
    //
}

Mając ten trait, możemy dostosować naszą generyczną implementację Vec<T>:

impl<T> Serialize for Vec<T> where T: Serialize + BlanketVecImpl {
    /* ... */
}

Voilà - dzięki temu stworzyliśmy generyczny impl dla wszystkich Vec<T> z pominięciem Vec<bool>, który teraz możemy bez problemu określić:

impl Serialize for Vec<bool> {
    /* ... */
}

Kod

Całość opiera się o dwa mechanizmy dostępne w nightly: auto_traits oraz negative_impls, i choć wolałbym uniknąć pracowania z takim rozwiązaniem w produkcyjnym kodzie, to przedstawia ono niemałą wartość edukacyjną, a i sam proces dojścia do tego rozwiązania był warty poświęconego czasu:

#![feature(auto_traits)]
#![feature(negative_impls)]

trait Serialize {
    fn serialize_in_place(&self, buffer: &mut String);

    fn serialize(&self) -> String {
        let mut buffer = String::new();
        self.serialize_in_place(&mut buffer);
        buffer
    }
}

mod bool {
    use super::*;

    impl Serialize for bool {
        fn serialize_in_place(&self, buffer: &mut String) {
            if *self {
                buffer.push_str("b(true)");
            } else {
                buffer.push_str("b(false)");
            }
        }
    }

    #[test]
    fn test_bool() {
        assert_eq!("b(true)", true.serialize());
        assert_eq!("b(false)", false.serialize());
    }
}

mod str {
    use super::*;

    impl Serialize for &str {
        fn serialize_in_place(&self, buffer: &mut String) {
            buffer.push_str("s(");
            buffer.push_str(self);
            buffer.push_str(")");
        }
    }

    #[test]
    fn test_str() {
        assert_eq!("s(hummus)", "hummus".serialize());
    }
}

mod vec {
    use super::*;
    use std::fmt::Write;

    pub auto trait BlanketVecImpl {
        //
    }

    impl !BlanketVecImpl for bool {
        //
    }

    impl BlanketVecImpl for Vec<bool> {
        //
    }

    impl<T> Serialize for Vec<T> where T: Serialize + BlanketVecImpl {
        fn serialize_in_place(&self, buffer: &mut String) {
            buffer.push_str("v(");

            for (item_idx, item) in self.iter().enumerate() {
                if item_idx > 0 {
                    buffer.push_str(", ");
                }

                item.serialize_in_place(buffer);
            }

            buffer.push_str(")");
        }
    }

    impl Serialize for Vec<bool> {
        fn serialize_in_place(&self, buffer: &mut String) {
            let mut bits = 0u8;

            if self.len() > 8 {
                unimplemented!("what is this, big-data?");
            }

            for (item_idx, &item) in self.iter().rev().enumerate() {
                if item {
                    bits |= 1 << item_idx;
                }
            }

            write!(buffer, "vb({})", bits).unwrap();
        }
    }

    #[test]
    fn test_vec() {
        assert_eq!(
            "vb(12)",
            vec![true, true, false, false].serialize(),
        );

        assert_eq!(
            "v(vb(2), vb(1))",
            vec![vec![true, false], vec![false, true]].serialize(),
        );

        assert_eq!(
            "v(s(foo), s(bar))",
            vec!["foo", "bar"].serialize(),
        );
    }
}