Jak oni rdzewieją (Advent of Code 2021)

9

Wrzucam na githuba moje mizerne próby z Rustem w ramach AoC, więc jeśli ktoś chciałby się popastwić i wyjaśnić czemu to wszystko jest źle, to zapraszam: https://github.com/Pharisaeus/AoC2021/tree/master/src :)

edit: dla jasności nie przerobiłem nawet żadnego kursu Rusta, odpalam IntelliJ i pisze "na czuja" :D

5

Łooo panie, ile kodu. Nawet traity implementujesz. Jest sens implementować traity jak i tak nie potrzebujesz polimorfizmu? (Chyba że potrzebujesz, tylko okiem z komórki rzuciłem). Ja wiem że w OOP się trzecie te interfejsy i klasy czy jest to potrzebne czy nie (wyjątkiem jest chyba tylko Kotlin) ale czy w Ruscie tez jest taka konwencja?

1
KamilAdam napisał(a):

Łooo panie, ile kodu. Nawet traity implementujesz. Jest sens implementować traity jak i tak nie potrzebujesz polimorfizmu?

Gdzie te implementacje traitów? impl Cośtam { ... } to nie jest implementacja traitu. Implementacja traitu to raczej coś a'la impl TraitName for StructName { ... }.

4
  1. Ostatnia instrukcja jest niejako "auto return", tzn. wystarczy:

    fn part1(commands: &[Command]) -> i32 {
        /* ... */
        pos * depth
    }
    

    Explicit return wykorzystywany jest jedynie w sytuacjach early-exit, wyjść z pętli itd.

  2. Dzięki temu, że pętla for item in items desugaruje się do:

    let items = items.into_iter();
    
    while let Some(item) = items.next() {
        /* ... */
    }
    

... oraz że istnieje IntoIterator dla &[T], wystarczy np. for command in commands {, bez wprost .iter().

  1. fn part1_fun(commands: &[Command]) -> i32 {
        let final_state = commands.iter()
            .fold(State1 { pos: 0, depth: 0 }, move_sub1);
        return final_state.pos * final_state.depth;
    }
    

    =>

    #[derive(Default)]
    struct State1 {
        /* ... */
    }
    
    impl State1 {
        fn score(self) -> i32 {
            /* ... */
        }
    }
    
    fn part1_fun(commands: &[Command]) -> i32 {
        commands
            .iter()
            .fold(Default::default(), move_sub1)
            .score()
    }
    
  2. Zwyczajowo dorzuca się adnotacje tylko do tych typów, z którymi kompilator ma problem - tzn.:

    let commands: Vec<Command> = contents.lines()
        .map(|line| Command::new(line))
        .collect();
    

    =>

    let commands: Vec<_> = contents.lines().map(Command::new).collect();
    
  3. Jeśli istnieje trait pokrywający daną funkcjonalność, to zazwyczaj nie robi się "freestanding" funkcji:

    impl Board {
        /* ... */
    
        fn clone(&self) -> Board {
            /* ... */
        }
    }
    

    ... a implementuje dany trait:

    impl Clone for Board {
        /* ... */
    }
    

    (clippy ma nawet na to linta! :-))

  4. Zwyczajowo settery nazywa się set_foo(), lecz gettery po prostu foo() (https://rust-lang.github.io/api-guidelines/naming.html).

  5. no sentinel values, only ADTs angry doge meme

    fn part1(data: &InputData) -> i32 {
        /* ... */
        for number in data.numbers.iter() {
            /* ... */
            if boards.is_any_winning() {
                return /* ... */;
            }
        }
        return -1;
    }
    

    =>

    fn part1(data: &InputData) -> Option<u32> {
        /* ... */
        for number in &data.numbers {
            /* ... */
            if boards.is_any_winning() {
                return Some(/* ... */);
            }
        }
        None
    }
    
  6. struct BinNumber {
        value: Vec<i32>,
    }
    

    =>

    struct BinNumber {
        value: Vec<bool>,
    }
    

    (to uprości np. negowanie)

Tak ogólnie to fajen - kod wygląda całkiem dobrze, a widzę, że i nawet miejsce na Itertools się znalazło :-)

3
Patryk27 napisał(a):
  1. Jeśli istnieje trait pokrywający daną funkcjonalność, to zazwyczaj nie robi się "freestanding" funkcji:
    impl Board {
        /* ... */
    
        fn clone(&self) -> Board {
            /* ... */
        }
    }
    
    ... a implementuje dany trait:
    impl Clone for Board {
        /* ... */
    }
    
    (clippy ma nawet na to linta! :-))

Można wstawić #[derive(Clone, Copy, Debug, Eq, PartialEq)] przed struct czy enum i mieć za darmo implementacje różnych traitów, pod warunkiem, że wszystkie pola też mają (rekurencyjnie) implementacje tych traitów. Szczegóły: https://doc.rust-lang.org/rust-by-example/trait/derive.html

P.S. Copy to tylko do bardzo lekkich struktur. Do czegokolwiek nietrywialnego należy użyć Clone. Różnica jest taka, że kopiowanie jest niejawne, automatyczne, a klonowanie jest jawne, więc widać gdzie ponosimy koszt klonowania.

1

