Why am I allowed to call Rng::gen?

I have the following code:

impl SettlementSize {
    fn human_readable(&self) -> String {
        match *self {
            SettlementSize::Village => "Village".to_string(),
            SettlementSize::Town => "Town".to_string(),
            SettlementSize::City => "City".to_string(),
        }
    }
}

impl Distribution<SettlementSize> for Standard {
    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> SettlementSize {
        let res = rng.gen_range(0, SettlementSize::iter().count());
        match res {
            0 => SettlementSize::Village,
            1 => SettlementSize::Town,
            2 => SettlementSize::City,
            _ => panic!("Unknown value!"),
        }
    }
}

fn get_settlement_size(mut rng: impl RngCore) -> SettlementSize {
    let size: SettlementSize = rng.gen();
    size
}

In the function get_settlement_size I am allowed to call gen, even thought that implemented in RngCore! It's implemented in Rng, which is extending Rng. When I look at the Rng trait documentation I see this:

An automatically-implemented extension trait on [`RngCore`] providing high-level
generic methods for sampling values and other convenience methods.

How does this work? How is the compiler allowed to just replace on trait with another? Can I have multiple automatic extension traits? Where I can read more about this? I tried googling for this, but I am not sure what is the main article about this, that clearly explains what is happening.

It's because Rng is provided through a blanket impl for all types that satisfy the RngCore trait. There's a lot more discussion of this in RFC 445.

1 Like

Thanks a lot, Raph! So the blanket impl will extend an existing trait (interface) with methods from other trait (interface) and say these method are callable OR does it go even deeper and say if Rng is implemented for RngCore, any place that accept Rng interface will also accept RngCore interface?

Does it mean that I can potentially have unexpected conversions? If B impl for A, and then C impl for B, a function can silently accept C when I pass A?

I remember once I hit a bug in C++ where a function expecting a string was accepting a bool, because bool got promoted to an int an that got promoted to a string. Are such situations possible?

It's not doing conversions, it's just extending the set of methods that are callable. So that class of bug seems unlikely.

And it won't accept Rng in all instances that accept RngCore. For example, a function that accepts dyn RngCore will not accept dyn Rng.

1 Like

There are two distinct properties when dealing with traits:

  • trait objects, i.e., type-erased objects that thus rely on virtual methods to each be able to provide different behaviors (this is similar to upcasting with inheritance, where the trait would be an interface / abstract virtual class, and where each type implementing the trait would be a direct descendant / child of that parent class. Then dyn Trait would represent the type of upcasted instances).

    As @raphlinus pointed out, in Rust currently there is no upcasting or downcasting between trait objects, so dyn Rng and dyn RngCore are distinct types with no relation to each other whatsoever (although you can embed virtual methods that can do the upcasting / downcasting...).

  • impl Trait for SomeType { ... }, in general, has to be perceived as namespaced "friend-like" (very wave-handed-ly) extensions.

    It is as if objects themselves defined a sort of "function resolution namespace", so that when one does instance.method(...), the compiler will look at the functions "namespaced" at typeof!(instance), and if it finds a fn method ... in it, call it as <typeof!(instance)>::method(instance, ...).

    And inside that "type namespace", one finds:

    1. First, the inherent impls, that is, the impl SomeType that thus do not involve any trait whatsoever;

    2. Then, for each trait Trait in scope (e.g., used or defined in the module or function body that contains the instance.method(...) call), it will look at that "intersection namespace" / sub-namespace of the functions defined inside the impl Trait for SomeType { ... } block.

    • (And regarding all this, there is some added complication in that it also looks for &SomeType impls, &mut SomeType impls, and even <SomeType as Deref{Mut}>::Target impls, etc. by dot . operator auto-deref.

All this to say, that doing

trait MyOwnCustomTrait { /* stuff */ }
impl MyOwnCustomTrait for Whatever { ... }

is a harmless thing to do, regarding action at a distance, at least: other users won't have MyOwnCustomTrait in scope (such trait may even be private and/or they may not know of its existence!), so their own method resolution sugar won't be affected by the fns that were defined inside that impl MyOwnCustomTrait for Whatever { ... }.

TL,DR: Extension trait are just a mechanism to allow people to define their own sugary .useful_method() for some types where it would have been perfectly possible to define fn useful_function() instead. Since functions do not compose that well, syntactically speaking, as method chains, it is great that people can define fn (SomeType).useful_method(Args...) -> Ret kind of functions, through extension traits.


An example to illustrate all this:

Indeed there is:

trait Rng /*
where
    Self */ : RngCore, // all implementors of `Rng` must `impl RngCore`
{
    fn gen<T> (self: &'_ mut Self) -> T
    where
        Standard : Distribution<T>,
     // Self : RngCore, /* from bound on trait */
    {
        Standard.sample(self)
    }

    ...
}

impl<This : RngCore> Rng for This {}

Now, imagine there being:

mod helper {
    pub
    fn gen<This, T> (this: &'_ mut This) -> T
    where
        Standard : Distribution<T>,
        This : RngCore,
    {
        Standard.sample(this)
    }
}

then, being able to write:

fn get_settlement_size (mut rng: impl RngCore) -> SettlementSize
{
    let size: SettlementSize = helper::gen(&mut rng);
    size
}

shouldn't come as a suprise.

Well, we can replace helper with Rng:

fn get_settlement_size (mut rng: impl RngCore) -> SettlementSize
{
    let size: SettlementSize = Rng::gen(&mut rng);
    size
}

And since Rng::gen used self to name its first parameter of type &'_ mut Self, then we can even use the .gen() method sugar directly, since the Rng trait is in scope.


So, again, it's just sugar. The only "danger" with that sugar would be that of shadowing a method with another, but this (almost)* never happens, given the priority that inherent impls have, and given that if two traits provide the same method and are both in scope, you will no longer be able to use this sugar because of that very ambiguity. So (almost) no subtle errors (or rather, lack thereof!) such as in C++.

  • (It also helps that Rust does not have implicit operator calls like C++ does, such as the conversion operators; the only thing that ever ressembles that in Rust is the special Deref{,Mut} trait(s), since it interacts with . dot operator auto-deref, and can indeed implicitly call the <typeof!(instance) as Deref>::deref{,mut}(instance). But since it is a built-in trait, only the "owner" of the type, i.e., the crate that defined that type can actually implement Deref{,Mut}, so there aren't that many suprises in practice from that point of view :slightly_smiling_face:)

* A trait impl whose receiver takes self: &Self will have priority over an inherent impl whose receiver takes self: &mut Self given how method resolution order operates.

2 Likes

No, it won't extend other traits.

A "blanket" impl is simply a conventional name for a trait that is provided for a large set of types. Impl<T> From<T> for T is also a blanket impl although there is no other trait being involved which could be extended.

Traits and (blanket) impls are not magic. It looks like you are assuming a lot more is going on than really is. It's this simple: impl<T: Foo> Bar for T means that Bar is provided for a type whenever it implements Foo. The compiler knows for every type whether or not it implements Foo, and then it can know just as well whether or not it should allow Bar's methods on it. There's no other direct interaction between the two traits at the type level.

(At the code level, such a bound usually implies that Bar's methods make use of Foo's methods, but that's neither necessary nor provable by the type system.)

Thanks a lot for all replies!

One more follow-up question: the reason that Rng helper methods that are called on RngCore can seamless switch to Rng object and pass that around is because:

trait Rng: RngCore

So even though I start with:

rngCore: RngCore
rngCore.gen()

when the context switches to gen(&self), self becomes of type Rng, which also implements RngCore type, making it support both. So this is a neat trick to have self be available as either type in calls originating from Rng methods?

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.