Даунтайт-черты внутри Rc для АСТ-манипуляции

Я пытаюсь манипулировать АСТ в Rust. Будет много манипуляций, и я хочу, чтобы мои деревья были неизменными, поэтому для экономии времени все ссылки будут Rc s.

Мои узлы дерева будут выглядеть так:

enum Condition {
    Equals(Rc<Expression>, Rc<Expression>),
    LessThan(Rc<Expression>, Rc<Expression>),
    ...
}

enum Expression {
    Plus(Rc<Expression>, Rc<Expression>),
    ...
}

Я хочу заменить случайный node данного типа на другой node того же типа. Чтобы выполнить родовые операции с деревьями, я сделал черту:

trait AstNode {
    fn children(&self) -> Vec<Rc<AstNode>>;
}

И все узлы реализуют это. Это позволяет мне ходить по дереву без необходимости разрушать каждый тип node для каждой операции, просто называя children().

Я также хочу клонировать node, обновляя только один из его дочерних элементов, и оставляя другие на месте. Предположим, что я смог генерировать узлы нужного конкретного типа (и я счастлив, что программа впадает в панику, если я ошибаюсь). Я добавлю следующий метод к признаку:

trait AstNode {
    fn clone_with_children(&self, new_children: Vec<Rc<AstNode>>) -> Self
        where Self: Sized;
}

Мой план состоит в том, чтобы вернуть детей, возвращаемых childen(), заменить один из них и вызвать clone_with_children() для создания node того же варианта перечисления, но с заменой одного node.

Моя проблема заключается в том, как написать clone_with_children().

Мне нужно отключить Rc<AstNode> до Rc<Expression> (или что у вас есть), сохраняя при этом refcount внутри Rc тем же, но ни одна из библиотек downcasting, которые я нашел, похоже, не может это сделать.

Я хочу, чтобы это было возможно, или я должен делать это совершенно по-другому?

Ответы

Ответ 1

Нет, вы не можете перетащить Rc<Trait> в Rc<Concrete>, потому что объекты объектов типа Rc<Trait> не содержат никакой информации о конкретном типе, которому принадлежат данные.

Здесь выдержка из официальной документации, которая применяется ко всем объектам признаков (&Trait, Box<Trait>, Rc<Trait>):

pub struct TraitObject {
    pub data: *mut (),
    pub vtable: *mut (),
}

Поле data указывает на собственно структуру, а поле vtable указывает на набор указателей функций, по одному для каждого метода признака. Во время выполнения все, что у вас есть. И этого недостаточно для восстановления типа структуры. (С Rc<Trait> блок data указывает на то, что также содержит сильные и слабые подсчеты ссылок, но никакой дополнительной информации о типе.)

Но есть как минимум 3 других варианта.

Во-первых, вы можете добавить все операции, которые вам нужно сделать для Expression или Condition, к признаку AstNode и реализовать их для каждой структуры. Таким образом, вам никогда не нужно вызывать метод, который недоступен для объекта-объекта, потому что этот признак содержит все необходимые методы.

Это также приводит к замене большинства элементов Rc<Expression> и Rc<Condition> в дереве Rc<AstNode>, так как вы не можете отключить Rc<AstNode> (но см. ниже о Any):

enum Condition {
    Equals(Rc<AstNode>, Rc<AstNode>),
    LessThan(Rc<AstNode>, Rc<AstNode>),
    ...
}

Вариантом этого может быть метод записи на AstNode, который принимает &self и возвращает ссылки на различные конкретные типы:

trait AstNode {
    fn as_expression(&self) -> Option<&Expression> { None }
    fn as_condition(&self) -> Option<&Condition> { None }
    ...
}

impl AstNode for Expression {
    fn as_expression(&self) -> Option<&Expression> { Some(self) }
}

impl AstNode for Condition {
    fn as_condition(&self) -> Option<&Condition> { Some(self) }
}

Вместо downcasting Rc<AstNode> to Rc<Condition> просто сохраните его как AstNode и вызовите, например. rc.as_condition().unwrap().method_on_condition(), если вы уверены, что rc на самом деле является Rc<Condition>.

Во-вторых, вы можете создать еще одно перечисление, объединяющее Condition и Expression, и полностью уничтожить объекты признаков. Это то, что я сделал в АСТ моего переводчика Схемы. С помощью этого решения не требуется нисходящее преобразование, потому что вся информация о типе присутствует во время компиляции. (Также с этим решением вам обязательно нужно заменить Rc<Condition> или Rc<Expression>, если вам нужно вынуть Rc<Node>).

enum Node {
    Condition(Condition),
    Expression(Expression),
    // you may add more here
}
impl Node {
    fn children(&self) -> Vec<Rc<Node>> { ... }
}

Третий вариант заключается в использовании Any и либо .downcast_ref(), либо Rc::downcast (в настоящее время только в ночное время) каждый Rc<Any> в свой конкретный тип по мере необходимости.

Небольшое отклонение от этого было бы добавить метод fn as_any(&self) -> &Any { self } в AstNode, а затем вы можете вызвать методы Expression (которые принимают &self), написав node.as_any().downcast_ref::<Expression>().method_on_expression(). Но в настоящее время нет (безопасно) upcast a Rc<Trait> в Rc<Any>, хотя нет реальной причины, по которой он не может работать.

Any - это, строго говоря, самое близкое к ответу на ваш вопрос. Я не рекомендую это, потому что понижение, или необходимость понижения, часто является признаком плохого дизайна. Даже в языках с наследованием классов, например Java, если вы хотите сделать то же самое (например, хранить кучу узлов в ArrayList<Node>), вам придется либо сделать все необходимые операции доступными на базе класс или где-то перечислять все подклассы, которые вам могут понадобиться для downcast, что является ужасным анти-шаблоном. Все, что вы сделали бы здесь с Any, было бы сопоставимо по сложности, просто изменив AstNode на перечисление.

tl; dr: Вам нужно сохранить каждый node AST как тип, который (a) предоставляет все методы, которые могут потребоваться для вызова и ( b) объединяет все типы, которые вам могут понадобиться, чтобы добавить один. Вариант 1 использует объекты признаков, в то время как вариант 2 использует перечисления, но они в принципе похожи. Третий вариант - использовать Any для включения downcasting.

Связанные Q & A для дальнейшего чтения: