rust - Rasgos abatidos dentro de Rc para la manipulación de AST
abstract-syntax-tree type-systems (1)
No, no puede reducir Rc<Trait>
a Rc<Concrete>
, porque los objetos de rasgo como Rc<Trait>
no contienen información sobre el tipo concreto al que pertenecen los datos.
Aquí hay un extracto de la documentación oficial que se aplica a todos los objetos de rasgo ( &Trait
, Box<Trait>
, Rc<Trait>
):
pub struct TraitObject {
pub data: *mut (),
pub vtable: *mut (),
}
El campo de data
apunta a la estructura en sí, y el campo vtable
apunta a una colección de punteros de función, uno para cada método del rasgo. En el tiempo de ejecución, eso es todo lo que tienes. Y eso no es suficiente para reconstruir el tipo de estructura. (Con Rc<Trait>
, los puntos de data
bloque también contienen los recuentos de referencias fuertes y débiles, pero no hay información de tipo adicional).
Pero hay al menos otras 3 opciones.
Primero, puede agregar todas las operaciones que necesita hacer en Expression
s o Condition
s al rasgo AstNode
e implementarlas para cada estructura. De esta manera, nunca tendrá que llamar a un método que no esté disponible en el objeto de rasgo, porque el rasgo contiene todos los métodos que necesita.
Esto también implica reemplazar a la mayoría de los miembros de Rc<Expression>
y Rc<Condition>
en el árbol con Rc<AstNode>
, ya que no se puede Rc<AstNode>
(pero consulte más abajo sobre Any
):
enum Condition {
Equals(Rc<AstNode>, Rc<AstNode>),
LessThan(Rc<AstNode>, Rc<AstNode>),
...
}
Una variación de esto podría ser la escritura de métodos en AstNode
que toman &self
y devuelven referencias a varios tipos concretos:
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) }
}
En lugar de Rc<AstNode>
a Rc<Condition>
, simplemente almacénelo como AstNode
y llame, por ejemplo, rc.as_condition().unwrap().method_on_condition()
, si está seguro de que rc
es de hecho un Rc<Condition>
.
En segundo lugar, podría crear otra enumeración que unifique Condition
y Expression
, y eliminar completamente los objetos de rasgos. Esto es lo que he hecho en el AST de mi propio intérprete de Scheme. Con esta solución, no se requiere una bajada porque toda la información de tipo está presente en el momento de la compilación. (También con esta solución, definitivamente tiene que reemplazar Rc<Condition>
o Rc<Expression>
si necesita obtener un Rc<Node>
).
enum Node {
Condition(Condition),
Expression(Expression),
// you may add more here
}
impl Node {
fn children(&self) -> Vec<Rc<Node>> { ... }
}
Una tercera opción es usar Any
, y .downcast_ref()
o Rc::downcast
(actualmente solo por la noche) cada Rc<Any>
en su tipo concreto según sea necesario.
Una pequeña variación sería agregar un método fn as_any(&self) -> &Any { self }
a AstNode
, y luego puedes llamar a métodos de Expression
(que toman &self
) escribiendo node.as_any().downcast_ref::<Expression>().method_on_expression()
. Pero actualmente no hay una manera de (con seguridad) upcast un Rc<Trait>
a un Rc<Any>
, aunque no haya ninguna razón real por la que no pueda funcionar.
Any
es, estrictamente hablando, lo más parecido a una respuesta a su pregunta. No lo recomiendo porque la baja, o la necesidad de bajar, a menudo es una indicación de un diseño deficiente. Incluso en idiomas con herencia de clase, como Java, si desea hacer el mismo tipo de cosas (almacenar un grupo de nodos en un ArrayList<Node>
, por ejemplo), tendrá que hacer que todas las operaciones necesarias estén disponibles en el La clase base o en algún lugar enumera todas las subclases a las que podrías necesitar bajar, lo que es un terrible anti-patrón. Cualquier cosa que haría aquí con Any
sería comparable en complejidad a solo cambiar AstNode
a una enumeración.
tl; dr: debe almacenar cada nodo del AST como un tipo que (a) proporciona todos los métodos a los que podría necesitar llamar y (b) unifica todos los tipos que podría necesitar para poner uno . La opción 1 usa objetos de rasgos, mientras que la opción 2 usa enumeraciones, pero son bastante similares en principio. Una tercera opción es usar Any
para habilitar la reducción de velocidad.
Preguntas y respuestas relacionadas para leer más:
Estoy tratando de manipular ASTs en Rust. Habrá muchas manipulaciones, y quiero que mis árboles sean inmutables, por lo que para ahorrar tiempo, todas las referencias serán Rc
s.
Mis nodos de árbol se verán así:
enum Condition {
Equals(Rc<Expression>, Rc<Expression>),
LessThan(Rc<Expression>, Rc<Expression>),
...
}
enum Expression {
Plus(Rc<Expression>, Rc<Expression>),
...
}
Quiero reemplazar un nodo aleatorio de un tipo dado por otro nodo del mismo tipo. Para hacer operaciones genéricas en árboles he hecho un rasgo:
trait AstNode {
fn children(&self) -> Vec<Rc<AstNode>>;
}
Y todos los nodos implementan esto. Esto me permite recorrer el árbol sin tener que desestructurar cada tipo de nodo para cada operación, simplemente llamando a children()
.
También quiero clonar un nodo al actualizar solo uno de sus hijos, y dejar los otros en su lugar. Supongamos que he podido generar nodos del tipo concreto correcto (y estoy feliz de que el programa se detenga si me equivoco). Agregaré el siguiente método al rasgo:
trait AstNode {
fn clone_with_children(&self, new_children: Vec<Rc<AstNode>>) -> Self
where Self: Sized;
}
Mi plan es tomar los hijos devueltos por childen()
, reemplazar uno de ellos y llamar a clone_with_children()
para construir un nodo de la misma variante de enumeración pero con un nodo reemplazado.
Mi problema es cómo escribir clone_with_children()
.
Necesito Rc<AstNode>
a Rc<Expression>
(o lo que sea), mientras mantengo el refcount dentro del Rc
igual, pero ninguna de las bibliotecas downcasting que he encontrado parece ser capaz de hacer eso.
¿Es posible lo que quiero o debo hacerlo de manera completamente diferente?