Some fancy features from D language


#1

Hi all,

As I learned D language before Rust, I noticed some useful features from D are not (yet?) implemented in Rust. Do you think those will have a chance to be implemented one day ?

  • .stringof property to get the name of a type

  • static if for conditional compilation depending on a type

  • typeof() to declare a variable of the same type than another one

For sure, D is older and more mature.

Thanks for your comments.


#2

D static if does far more than that.


#3

@leonardo

Maybe, but that’s the feature I’m interested in !


#4

This is currently only available through the unstable type_name intrinsic, but the Rust team has indicated that they will consider adding a stable wrapper for it.


#5

You can use a macro to get the name of a type, for example:

struct MyOwnType {
    foo: u32,
    bar: String,
}

macro_rules! type_to_string {
    ($type:ty) => {
        stringify!($type)
    }
}

fn main() {
    let stuff = MyOwnType{ foo: 42, bar: "Hello world!".to_string()};

    let type_name = type_to_string!(MyOwnType);

    println!("name of type: {}", type_name);
}

It prints:

name of type: MyOwnType

Or does it do s.th. different ?


#6

Macro method doesn’t work for generic type parameter.


#7

This code works:

struct MyOwnType<T> {
    foo: u32,
    bar: String,
    tmp: T,
}

macro_rules! type_to_string {
    ($type:ty) => {
        stringify!($type)
    }
}

fn main() {
    let stuff: MyOwnType<f64> = MyOwnType{ foo: 42, bar: "Hello world!".to_string(), tmp: 12.34};

    println!("name of type: {}", type_to_string!(MyOwnType<T>));
    println!("name of type: {}", type_to_string!(MyOwnType<f64>));
    println!("name of type: {}", type_to_string!(MyOwnType));
}

It prints:

name of type: MyOwnType<T>
name of type: MyOwnType<f64>
name of type: MyOwnType

Or did you mean s.th. different ?


#8

Yes, I meant something different. For example, name_of_element_type for Vec<T> shouldn’t be “T”, it should be “i32” for Vec<i32>, etc.


#9

Oh sorry, you mean after monomorphization ?
Yes that’s true, Rust macros are applied before that. So stringof in D is more useful indeed.
As @mbrubeck said it we’ll have to wait until type_name is available in stable.

For the other features: when would typeof() be useful in Rust?


#10

When you want something to the have the same unknown type of something else…


#11

That makes sense, but is there a real example when this is needed (where the type is unknown)?

If a specific type is not kown this usually involves a generc type parameter (in my example above T) and instead of typeof() you could use that parameter directly.

Maybe this involves some more complicated type-juggling than I can imagine :wink:


#12

Right. In D language inside a function “typeof(return)” is an alias of the result type of the function itself. This is used often (the better Rust’s type inference could make this feature a bit less useful, but type inference slows down compilation, while D is designed for a quick compilation).

Some usage examples. Here typeof(return) maked the code more DRY avoiding you to write the RationalT!U struct name twice:

RationalT!U rational(U)(in U n) pure nothrow {
    return typeof(return)(n);
}

In D you can call “.length” statically on a type that has such attribute (like fixed-size arrays). Here typeof() gives the type of the result of the ripemd160Of function. Such type is a fixed-size array, so its len gets computed at compile-time:

private enum RIPEMD160_digest_len = typeof("".ripemd160Of).length;

Sometimes you have the type elsewhere but typeof() helps you find it locally, ignoring how the type is called elsewhere:

