Type parameters in modules


#1

The thread ‘module wide’ shared function parameters, TLS, dynamic scope emulation briefly mentions allowing type parameters in modules, but the conversation mostly seems to be about value parameters instead. The thread module wide type-parameters, thoughts pertains more specifically to this, but the topic quickly evolved into a comparison of C++ templates and Rust generics. I attempted to post my thoughts there, but the thread is old and there hasn’t been any further discussion on it since. Because of this, I’d like to create a new thread that will hopefully give this idea a little bit more exposure.

I’m working on an interpreter. Programs read streams of bytes and write streams of bytes, but rather than forcing STDIN and STDOUT for IO, I’d like it to be parameterized over two types R: std::io::Read and W: std::io::Write, as this makes things such as testing easier. This means that almost every struct I’m defining in the program includes <R: Read, W: Write>, which is quite noisy. I’d prefer if I could simply make the module parameterized over R and W.

I’d have two syntaxes for declaring parameters:

  • You can declare parameters from anywhere inside a module by writing

    mod<R: Read, W: Write>;
    ...
    
  • A module defined with the mod foo { } syntax can be given parameters as

    mod foo<R: Read, W: Write> { ... }
    

Say we have a parameterized module foo<T> defined in some other file. It would be imported just like any other module:

mod foo;

Any attempt to actually access items in this module, however, would require that it is somehow disambiguated:

foo::<i32>::bar();
use foo::<Option<Vec<usize>>>::Baz;

In the example for the interpreter, main.rs would look like:

use std::io::*;

mod interpreter;

fn main() {
    let src: String = /* read a file */;
    interpreter::<Stdin, Stdout>::interpret(&src, stdin(), stdout());
}

#[cfg(test)]
mod tests {
    use super::interpreter;
    fn run_and_collect_output(src: &str,
                              input: &str) -> Option<String> {
        let output: Vec<u8> = Vec::new();
        interpreter::<&[u8], Vec<u8>>
                   ::interpret(src, input.as_bytes(), output);
        output
    }
    #[test]
    fn test() { ... }
}

Perhaps there could be some inference for this, the way there is for type parameters in functions. That way ::<Stdin, Stdout> and ::<&[u8], Vec<u8>> could be omitted.

In addition, perhaps there should be some way to “distribute” the type parameters to a module’s constituent items. For example, say a math3d module was parameterized over F: num::Float and exposed

pub struct Point(F, F, F);
pub fn distance(p1: Point, p2: Point) { ... }

It should be possible to turn math3d's type parameter into a type parameter of distance so that you could import it generically.

use<F: Float> math3d::<F>::distance;
/* `distance` will now behave as if it had a type parameter F */

To import math3d in such a way that all of its items had its type parameters distributed to them, you could write:

use<F: Float> math3d::<F>;

This way, math3d would become unparameterized and all of its items would take the parameter instead (the way the module would be written today).

I’m not sure I like that syntax, though, because it’s not obvious what the difference between use foo; and use<T> foo::<T>::bar; is. Maybe distribution could be done automatically if no type parameters are supplied to the module. This would mean that we would get the existing type parameter inference for free!

Syntax is up for bikeshedding, but that’s the general idea.


#2

awesome proposal; I like the suggestions for how to declare file-level module-wide type parameters.


#3

I just realized that the current syntax would make the parser much more complicated because you need arbitrary lookahead to distinguish between parameters and comparison, so you’d need to use a turbofish.