Zwyczajowo dorzuca się adnotacje tylko do tych typów, z którymi kompilator ma problem - tzn.:

let commands: Vec<_> = contents.lines().map(Command::new).collect();

Można użyć collect_vec() i wtedy w ogóle nie musisz podawać typu.

0

Chciałem dziś zrobić ładnie z .fold() ale kopiowanie boarda po oznaczeniu pola strasznie się ślimaczyło, a nie umiem przekonać borrow-checkera do jakiegoś:

    lines.iter()
        .fold(board, |board, line| board.mark_line(line));

ale w sytuacji kiedy mark_line nie zwraca kopii Board tylko &Board

edit: Żeby wyjaśnić o co pytam. Można by napisać taki kod:

use itertools::Itertools;

struct Board {
    board: Vec<i32>,
}

impl Board {
    fn new(size: i32) -> Board {
        return Board { board: (0..size).map(|x| 0).collect_vec() };
    }
    fn mark_field(&self, index: usize) -> Board {
        return Board {
            board: self.board
                .iter()
                .enumerate()
                .map(|(i, &value)| if i == index { value + 1 } else { value })
                .collect_vec()
        };
    }
}

pub(crate) fn solve() {
    let board = (0..10000)
        .fold(Board::new(10000), |board, index| board.mark_field(index));
    println!("{}", board.board[1]);
}

Czyli za każdym razem kiedy robimy mark_field zwracamy nową planszę. Tylko że jak plansza jest duża to nagle robi sie to bardzo wolne, z oczywistych względów.

I teraz zastanawiałem się, czy można tak zrobić, zeby nasze mark_field zwracało zmodyfikowaną początkową tablicę, a nie tworzyło nową za każdym razem (pytanie co jest "bardziej rustowe" i moze jest jakaś trzecia opcja?). I pisząc tego posta doszedłem do tego, ze mogę zrobić:

    fn mark_field(mut self, index: usize) -> Board {
        self.board[index]+=1;
        return self;
    }

Bo mogę przyjąć sobie self a nie &self jako argument.

0

Jak wrażenia z Rusta po AoC2021? Jakieś refleksje?

1

Hmm dość specyficzny język. Moim zdaniem miejscami mocno autystyczny, kiedy np. nie możesz porównać &int z intem i musisz robić jakiś cyrk w stylu &0. Dodatkowo nie wyobrażam sobie pisania w Ruście bez silnego wsparcia IDE, które pokazuje wyniki inferencji typów, szczególnie kiedy nagle po jakimś .iter() czy w jakimś .map() dostajesz &&X.
Swoją drogą, biorąc pod uwagę brak raw-pointerów jako takich (w sensie jakiegoś int* ptr = 0x12345; w C), zastanawiam się, czemu składnia idzie w kierunku pointerów z C (w kontekście & i *) a nie czegoś na wzór referencji.

0

Swoją drogą, biorąc pod uwagę brak raw-pointerów jako takich

FWIW, istnieją raw pointery:

fn main() {
    let ptr = 0x12345 as *const i32;
    println!("{}", unsafe { *ptr }); // 99% szansy na segfault
}

a nie czegoś na wzór referencji.

W sensie, że bez ręcznego pisania *? Taki mechanizm już istnieje i nazywa się auto-deref :-)

0
Shalom napisał(a):

Dodatkowo nie wyobrażam sobie pisania w Ruście bez silnego wsparcia IDE, które pokazuje wyniki inferencji typów, szczególnie kiedy nagle po jakimś .iter() czy w jakimś .map() dostajesz &&X.

To może być przyzwyczajenie javowe - zupełnie niepotrzebne natręctwo do podglądania typów. Podobny problem niektórzy raportują w kotlinie... podczas kiedy całkiem dobrze się pisze z wyłączonymi hintami typów. W haskellu to norma, że typów nie widzisz przez całe długie kawałki kodu i zwykle żadnego specjalnego wsparcia IDE nie ma.
A rust z wnioskowaniem typów jest bliżej nawet tego haskella.

0

@jarekr000000 może gdyby były jakieś naturalne koercje pomiędzy tymi & to by się dało, ale kiedy nie możesz porównać &int z intem, a gdzieś nagle dostajesz & albo w ogóle && to moim zdaniem jednak ciężko.

0

Swoją drogą, biorąc pod uwagę brak raw-pointerów jako takich (w sensie jakiegoś int* ptr = 0x12345; w C), zastanawiam się, czemu składnia idzie w kierunku pointerów z C (w kontekście & i *) a nie czegoś na wzór referencji.

@Shalom pisanie przez parę lat w C++ skłoniło mnie do konkluzji, że implicit referencje to zło wcielone. Semantyka wskaźników używana w C czy Javie jest prosta i skuteczna. To, że w zależności od kontekstu referencja zachowuje się jak wskazywany typ albo nie powoduje dużo problemów np. nieustanne niepotrzebne kopie, konieczne cuda na kiju (jak std::reference_wrapper) popieprzone wyłomy w systemie typów. Zauważ, że w wielu przypadkach Rust idzie na rękę i sprawia, że referencje są przezroczyste w przypadkach, gdy jest to całkowicie nieszkodliwe np. wołanie metod.

1 użytkowników online, w tym zalogowanych: 0, gości: 1