char[] bitcoinEncode(in ref PPoint p) pure nothrow {
    ubyte[typeof(PPoint.x).length + typeof(PPoint.y).length + 1] s;

Another example of the same:

auto minD = Unqual!(typeof(T.re)).infinity;

You can define in a more DRY way static data inside a struct:

static immutable black = typeof(this)();

Another example of DRY:

struct GameOfLife {
  enum Cell : char { dead = ' ', alive = '#' }
  Cell[][] grid, newGrid;

  this(in int x, in int y) pure nothrow @safe {
    grid = new typeof(grid)(y + 2, x + 2);
    newGrid = new typeof(grid)(y + 2, x + 2);
  }

#13

That doesn’t seem particularly DRY to me, you’re repeating grid and newGrid twice on the same line. Surely specifying the type constructor (which in rust can omit type parameters) would make the code easier to read, as the reader wouldn’t have to go looking for the declarations of grid to understand what function is being called?


#14

Well, in that D code if you don’t want to write this:

grid = new typeof(grid)(y + 2, x + 2);
newGrid = new typeof(grid)(y + 2, x + 2);

You probably have to write:

grid = new Cell[][](y + 2, x + 2);
newGrid = new Cell[][](y + 2, x + 2);

Unless you define a type alias of Cell[][]:

struct GameOfLife {
    enum Cell : char { dead = ' ', alive = '#' }
    alias Grid = Cell[][];
    Grid grid, newGrid;

    this(in int x, in int y) pure nothrow @safe {
        grid = new Grid(y + 2, x + 2);
        newGrid = new Grid(y + 2, x + 2);
    }
}

In this code example this seems a better solution.


#15

Thanks for clarification leonardo!

It looks like it makes currently more sense in D than in Rust. D also has more type reflection at run time.


#16

D has more type reflection at compile-time!


#17

@willi_kappler: D’s typeof is analogous to C++'s decltype, which has been used in many places that we’ve traditionally identified the need for impl Trait in Rust. For example, a recognized pain point with writing abtract Rust is when we have generic-heavy code, like chained Iterator impls, or Futures in the async tokio crate, that commonly result in monomorphized types that are tough to write by hand. Try to write the return type of expression like this:

vec![1, 2, 3].iter().map(|x| x + 2).filter(|x| x % 2 == 0).map(|x| x * 2)

The return type here is actually std::iter::Map<std::iter::Filter<std::iter::Map<std::slice::Iter<'_, {integer}>, [closure@...]>, [closure@...]>, [closure@...]>. Gross, right? But we deal with this in Rust because we favor existential types and thus monomorphization, so we can optimize more through the functional paradigm we use regularly in Rust. This way, rustc can make some very expressive code zero-cost.

For D, it’s much the same. In a similar situation, we don’t necessarily want to use dynamic dispatch (interface or abstract class instance in D, trait object in Rust), but writing the type of a particular expression might be difficult or even impossible. In D’s case, an additional consideration is that there’s something called “Voldemort” types, where a function’s return type isn’t necessarily declared to client code. An example from D’s docs:

// Note: the return type is auto, because we cannot actually name it outside the function!
auto createVoldemortType(int value)
{
    struct TheUnnameable
    {
        int getValue() { return value; }
    }
    return TheUnnameable();
}

auto voldemort = createVoldemortType(123);
writeln(voldemort.getValue());  // prints 123

auto in this case, is totally necessary in order to get a result from the return of createVoldemortType. If we want to use a type variable for that return anywhere else, like in a template function, we would need typeof. That’s because while the type of a Voldemort type is unavailable to client code, the compiler still allows us to USE the type when exposed via return value, though it MUST remain unnamed (hence, “Voldemort” types!). Type inference in Rust is equivalent to using auto, but that doesn’t cover cases where a library would need to explicitly define types, like in a signature for a function.

Rust has a rather different design for type usage – if client code can use a given type in a library, then it needs to be pub, and have it’s declaration out in the open. For this reason, many libraries use their own wrapper structs, instead of giving back complicated Iterator types. This may become entirely unnecessary in the future once impl Trait lands, which I’m super excited for!

As a user of D and Rust, I’d also like to clarify that D makes heavy use of compile-time templating facilities, and typeof is one of those – in none of the examples that @leonardo showed were runtime facilities being used. In fact, D’s compile-time facilities are pretty expansive, and it’s not uncommon to work with entirely abstract compile-time-generated types in libraries. The Unqual template that @leonardo used above is a good example.

Note for @leonardo: AFAIK, type inference has not ever been a bottleneck for the Rust compiler. I don’t know if that was the intent of your comment with type inference and compilation speed, but I’d be interested to know just how much of an impact that particular feature has.

EDIT: Responding to @dandyvica’s original question, the bottom line for Rust is:

  • stringof: Coming someday!
  • static if: Uncertain. We’ll make progress with const generic args, but nothing as powerful has been planned AFAIK.
  • typeof: We’ll get something to handle common use cases for return type inference with impl Trait for return types, but for dealing with abstract types nothing seems planned for this AFAIK. That means for things like the length example @leonardo gave, there’s no equivalent planned. They sound interesting, though, and I’m sure an RFC could be made!

#18

Right.

One way to emulate a safer subset of D static if is to introduce a static if inside Rust functions that works on traits only. So it’s similar to trait specialization, but it’s used inside functions, avoiding combinatorial explosion of specialized functions, and allowing a subset of design by static introspection recently used in D (in allocators):

impl<A, T> Extend<A, T> for Vec<A> where T: IntoIterator<Item=A> {
    fn extend(&mut self, iterable: T) {
        static if T : &'a [A] {
            // specialized
        } else {
            // generic
        }
    }
}

(This is not going to work as is).