Search code examples
rustabstract

What is the Rust equivalent for abstract classes?


I am still a beginner in Rust and I am stuck on a point concerning the traits. Despite many attempts, I can't find a code equivalent in rust for abstract classes. Here is an example of Typescript code using them:

export interface NodeConstructor<T> {
  new (data: T): Node<T>;
}

export abstract class Node<T> {
  public data: T;
  public key: string;
  public parentKeys: string[];
  public childKeys: string[];
  public parents: Node<T>[];
  public children: Node<T>[];

  constructor(data: T) {
    this.data = data;
    this.key = this.buildNodeKey();
    this.parentKeys = this.buildParentKeys();
    this.childKeys = this.buildChildKeys();
    this.parents = [];
    this.children = [];
  }

  get hasParents(): boolean {
    return !!this.parents.length;
  }

  get hasChildren(): boolean {
    return !!this.children.length;
  }

  abstract buildNodeKey(): string;

  abstract buildChildKeys(): string[];

  abstract buildParentKeys(): string[];
}

Thanks to the 'mwlon' solutions in this post, I arrived at this result:

pub struct Node<T, BUILDER: ?Sized> where BUILDER: NodeBuilder {
    pub data: T,
    pub key: String,
    pub parent_keys: Box<[String]>,
    pub child_keys: Box<[String]>,
    pub parents: Option<Box<[T]>>,
    pub children: Option<Box<[T]>>,
    builder: BUILDER,
}

pub trait NodeBuilder {
    fn build_node_key(&self) -> String;
    fn build_parent_key(&self) -> Box<[String]>;
    fn build_child_key(&self) -> Box<[String]>;
}

impl<T , BUILDER> Node<T, BUILDER> where BUILDER: NodeBuilder {
    pub fn new(&self, data: T) -> Node<T, BUILDER> {
        Self{
            data: data,
            key: BUILDER::build_node_key(&self.builder),
            parent_keys: BUILDER::build_parent_key(&self.builder),
            child_keys: BUILDER::build_child_key(&self.builder),
            parents: None,
            children: None,
            builder: self.builder
        }
    }
    pub fn has_parents(&self) -> bool {
        match &self.parents {
            Some(_x) => true,
            None => false,
        }
    }
    pub fn has_children(&self) -> bool {
        match &self.children {
            Some(_x) => true,
            None => false,
        }
    }
}

Which implements like this:

struct TestModel {
    name: String,
    children: Option<Box<[String]>>,
    parents: Option<Box<[String]>>
}
impl node::Node<TestModel, dyn node::NodeBuilder> {
    fn build_child_key(data: TestModel) -> Box<[String]> {
        match data.children {
            Some(x) => x.clone(),
            None => Box::new([]),
        }
    }
    fn build_node_key(data: TestModel) -> String {
        data.name.clone()
    }
    fn build_parent_key(data: TestModel) -> Box<[String]> {
        match data.parents {
            Some(x) => x.clone(),
            None => Box::new([]),
        }
    }
}

But I still have one error I can't get over:

cannot move out of `self.builder` which is behind a shared reference

move occurs because `self.builder` has type `BUILDER`, which does not implement the `Copy` traitrustc(E0507)
node.rs(28, 22): move occurs because `self.builder` has type `BUILDER`, which does not implement the `Copy` trait

I can't implement 'Copy' on Builder since it is also a trait. Is there something I'm missing? What are the best practices for such a structure in Rust? I'm using rustc 1.59.0 (9d1b2106e 2022-02-23)


Solution

  • An abstract class couples together a piece of data which has the same shape for every instance of the type, and a set of behaviors which may differ between different instances of the type. This is a relatively unusual pattern in Rust, because Rust encourages decoupling of data and behavior more than other languages.

    A more idiomatic translation of what you are trying to do would probably be along these lines. First, we make a type that holds the data representing a node:

    pub struct Node<T> {
        data: T,
        key: String,
        parent_keys: Box<[String]>,
        child_keys: Box<[String]>,
        parents: Option<Box<[T]>>,
        children: Option<Box<[T]>>,
    }
    

    We will need a way to create instances of this type, so we will give it a constructor. I do not know exactly how you intend parents and children fields to be filled, so I will leave them as None for now. However, if they are to be filled by reading data from an external source using parent_keys and child_keys, this constructor could be the right place to do that.

    impl<T> Node<T> {
        pub fn new(data: T, key: String, parent_keys: Box<[String]>, child_keys: Box<[String]>) -> Node<T> {
            Node { data, key, parent_keys, child_keys, parents: None, children: None }
        }
    }
    

    Next, we want a trait to abstract over possible behaviors. In your case, the behavior appears to be 'a way to create a node.' A trait should have exactly the methods necessary to implement its behavior, so:

    pub trait NodeBuilder {
        fn build_node<T>(&self, data: T) -> Node<T>;
    }
    

    We can use generic parameters bounded by NodeBuilder in methods or structs to abstract over types that are capable of building a node. And we can define which types are capable of this by implementing NodeBuilder for them, like so:

    struct TestModel {
        name: String,
        children: Option<Box<[String]>>,
        parents: Option<Box<[String]>>
    }
    
    impl NodeBuilder for TestModel {
        fn build_node<T>(&self, data: T) -> Node<T> {
            let parents = self.parents.clone().unwrap_or_else(Default::default);
            let children = self.children.clone().unwrap_or_else(Default::default);
            Node::new(data, self.name.clone(), parents, children)
        }
    }
    

    As you can see, this solution avoids coupling data and behavior when it is not necessary. It is, however, specific to the particular situation you have. A different abstract class might translate to a different set of types and traits. This is common when translating across programming paradigms: the role of one pattern in language A might be filled by several patterns in language B, or vice versa.

    Instead of focusing too much on how to replicate a pattern like 'abstract classes' in Rust, you should ask yourself what problem that pattern was solving in TypeScript, and consider how to best solve the same problem in Rust, even if that means using different patterns.