Rust Runtime Polymorphism

Rust has:

What other forms of Runtime Polymorphism have ppl done in Rust? And what would make them better?

I was always taught that runtime polymorphism is being able to write code against a certain contract and being able to switch between implementations at runtime. In other words, dynamic dispatch via trait objects.

You can get a more powerful form of this by rolling your own dynamic dispatch with vtables and Any or raw pointers, but the idea is still the same... Introduce a level of indirection which lets you "forget" the concrete type at runtime, then pass around that pointer and a set of functions which know how to invoke the correct functionality.

The Arc<Mutex<...>> bit you mention is a red herring and isn't needed for runtime polymorphism. You are adding it because you want to implement the same pervasive shared mutability patterns you would use in a traditional OO language.

Using Any in Rust is just like passing around an Object and downcasting to a specific implementation in a language like Java or C#. It isn't polymorphism, it's a way to say "I don't care about the type" and not too different from a dynamically typed language (besides the additional friction).

QCell also has nothing to do with polymorphism. It's just a way to guarantee single mutability XOR shared readonly access at compile time with less friction than the typical ownership/lifetime-based approach (or rather, the friction has been moved to somewhere you r particular use case may not care about).

1 Like

So what do you call that ConfigManager example where the same object is fulfilling 3 different optional contracts?

It's using trait objects, so you are doing runtime polymorphism. The actual polymorphism bit is the dyn DataSourceBase and dyn DataSource<T>... All that Vec<Arc<RwLock<_>>> stuff is just the way you've chosen to manage/share your resources and has nothing to do with polymorphism.

2 Likes

What do you call the multiple traits thing?

What do you mean by "the multiple traits thing"? Can you post a simplified code example?

This is about as simplified as it gets:

pub struct ConfigManager {
    owner: QCellOwner,
    resources: Vec<Resource>,
}

pub struct AddConfigSource<'a, T: DataSourceBase + Send + Sync + 'static> {
    resource: &'a mut Resource,
    source: Arc<QCell<T>>,
}

struct Resource {
    // actual resource
    base: Arc<QCell<dyn DataSourceBase + Send + Sync>>,
    // views of the actual resource
    title: Option<Arc<QCell<dyn DataSource<InstanceTitle> + Send + Sync>>>,
    url: Option<Arc<QCell<dyn DataSource<InstanceBaseUrl> + Send + Sync>>>,
    repolist: Option<Arc<QCell<dyn DataSource<RepoListUrl> + Send + Sync>>>,
}

impl Resource {
    fn new(base: Arc<QCell<dyn DataSourceBase + Send + Sync>>) -> Self {
        Self {
            base,
            title: None,
            url: None,
            repolist: None,
        }
    }
}

impl ConfigManager {
    pub fn add_source<T>(&mut self, source: T) -> AddConfigSource<'_, T>
    where
        T: DataSourceBase + Send + Sync + 'static,
    {
        let arc = Arc::new(QCell::new(&self.owner, source));
        self.resources.push(Resource::new(arc.clone()));
        AddConfigSource {
            resource: self.resources.last_mut().unwrap(),
            source: arc,
        }
    }
}

impl<'a, T: DataSourceBase + Send + Sync + 'static> AddConfigSource<'a, T> {
    pub fn for_title(self) -> Self where T: DataSource<InstanceTitle> {
        let arc = &self.source;
        self.resource.title.get_or_insert_with(|| {
            arc.clone()
        });
        self
    }
    pub fn for_base_url(self) -> Self where T: DataSource<InstanceBaseUrl> {
        let arc = &self.source;
        self.resource.url.get_or_insert_with(|| {
            arc.clone()
        });
        self
    }
    pub fn for_repo_lists(self) -> Self where T: DataSource<RepoListUrl> {
        let arc = &self.source;
        self.resource.repolist.get_or_insert_with(|| {
            arc.clone()
        });
        self
    }
}

What would you call this pattern?

That looks a bit too specific to have a general pattern name, but to me, it feels like a complex way to check whether some object implements a particular trait.

Maybe you are trying to copy a pattern from C#/Java where you check whether an Object variable implements a particular interface and cast it to that interface if so?

interface IDataSourceBase { ... }

interface IDataSource<T>: IDataSourceBase { 
    T Get();
}

class Foo: IDataSourceBase, IDataSource<InstanceUrl> { ... }

