Laboratoria: 11 I

Manual reborrowing, visitor pattern i impl for &T

W tym zadaniu pokryjemy trzy niezależne tematy:

  • wzorzec projektowy visitor,
  • implementację traitu dla typu referencyjnego,
  • manualny reborrowing.

Pracować będziemy z prostą hierarchią typów dla drzewa binarnego:

enum Node {
    Inner(InnerNode),
    Leaf(LeafNode),
}
struct InnerNode {
    value: i32,
    left: Box<Node>,
    right: Box<Node>,
}
struct LeafNode {
    value: i32,
}

Dostarczamy trait Visitor, który korzystając z metody accept typu Node umożliwi wygodne odwiedzanie węzłów drzewa:

trait Visitor {
    fn visit_inner(&mut self, node: &InnerNode);
    fn visit_leaf(&mut self, node: &LeafNode);
}

impl Node {
    fn accept<V: Visitor>(&self, visitor: &mut V) {
        match self {
            Node::Inner(inner) => visitor.visit_inner(inner),
            Node::Leaf(leaf) => visitor.visit_leaf(leaf),
        }
    }
}

1. Zaimplementuj trait Visitor dla typu TreePrinter (wartości węzłów mogą być w dowolnym porządku (pre-, in-, post-order)):

struct TreePrinter {
    ident: usize,
}
impl Visitor for TreePrinter {
    ...
}

2. Przetestuj działanie TreePrinter na przykładowym drzewie:

fn main() {
    let root = Node::Inner(InnerNode {
        ...
    });
    root.accept(&mut NodePrinter);
}

Output powinien wyglądać mniej więcej:

Inner node: 0
 Leaf node: 1
 Inner node: 2
  Leaf node: 3
  Leaf node: 4

3. Domyślnie, metody Visitor::visit_* przyjmują referencję &mut self. Dzięki temu, możemy w łatwy sposób modyfikować stan wizytora podczas odwiedzania węzłów. Niestety, może zdarzyć się sytuacja, gdy chcemy utworzyć nowy typ V implementujący Visitor, ale nie możemy pracować na obiektach typu &mut V (przykład opisany poniżej). Żeby umożliwić wszystkie możliwe przypadki, musimy zmienić sygnaturę metody Visitor::visit_* na przyjmujące self:

trait Visitor {
    fn visit_inner(self, node: &InnerNode);
    fn visit_leaf(self, node: &LeafNode);
}

Żeby utrzymać funkcjonalność z poprzedniego punktu, musimy zaimplementować Visitor dla &mut TreePrinter (zamiast dla TreePrinter) oraz delikatnie zmodyfikować metodę Node::accept:

impl Node {
    fn accept<V: Visitor>(&self, visitor: V) {
        match self {
            Node::Inner(inner) => visitor.visit_inner(inner),
            Node::Leaf(leaf) => visitor.visit_leaf(leaf),
        }
    }
}

impl Visitor for &mut TreePrinter {
    ...
}

Zastosuj zmiany i przetestuj działanie (funkcja main nie powinna ulec żadnym zmianom). Oczekiwany jest błąd kompilacji use of moved value: `self`. Napraw go używając manualnego reborrowingu (warto doczytać o tym mechanizmie w sieci).

Przykładowa sytuacja gdy nie możemy operować na obiektach typu ``&mut V``: mamy typ Interpreter , który przechodzi po AST kodu i wykonuje go. Od czasu do czasu musimy zewaluować pewne wyrażenie, co wymaga uruchomienia innego wizytora (np. Evaluator ) na tym wyrażeniu. Niestety, do ewaluacji niektórych wyrażeń (np. to wywołania funkcji) potrzebujemy rekurencyjnie wywołać interpreter, co jest niemożliwe, gdyż nie możemy mieć dwóch referencji &mut Interpreter na raz.

Rozcinanie cyklów

W tym zadaniu zajmiemy się rozcinaniem cyklów referencji Rc zapobiegając wyciekom pamięci.

Dana jest klasa:

#[derive(Clone)]
struct Node {
    value: &'static str,
    hard_links: RefCell<Vec<Rc<Node>>>,
    weak_links: RefCell<Vec<Weak<Node>>>,
}

impl Node {
    pub fn new(value: &'static str) -> Rc<Node> {
        Rc::new(Node {
            value,
            hard_links: RefCell::new(vec![]),
            weak_links: RefCell::new(vec![]),
        })
    }

    pub fn add_link(&self, node: Rc<Node>) {
        self.hard_links.borrow_mut().push(node);
    }
}

impl std::fmt::Debug for Node {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}", self.value)?;
        for link in self.hard_links.borrow().iter() {
            write!(f, "{:?}", link)?;
        }
        Ok(())
    }
}

Domyślny konstruktor - Node::new - tworzy nowy węzeł, który nie posiada żadnych połączeń. Metoda Node::add_link dodaje nowe połączenie do węzła. To połączenie jest silne, tzn. węzeł node nie może zostać usunięty, dopóki istnieje jakikolwiek węzeł, który na niego wskazuje. Możemy utworzyć graf skierowany i wydrukować jego reprezentację:

fn main() {
    let s1 = Node::new("word ");

    let s2 = Node::new("the ");
    s2.add_link(s1.clone());

    let s3 = Node::new("is ");
    s3.add_link(s2.clone());

    let s4 = Node::new("bird ");
    s4.add_link(s3.clone());

    let s5 = Node::new("the ");
    s5.add_link(s4.clone());

    println!("{:?}", s5);
}

Niestety, łatwo jest utworzyć cykl referencji. Wystarczy dodać

s1.add_link(s5.clone());
println!("{:?}", s5);

i otrzymamy fatal runtime error: stack overflow.

Napisz metodę fn acycle(root: Rc<Node>) -> (), która otrzymuje źródło grafu i usuwa wszystkie cykle referencji, zamieniając minimalną liczbę linków na słabe połączenia. Przetestuj rozwiązanie na nietrywialnych przykładach.