Prospects of modules as first-class types

I have been rewriting a large C project in Rust, and in doing so I have been melting my brain while thinking about the best ways to make working on the Rust version as easy as possible. To highlight the applicability of what I'm talking about, my problem reduces to the following in abstract terms:

  1. You have a project whose purpose is to provide the functionality offered by an ever-expanding set of discrete and mutually independent modules which we'll call M. Let's say that this set of modules is in a library crate.
  2. Your project has a core which provides 1) an entry point to all of the behavior defined in M and 2) common behavior for different M-element implementations. Let's say that this core is defined as appropriate across set of library crates and one binary cate other than the library crate containing M.
  3. You would like to minimize the amount of changes an implementer of a new element of M would have to make on the rest of the project in order to allow the behavior of their newly implemented element to be accessible through the project's core.

Now, the C implementation of the project that I'm working on solved this by having each member of M be it's own executable, and the rest of the project simply libraries which define a framework to construct new elements of M.

It was my ambition to have the Rust implementation of this project make all of the project's functionality available through a single executable through one of many user interfaces. However, since you cannot possibly do something like the following in Rust, I understood from the beginning that satisfying point 3 completely would be impossible:

/* In project core */

let target = user_input();

// Automatically include new modules' functionality
for module in some_crate::modules() {
    if module::name() == target {
        // Note that this would also benefit a lot from module
        // parameterization (to be able to check types at compile
        // time)
        do_something(module::FirstClassItem);
    }
}

As opposed to this, which requires implementers of new members of M to go in there and add their new module to the match arms:

/* In project core */

let target = user_input();

match target {
    "module_name" => do_something(some_crate::module_name::FirstClassItem),
    ...
    _ => (),
}

Now, what I wonder is:

  • Is there, in fact, a way to do this in Rust?
  • Does anyone have any tips to making a setup like this work?
  • Is there any indication that Rust might have support for first-class modules in the future?

Thank you!

You can "discover" modules at compile time with procedural macros. automod is an example of how that works. I wouldn't really recommend going that route but it is technically possible.

A macro_rules macro can cut down on the duplication a bit, and doesn't require nearly as much work to get running.

mod one;
mod three;
mod two;

macro_rules! find_target {
    ($target:expr => $($name:ident),+) => {
        match $target {
            $(
            stringify!($name) => do_something($name::FirstClassItem),
            )+
            _ => panic!("Not a module!")
        }
    };
}

pub fn run_target(target: &str) {
    find_target!(target => one, two, three);
}

fn do_something<T: 'static>(_: T) {
    println!("{}", std::any::type_name::<T>())
}

If you create those three files and put pub struct FirstClassItem; In them that will compile.

You can go one step further to avoid needing to even duplicate the mod items, though I'm not really sure it's necessary

macro_rules! declare_modules {
    ($($name:ident),+) => {
         $(mod $name; )+

        pub fn run_target(target: &str) {
            match target {
                $(
                stringify!($name) => do_something($name::FirstClassItem),
                )+
                _ => panic!("Not a module!")
            }
        }
    };
}

declare_modules!(one, two, three);

fn do_something<T: 'static>(_: T) {
    println!("{}", std::any::type_name::<T>())
}

None of that is really dynamic in the runtime sense, but since you mentioned something about type checking that's probably not a deal breaker.

1 Like

This is really great, thank you. I was too afraid to even think about how I would solve this using macro witchery, but your solution is really workable.

I agree that this isn't dynamic in the strong sense of the word, but it's as good as dynamic for the workflow I'm trying to make possible. I'll give it a shot!

I have a feeling that you are simply approaching this from the wrong angle. Rust has traits that provide both compile-time and run-time polymorphism (generics and trait objects). Instead of switching on a big fat list of strings, why not just make target a type parameter or a trait object, and invoke a trait method on that?

4 Likes

You can also use crates like inventory or linkme to have the linker collect all of the implementations into a single static list that can be inspected at runtime.

2 Likes

Thanks for the concern. I am not confident enough to say that what you suggest would not fix my problem, so to avoid the whole XY conundrum I'll give a brief explanation of what it exactly is that I am trying to do (if you would like to reference the actual project, it is here):

  • I am working on a project for solving games.
  • The elements of the M I referenced before are Game trait implementations, and have a 1-1 relationship to modules (e.g. the type TicTacToe implements Game and lives in the tic_tac_toe module).
  • The problem is that I cannot have someone make a new game_x module (with a GameX type inside it which implements Game and other traits) and have them not touch any other part of the project, and have their implemented game still be available for performing operations upon from the perspective of a user (because at some point I need to map the user's input string to game_x::GameX outside the game_x module itself).

Now, I believe that what you suggest target do in this case is the job of the Game trait, which is absolutely doing a lot of heavy lifting in terms of abstraction. But other than what @semicoleon suggested, I do not know of another way to make modules be accessible dynamically or "proto-dynamically".

Does this sound about right, or have I misinterpreted your suggestion?

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.