class Program 
{
    static void Main() {
        IDataSourceBase data = getDataSourceFromSomewhere();

        if (data is IDataSource<InstanceUrl> urlDataSource) 
        {
            InstanceUrl url = urlDataSource.get();
            Console.WriteLine("URL is {}", url);
        } else {
            Console.WriteLine("Unknown URL");
        }
    }
}

That sort of architecture isn't very idiomatic in Rust (probably why you ran into a lot of friction), but if I had to I'd probably write it like this:

trait DataSourceBase {
  fn title(&self) -> Option<dyn DataSource<InstanceTitle>> { None }
  fn url(&self) -> Option<dyn DataSource<InstanceBaseUrl>> { None }
  fn repolist(&self) -> Option<dyn DataSource<RepoListUrl>> { None }
}

trait DataSource<T>: DataSourceBase {
  fn get(&self) -> &T;
}

You would then pass an Arc<dyn DataSourceBase> around. If you need access to title data you call data_source.title() and handle the case where it doesn't implement DataSource<InstanceBaseUrl>.

struct UrlAndTitle {
  url: InstanceBaseUrl,
  title: InstanceTitle,
}

impl DataSourceBase for UrlAndTitle {
  fn title(&self) -> Option<dyn DataSource<InstanceTitle>> { Some(self) }
  fn url(&self) -> Option<dyn DataSource<InstanceBaseUrl>> { Some(self) }
  // note: leave repolist() with the default implementation that returns None
}

impl DataSource<InstanceBaseUrl> for UrlAndTitle {
  fn get(&self) -> InstanceBaseUrl { &self.url }
}

impl DataSource<InstanceTitle> for UrlAndTitle {
  fn get(&self) -> InstanceTitle { &self.title }
}

fn main() {
  let data: Arc<DataSourceBase> = get_data_source_from_somewhere();

  match data.url() {
    Some(url) => println!("URL is {}", url),
    None => println!("Unknown URL"),
  }
}

If the data source needs to mutate its fields it'll need to use locks to synchronise access, but from the context it looks like this is intended for some sort of web server so the C# implementation would also need to use locking to avoid data races.

Either way, the interior mutability is an internal detail of the UrlAndTitle type, whereas the DataSourceBase trait just cares about the public interface and shouldn't mention anything about QCell or Arc.

1 Like

Nah the original Python was even more exciting, it didn't even really care about types:

class DataProperty(Enum):
    """Represents values that can be returned by a data source.

    See documentation for DataSource get_property_value and
    DataSource get_property_values for more details.
    """
    INSTANCE_TITLE = (1, str)
    INSTANCE_BASE_URL = (2, str)
    VCS_REPOS = (3, PCTP)
    REPO_LIST_SOURCES = (4, RepoListSource)

    def get_type(self):
        """Returns the expected type for values from this DataProperty.
        """
        return self.value[1]

class DataSource(abc.ABC):
    @abc.abstractmethod
    def get_property_values(self, prop):
        """Yields the values associated with the given property.

        If duplicated, earlier values should override later values.

        Args:
            prop (DataProperty): The property.

        Yields:
            The values associated with the given property.

        Raises:
            PropertyError: If the property is not supported by this data
            source.
            LookupError: If the property is supported, but isn't available.

        """
        raise PropertyError


class ConfigManager(DataSource):
    def get_property_values(self, prop):
        if prop not in self.get_supported_properties():
            raise PropertyError
        elif prop == DataProperty.VCS_REPOS:
            return self._get_vcs_repos()
        elif prop == DataProperty.REPO_LIST_SOURCES:
            return self._get_repo_list_sources()
        else:
            # short-circuiting, as these are only supposed to return a single value
            for source in self.sources:
                try:
                    return source.get_property_values(prop)
                except PropertyError:
                    pass
                except LookupError:
                    pass
            raise LookupError

    def _get_vcs_repos(self):
        for source in self.sources:
            if DataProperty.VCS_REPOS in source.get_supported_properties():
                try:
                    iterator = source.get_property_values(DataProperty.VCS_REPOS)
                except LookupError:
                    pass
                else:
                    yield from iterator

    def _get_repo_list_sources(self):
        for source in self.sources:
            if DataProperty.REPO_LIST_SOURCES in source.get_supported_properties():
                try:
                    iterator = source.get_property_values(DataProperty.REPO_LIST_SOURCES)
                except LookupError:
                    pass
                else:
                    yield from iterator

but we really like the .register(Thing).as_thing().as_other().whatnot() Runtime Polymorphism pattern.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.