Rfcs: Expose the type_name intrinsic

Created on 27 Dec 2015  ·  167Comments  ·  Source: rust-lang/rfcs

We have an intrinsic which returns a &'static str naming a type but it's not exposed in a safe/stable manner anywhere: http://doc.rust-lang.org/std/intrinsics/fn.type_name.html

Something like this should suffice:

pub fn type_name<T: Reflect + ?Sized>() -> &'static str {
     unsafe { std::intrinsics::type_name::<T>() }
}
T-lang T-libs disposition-merge finished-final-comment-period

Most helpful comment

This has now been stabilized as core::any::type_name with &'static str return type

All 167 comments

What is the usecase for this?

Probably would be good for diagnostics/panic messages in libraries. E.g. .unwrap() would be so much better if it said “called unwrap() on a None variant of Option<MonoType>” IMO. Exposing this would allow making similar messages possible in libraries.

@sfackler why the Reflect bound?

We use bounds like Any and Reflect to make sure that people are opting into paramicity violations. Getting a name of a type might be harmless enough that we could drop the bound, though.

Thoughts, @rust-lang/libs?

I think you would have to drop the bound for that example to be workable, no? Option::<T>::unwrap would need to add a T: Reflect bound and that's a breaking change AFAIK. Edit: guess not.

I would ditch the reflect bound, but I'm not actually that familiar with our current reflection strategy. If this makes it incoherent, that's bad.

I am _slightly_ terrified that people will use this to impl adhoc name-based specialization.

You can already do that in a less messed up way by comparing TypeIds.

@Ticki macros don't have type information and don't work in generic contexts

@sfackler It needs the Reflect bound, or else Reflect really isn't doing much for us :)

That said, Reflect is likely going away anyway, if specialization lands.

OTOH mem::size_of already violates parametricity

@jonas-schievink Indeed, but in a much "lossier" way. (FWIW, I'm not terribly fond of the whole Reflect business in any case, but until we deprecate it, we should at least not introduce new leaks.)

I don't even see anywhere in-tree where this is used except test cases. It was added in https://github.com/rust-lang/rust/commit/e256b7f049b44fa697b9f9c5e75b4433a7d9ffdf but even then didn't appear to be used.

Please lets come up with a concrete use case and use it, not just add the feature speculatively.

@brson I opened this issue because it was a thing I wanted to use for a concrete use case.

Specifically, rust-postgres reports this error when the user attempts to convert a Postgres value to an incompatible Rust value or vice versa, for example a Postgres VARCHAR to a Rust i32, but right now all it can say is that a value of type VARCHAR could not be converted to... something. If type_name was stable, I could stick a &'static str in that error type and be set.

I would expect more something like

 // no need for Reflection, resolved at compile time, possibly make it even const
pub fn type_name<T>() -> &'static str
 // resolved dynamically, hence needs reflection, no obvious need for size bound
pub fn type_name_of_val<T: Any>(val: &T) -> &'static str

The former would be convenient when generating error messages as pointed out by @nagisa.
I expect the latter to be used in conjunction with Any, in order to provide a better error message when a downcast_{ref,mut} fails (in a way, this would provide a "string" version of TypeId):

pub fn downcast<'a, T: Any>(x: &'a Any) -> Result<&'a T, String> {
    match x.downcast_ref() {
        Some(y) => Ok(y),
        None => Err(format!("got {}, expected {}", type_name_of_val(x), type_name<T>())),
    }
}

@ranma42 You can implement type_name_of_val using type_name without requiring an Any constraint, so I don't think this is the right distinction to make.

One distinction I think we _should_ make is between things which exist for debugging/development purposes and things which are used for implementing "actual/intended program behavior". I think it's totally fine to pierce whatever abstraction boundaries you want in the former case, and not at all in the latter. Of course, how to prevent things intended for the former from being abused for the latter is an interesting question, but even Haskell has Debug.Trace which does IO inside pure code, but "only for debugging purposes".

So that then raises the question of what type_name is for. If it's mainly a debugging aid (I think you could plausibly argue that emitting better error messages falls in this category, even if that's something which is observable runtime behavior), then having it be constraint-free seems fine. On the other hand, if actual program logic is going to rely on it in some way, then I strongly believe it should require a Reflect/Any constraint (this kind of thing is what they _exist_ for).

@glaebhoerl I might be missing something (in particular, the signature I used on the function is possibly wrong), but in my mind the purpose for type_name_of_val would be to obtain the type of a value wrapped in a fat pointer. This type might depend on the actual execution (for example a &Any reference might point to an Option::<u32> or to a u32 indifferently). How could this be implemented using type_name?

Ah. In that case you probably wanted to write pub fn type_name_of_val(val: &Any) -> &'static str.

I also have a use-case for this (entity component system: asserting that each entity with a certain component has all of that component's dependencies). I can currently print a message along the lines of "Entity {} failed to fulfill all its dependencies", which isn't the most user-friendly.

One way to make the "only for debugging purposes" @glaebhoerl wants is to simply enforce it, by exposing a new fn like this:

#[cfg(debug_assertions)]
pub fn debug_typename<T>() -> &'static str {
    unsafe {
        intrinsics::type_name::<T>()
    }
}

#[cfg(not(debug_assertions))]
pub fn debug_typename<T>() -> &'static str {
    "[no type information]"
}

It doesn't need to be a macro, but it could be -- for some reason it feels better to have a macro that changes behavior at runtime (like debug_assert!) than a function. Also, if this function/macro is in core then intrinsics::type_name doesn't have to be stabilized.

@durka sadly this doesn't work quite right because cfgs are statically evaluated during compilation _of the library_. In this case, the standard library. So they would have to be using a custom "debug std" to see these names. Although perhaps this is what you're going for?

@Gankro meh, you're right. So that's the real reason it would be a macro :) -- that way it could expand into a #[cfg(debug_assertions)] in the user crate.

macro_rules! debug_typename {
    ($t:ty) => {
        (
            #[cfg(not(debug_assertions))] "[no type information]",
            #[cfg(debug_assertions)] unsafe { ::std::intrinsics::type_name::<$t>() }
        ).0
    }
}

I wanted to use this recently to add some instrumentation to std. Though that doesn't require stabilization I admit this is useful :)

I found this useful for adding meaningful debug logging to a function that accepts a generic type, where I was only concerned about figuring out if it was being called for certain types.

I'd like to add a stable wrapper on top of this, but I'm not totally sure where it should go. std::any seems like the least-wrong place?

We wanted to use this intrinsic before for pass names in MIR pipeline, but it seemed to return fully qualified pathes for foreign types, where all we hoped for was just the name of type.

It should either:

  • have an argument specifying whether the type_name returns fully qualified path; or
  • be renamed; or
  • be fixed to only ever return the type name only.

It may be overkill, but it could return a struct with the path, name, and generic arguments all split out.

I ran into this recently. In particular right now I have some default implementations that for some feature need to be filled in. However a user currently cannot tell for which type it's not implemented. This intrinsic helps this greatly.

What it looks like:

    fn into_deserializer_with_state(self, state: State) -> Self::Deserializer
        where Self: Sized
    {
        if !state.is_empty() {
            panic!("This deserializer ({}), does not support state", unsafe {
                ::std::intrinsics::type_name::<Self>()
            });
        }
        IntoDeserializer::into_deserializer(self)
    }

Additionally the sentry create currently parses Debug for this which is ridiculous :)

This intrinsic is very useful for macros and error reporting - it can make the difference between a useless failure message and a production issue that can actually be debugged.

Update, regarding discussion further up: the Reflect trait and the bound on type_name are gone.

I would propose to expose it with the full path only for now. It's also the format that comes in from backtraces at the moment and it would make sense to have a parsing function that takes such an identifier and breaks it into the individual elements.

Maybe the syn crate can already do that parsing?

Update, regarding discussion further up: the Reflect trait and the bound on type_name are gone.

type_name was never bounded on Reflect.

(Oh ok, so the bound was only in the same wrapper proposed in this thread. Regardless, the trait is still gone.)

For another example use-case of this:

I was thinking of implementing a Bracket wrapper type struct Bracket<T>(T);, that prints log messages on new and drop, with a counter for current object count (maintained by a static mutexed map of type_name->count).

I could use this for, i.e. instrumenting the File type almost transparently (implementing Deref for Bracket), so that current open-file count could be exposed via transforms on the log, or more directly by querying count on Bracket<File> and exporting to Prometheus/TSDB/procfs/etc.

OTOH the explicit enumeration of counted types is probably preferable in most apps. I'm gonna explore this further with TypeId and specialization to explicitly "subscribe" to counts.

Since stabilizing this would severely weaken parametricity (a meta-theoretical property of the type system) I've added T-lang and I want to block this on stabilization of specialization. Once that is stable, I think we can also stabilize type_name.

@Centril can you say more about how this relates to specialization?

@Centril Any is stable, and the last time an analogous situation came up, it was decided to stabilize discriminant_value as well without waiting for specialization. Given that, I don't think there is much parametricity left to preserve at this point. Wish it weren't so.

@SimonSapin type_name enables you to extract information about the concrete type a type parameter is instantiated at and make decisions based upon it. This makes it easier to violate parametricity. Specialization completely obliterates the property.

@glaebhoerl

@Centril Any is stable, [...] Given that, I don't think there is much parametricity left to preserve at this point. Wish it weren't so.

Sure, it is true that Any makes parametricity violations easier; but you still have to bound on T: Any to do it. It is similarly true that discriminant_value, size_of, and align_of also make it easier to violate parametricity, but I think type_name can extract much more information and more easily / conveniently (so violations are more likely to occur).

[...] the last time an analogous situation came up, it was decided to stabilize discriminant_value as well without waiting for specialization. [...]

Sure; I think that was a mistake. I don't want to throw out whatever we have left of parametricity without being sure that we gain something of near-equal value in return. Given the uncertainty around the soundness of specialization, I think that we should have, and we should wait until we know that we can stabilize specialization before weakening parametricity any further.

type_name enables you to extract information about the concrete type a type parameter is instantiated at and make decisions based upon it.

But by that logic the only path forward is not to have type_name which seems to be a terrible idea. I see much more value in having type_name available than to prevent parametricity violations.

you still have to bound on T: Any to do it

impl<T: 'static> Any for T exists so you really only need to bound by 'static. I guess we still have some vague semblance of parametricity for non-'static types as a result, kind of as an accident.

@mitsuhiko The path forward is to stabilize specialization; then we can have a stable type_name. I don't think type_name is nearly as valuable as parametricity; but the performance benefits of specialization and the types of ergonomic APIs it may enable are in the same league.

@glaebhoerl oh true; but bounding on 'static is still something many wouldn't be willing to do, so I think some parametricity is preserved; not a lot, but I want to retain what little we have left.

@Centril The argument that makes sense to me is that if type_name weakens parametricity in the language, then that weakening has already effectively occurred in the ecosystem (a stable type_name already exists and is useful and has seen usage in the form of the named_type and typename crates). And so to me at least, stabilising type_name is improving the ergonomics of something that is evidently already extant and valuable, rather than introducing a novel weakening to the Rust ecosystem. (And as far as I'm aware, weaking parametricity here doesn't seem impactful to the complexity of rustc now or likely in the future.)

For what it's worth I've found leveraging type_name in error messages to be very valuable for debugging. I haven't yet been able to come up with an alternative that fulfils this need but doesn't infringe on parametricity.

@alecmocatta None of named_type or typename violate parametricity in the same way as Debug or Show in Haskell does not violate parametricity; these have traits which must be derived or manually implemented. There's nothing universal about these traits. They don't apply for all types.

I think if the solution will be adding a TypeName trait then I think we might as well not bother with it. It's unlikely that any of the usescases for this function (certainly none that I have) would work by forcing another traint bound to exist.

Why is it useful to enforce parametricity at the language level? As far as I can tell the Secret<T> example in https://aaronweiss.us/posts/2018-02-26-reasoning-with-types-in-rust.html relies on privacy more than parametricity.

The old RFC threads about specialization dug pretty deep into "why does parametericity matter?" As far as I could tell, they concluded that parametericity doesn't translate into any practical benefits for Rust, which is why specialization was accepted at all. So I'm kinda surprised this is being brought up again.

@Ixrec Most likely that's because @Centril wasn't on the language team when that happened, and seems to have a different assessment. (I wish he had been!)

"Parametricity" is a pretty high-falutin' word, I wish we had a more down-to-earth one; the basic gist is just that if other functions and libraries do dynamic casts and reflection on the types you pass to them, they have to declare this in their API.

Parametricity has been discussed before and the conclusion was it's not
critical for Rust, and anyway we are pretty committed to stabilizing
specialization. That's why we got rid of Reflect.

I understand your line of reasoning (rip off the parametricity bandaid all
at once) but it has the effect of holding up this small useful feature
based on an unrelated large feature that may yet have unpredictable delays.
So that's just frustrating for people who want this, and I would urge
reconsideration.

On Fri, Oct 19, 2018, 08:41 Ixrec notifications@github.com wrote:

The old RFC threads about specialization dug pretty deep into "why does
parametericity matter?" As far as I could tell, they concluded that
parametericity doesn't translate into any practical benefits for Rust,
which is why specialization was accepted at all. So I'm kinda surprised
this is being brought up again.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/rust-lang/rfcs/issues/1428#issuecomment-431349570,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAC3nx7AynBu_wycf7jHtsIwxmQ-Wb_Iks5umch7gaJpZM4G7iEP
.

For reference, the comment I was thinking of was https://github.com/rust-lang/rfcs/pull/1210#issuecomment-187777838 The most relevant bits imo are:

  • achieving true zero-cost abstractions ultimately requires some sort of special-casing -- for example, writing a customized variant on extend that uses memcpy for slices of copy types -- since you want to bypass the "generic" implementation when you can write a more efficient one that is tailored to specific types or capabilities

    • this goal is fundamentally at odds with parametricity

  • we haven't found a compelling need for parametricity in reasoning about Rust code, whereas having effective, zero-cost abstractions is crucial
  • when it comes to unsafe code, any guarantees that one might have wanted from parametricity can also be achieved -- and can be achieved more robustly -- with privacy

    • although the ergonomics here could admittedly be improved

afaik this is all still true today

FWIW, I have a very incomplete RFC draft lying around for a structural type_name, but it is so low on my TODO list that I’ll probably never get back to it.


I agree that this functionality definitely does not carry its weight at the moment and there’s no particular rush to stabilise it before the larger, more interesting and impactful features. Sure, it is nice to have, but it does not block anything that was not possible until today in library with a well thought out trait.

I am aware of all this discussion from back then.

we haven't found a compelling need for parametricity in reasoning about Rust code, whereas having effective, zero-cost abstractions is crucial

This links to an elaboration by Aaron in which he writes:

His perspective is that, first of all, parametricity tells you more in a language without side-effects, like Haskell.

This is directly untrue today. const fns are referentially transparent and so you may prevent side-effects.

I also disagree with the notion that "Privacy is the new parametricity".
Parametricity is a global property a type system enjoys which lets you reason about polymorphic functions just by seeing their type signatures; (you may derive "free theorems", e.g. for<T> impl Fn(T) -> T must diverge or be the identity function). Privacy is a local property which will not let you derive such theorems, or at least, not nearly as readily.

Back then, @glaebhoerl wrote:

But the specific concern was whether we understand why Haskellers consider it so valuable, and this continues to hold.

I think this was largely left unanswered. As a fellow Haskeller, I think it is a valuable property for a language to have.

That said, zero cost abstractions or type-case in a dependently typed language (or with traits and using associated types...) are also valuable properties. But I want to make sure that we get those valuable properties in trade for the amount of parametricity that we have left. Not getting the zero cost abstractions and losing parametricity at the same time is no good trade-off to me.

So that's just frustrating for people who want this, and I would urge reconsideration.

I do not understand the urgency here; there hasn't been much movement on this issue anyways for several months before I wrote my comment, and then suddenly there were a lot of them. At most, type_name make some APIs more ergonomic, for example dbg!(...) but that requires specialization anyways.

It does not just make it more convenient. In production error reporting systems absolutely need this. We have server apps where we resort to running s regex over Debug to approximate the type which has a massive performance impact.

A stable API to extract that type would help us tremendously.

@mitsuhiko I empathize, but I cannot in good conscience trade type system properties for short term benefits. I would suggest using named_type or typename in the interim if performance is a concern; this will make the APIs less ergonomic, but at least you don't need to use regexes then.

Those crates cannot be used in my case because they require new trait bounds I do not have.

I'm still confused. Aren't we losing the type system property anyway when specialization stabilizes? What's the motivation for preserving it longer when that type system property is itself only a short term benefit? Or are we suggesting reopening the question of whether we should add specialization to Rust at all?

I have no opinions on whether this particular feature is urgent or not, I'm just baffled as to where all this sudden talk of parametricity is coming from.

Aren't we losing the type system property anyway when specialization stabilizes?

We are.

What's the motivation for preserving it longer when that type system property is itself only a short term benefit?

I think the property has more value than what type_name provides as long as it exists; I want to ensure that we get something of equal value in return when removing it (specialization). In particular, I want to ensure that it doesn't turn out that specialization cannot happen for technical reasons and then we lost parametricity anyways.

Or are we suggesting reopening the question of whether we should add specialization to Rust at all?

I am not.

In particular, I want to ensure that it doesn't turn out that specialization cannot happen for technical reasons and then we lost parametricity anyways.

Ah, that point actually makes a lot of sense to me. Thanks for clarifying.

I quickly skimmed through the conversation. One use case I didn't see mentioned is augmenting the lack of type resolution for proc macros.

I recently worked on a type system for extern functions. A simplified version of it is:

// Trait for defining type mappings.
trait ExternType {
    type InputType: ExternInto<Self>;
}

// Trait implementation for type.
// User code can specify more of these for their own types!
impl ExternType for String {
    type InputType = *const c_char;
}

// Define extern-enabled function.
#[derive_extern]
fn foo(s: String) { ... }

// Expands to:
extern "C" fn extern_foo(s: <String as ExternType>::InputType) { ... }

The problem I faced is that I want to generate a IDL/C++ wrapper for the above function. My previous approach had a list of hard coded types in the procedural macro that knew that String will be represented as *const c_char. However such a hard coded list couldn't be extended by the crate user.

Implementing that logic in Rust's type system felt more elegant for the Rust code. The _huge_ downside is that there's no clear way to figure out what the _real_ type behind <Bar as ExternType>::InputType is.

While the ability to resolve that into a concrete type through a "runtime" (as opposed to macro expansion time) function wouldn't help the macro-use case, it would allow the crate to implement its own extern "C" fn get_extern_types function that a command line tool could then hook into when given a pre-compiled binary.

Also the current crates do not serve it, as they cannot resolve associated types into concrete types.

I admit this is somewhat of a niche use case, but I wanted to bring it up as I feel it's somewhat orthogonal to the current reflection/debugging use cases mentioned above.


Edit: Oh, and I fully understand that the type_name provides kind of a workaround for a workaround in this use case. Ability to inspect the associated types somewhere during the compilation would be a lot better (and still affect the compilation output for linking IDL as Windows resources).

I still really, really want this intrinsic stabilized but I'm not sure what the path forward here is at this point. Is there something that could be done to move this towards API availability?

Is there something that could be done to move this towards API availability?

Help with the design and stabilization of specialization.

@Centril so this is blocked until we know that specialization works or does not work, because if specialization does not work parametricity becomes an option again and would prevent this feature from being ever stabilized?

@mitsuhiko We already lost parametricity for generic functions with 'static bounds on their type parameters. What remains is some portion of parametricity for parameters that don't have these bounds (except for size_of, align_of and such operations that violate parametricity but in relatively harmless ways...).

When we stabilize specialization we lose what remains, and so type_name does not remove any properties from the type system that specialization hasn't already removed. When that happens type_name therefore becomes OK for me to stabilize in some form (we'll still need to work out the right interface for type_name.. maybe something like in https://github.com/rust-lang/rfcs/issues/1428#issuecomment-431359322 or something else...).

would prevent this feature from being ever stabilized?

Yes.

This seems completely orthogonal to specialization - I don't see how this would block on that in any way. There are quite a few non-parametric functions in the standard library already.

would prevent this feature from being ever stabilized?

Yes.

As far as I understand this is your personal opinion, @Centril. I don’t know of any team’s decision on this either way. My personal opinion is that this feature is more valuable than parametricity, and we should stabilize it regardless of what happens with specialization.

This is a three-years-old tracking issue for useful functionality that has been around since roughly forever.

This thread has proposals to change the return type to a structure (I assume similar to https://docs.rs/syn/0.15.23/syn/struct.Path.html), or to show the concrete type behind a trait objects. These could be added later as new APIs, so I don’t think they need to block stabilizing what’s already implemented.

The only real concern has been the loss of parametricity, in case specialization never happens.

At this point, I believe there is well enough desire for specialization that we’ll find a way to make it happen, in one form or another, although it may take time. In the mean time, I don’t think we should block already-implemented useful functionality, just in case we potentially give up on specialization entirely.

Regardless of what happens to specialization, we already have Any, mem::discriminant, size_of, and possibly other existing features that weaken/remove parametricity in Rust.

Finally there is already extensive discussion in the Rust around the removal of the Reflect trait and acceptance of specialization about moving away from parametricity, and whether it is actually useful to Rust.

Given this repeated precedent, I think the onus of proof is on people arguing that we should “retain what little we have left” to show why that is valuable.

As such, I am formally proposing that we add a safe stable wrapper function for fn type_name::<T: ?Sized>() -> &'static str (in a module to be determined), despite this concern.

@rfcbot fcp merge

Team member @SimonSapin has proposed to merge this. The next step is review by the rest of the tagged team members:

  • [ ] @Centril
  • [x] @Kimundi
  • [x] @SimonSapin
  • [x] @alexcrichton
  • [x] @aturon
  • [x] @cramertj
  • [x] @dtolnay
  • [x] @eddyb
  • [x] @joshtriplett
  • [x] @nikomatsakis
  • [x] @nrc
  • [x] @pnkfelix
  • [x] @scottmcm
  • [x] @sfackler
  • [x] @withoutboats

Concerns:

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

LGTM as long as we make the same caveat that we do for Debug impls -- the exact representation is subject to change and we will break your code if you expect it not to change.

In particular, how will this work with the module part of the name? I suppose it's too early to guarantee any particular path (for example, would the type name of a Vec<i32> be "std::prelude::v1::Vec<i32>" or "std::vec::Vec<i32>" or "alloc::vec::Vec<i32>" ?). This can be a source of de facto breakage in later versions. Imagine, we want to refactor and move the Vec type into private module alloc::vec::internal::Vec and of course reexport it at the old location. Would this "break" the type name?

Potentially surprising: the same type can have different type_names in different places:

#![no_std]
#![feature(core_intrinsics)]

pub fn p() -> &'static str {
    unsafe {
        core::intrinsics::type_name::<dyn Fn()>()
    }
}

pub fn q<T: ?Sized>() -> &'static str {
    unsafe {
        core::intrinsics::type_name::<T>()
    }
}
#![feature(core_intrinsics)]

fn main() {
    println!("{}", lib::p());
    println!("{}", lib::q::<dyn Fn()>());
}
dyn core::ops::Fn()
dyn std::ops::Fn()

As far as I understand, the only use of this feature that we actually want to support is better logging/debugging info, in which case the different names shouldn't be a serious issue. It might make it a bit harder to aggregate crash messages in a large system, but that's pretty niche, I think?

@nagisa noted that:

FWIW, I have a very incomplete RFC draft lying around for a structural type_name, but it is so low on my TODO list that I’ll probably never get back to it.

which suggests to me that the API proposed for stabilization might not be optimal.

@rfcbot concern is-this-the-right-api?


As @dtolnay and @bluss noted, it's not clear from the proposal what the guarantees around the API type_name are... what changes may the compiler team do to the output of this function? Until this is resolve, let's note:

@rfcbot concern unclear-guarantees-and-underspecified

The results above due to @dtolnay also points at incoherence. In particular, this shows that you cannot rely on type_name giving globally consistent results. That does seem surprising...


@rfcbot concern premature-loss-of-remaining-parametricity

First, I note that @nagisa wrote:

I agree that this functionality definitely does not carry its weight at the moment and there’s no particular rush to stabilise it before the larger, more interesting and impactful features. Sure, it is nice to have, but it does not block anything that was not possible until today in library with a well thought out trait.

I agree with this assessment.

At this point, I believe there is well enough desire for specialization that we’ll find a way to make it happen, in one form or another, although it may take time.
In the mean time, I don’t think we should block already-implemented useful functionality, just in case we potentially give up on specialization entirely.

I do not agree with this way of dealing with type system properties. Specialization has proven itself to be tricky over time wrt. soundness and has problematic interactions with other proposed features. While I'd like for specialization to happen, and I think we should focus efforts into doing that, the risk is too great here.

Regardless of what happens to specialization, we already have Any, mem::discriminant, size_of,
and possibly other existing features that weaken/remove parametricity in Rust.

Yes, we do. And you have essentially type-case for all type parameters bounded (implied or explicitly) by 'static. However, as many do not want to bound their type parameters with 'static, to be more generic,
I believe that parametricity still applies to sufficient extent to be more useful than type_name.

Finally there is already extensive discussion in the Rust around the removal of the Reflect trait and acceptance of specialization about moving away from parametricity, and whether it is actually useful to Rust.

Given this repeated precedent, I think the onus of proof is on people arguing that we should “retain what little we have left” to show why that is valuable.

I've written about this to some extent here: https://github.com/rust-lang/rfcs/issues/1428#issuecomment-431363029
When specialization was first considered, @glaebhoerl noted that:

But the specific concern was whether we understand why Haskellers consider it so valuable, and this continues to hold.

From what I can tell, this concern was never adequately addressed or even at all. Seeing the value of parametricity doesn't just extend to Haskellers. For example, Idris and Agda prohibit pattern matching on Type which means that you have parametricity for types.

I would also note that one specific rationale that Aaron brought up in the discussions on specialization was:

His perspective is that, first of all, parametricity tells you more in a language without side-effects, like Haskell.

Given that we have const fn, this rationale for Rust no longer holds.

Furthermore, as for the usefulness of parametricity, it's not just about cute "free theorems" about knowing that fn ??<T>(x: T) -> T { ?? } is the identity function or will diverge. The value it brings is that more generally you can learn things about a function solely from its signature which makes types as a means of documentation more valuable. This also makes it possible to write a generic algorithm without having to fear whether someone special cased something in a weird way somewhere deep in your call stack. Or as noted in this paper:

Parametricity can be thought of as the dual to abstraction.
Where abstraction hides details about an implementation from the outside world,
_parametricity hides details about the outside world from an implementation._

This makes it possible to reduce the overall reasoning-footprint of code.

which suggests to me that the API proposed for stabilization might not be optimal.

The use case I had in mind for type_name when I added it was very simple - an aid to diagnostics. For example, we could use it to produce errors in rust-postgres that say "unable to convert from the Postgres type TEXT to the Rust type Vec<u32>" rather than "unable to convert from the Postgres type TEXT to some Rust type". I don't really care if that message contains Vec<u32>, std::vec::Vec<u32>, alloc::vec::Vec<u32> or whatever.

None of that requires the ability to crawl the type name's AST, and I do not understand what use cases would want to do that.

While I'd like for specialization to happen, and I think we should focus efforts into doing that, the risk is too great here.

What is the specific, concrete, risk of the existence of this function, and how does that have anything to do with specialization? This API does not use specialization, and cannot be implemented in terms of specialization.

However, as many do not want to bound their type parameters with 'static, to be more generic,
I believe that parametricity still applies to sufficient extent to be more useful than type_name.

What is the specific, concrete, usefulness of parametricity in non-static type parameters today? Are functions with 'static bounds on their generics actually more scary to use in practice? Are there large swaths of code that do bad things by being non-parametric with 'static type bounds?

The value it brings is that more generally you can learn things about a function solely from its signature which makes types as a means of documentation more valuable.

Why should I care how much I can learn from a function's signature in isolation? It's sitting right next to the function's name and the actual documentation!

This also makes it possible to write a generic algorithm without having to fear whether someone special cased something in a weird way somewhere deep in your call stack.

There is an infinite amount of weird things that bad codebases can do. Don't use codebases you don't trust to not do bad things. Someone could system("rm -rf /") deep in my call stack as well, and the type signatures I'm looking at won't tell me that.

My interest is diagnostics and reporting to upstream crash reporters. I’m absolutely okay with types not being guaranteed anything about stability or format. Similar restrictions already apply to function names in the callstack and we have to deal with that.

Is it consensus that this function is only intended for debugging and error-printing purposes, or is that so far only some peoples' opinion?

If it's consensus, maybe we should tweak the API to make it more explicitly apparent. Call it type_name_for_debug or debug_type_name, or put it under some appropriate module (e.g. debug) if one exists (but I don't think it does?), or perhaps make a struct TypeName<T> ZST and implement Debug for it (and perhaps Display in the future, if we ever change our minds about "just for debug").

As I've apparently already written a long time ago, I think "only for debugging" also offers us a principled rationale to ignore the parametricity violation and potentially bridge that impasse. After all, gdb can also trivially be used to violate any abstraction boundary. The purpose of type system properties and restrictions is to regulate the interaction of the program with itself and with its environment; not with the developer. Haskell also has Debug.Trace, which is a "blatant" violation of purity, justified on similar grounds. In both cases it's possible to abuse these for non-debugging-related purposes, but we expect users not to do that, and would tell them so in the documentation.

I believe that is the consensus among people in this thread who are finding this feature useful and want it stabilized.

FWIW, I have a very incomplete RFC draft lying around for a structural type_name, but it is so low on my TODO list that I’ll probably never get back to it.

which suggests to me that the API proposed for stabilization might not be optimal.

"very incomplete draft lying around that I’ll probably never get back to" does not sound like a strong use case to me.

I believe that is the consensus among people in this thread who are finding this feature useful and want it stabilized.

That seems to be the case, yes. However it is fairly easy to imagine people doing things that are not supported (e.g. trying to parse the type returned from type_name). This was proposed elsewhere at some point, despite general consensus being that the format of type_name is not stable at all.


FWIW, I have a very incomplete RFC draft lying around for a structural type_name, but it is so low on my TODO list that I’ll probably never get back to it.

which suggests to me that the API proposed for stabilization might not be optimal.

"very incomplete draft lying around that I’ll probably never get back to" does not sound like a strong use case to me.

I wrote the chunk of said draft during the all-hands last year, but then found myself distracted by more interesting and wider-reaching problems. I have to allocate my finite time somehow, and type_name was one of the things that ended being cut.

I still do think that some change to the function is necessary before it is stabilised. For all it is worth it could be something similar to what we have done to std::mem::discriminant by having type_name return an opaque wrapper with certain standard trait implementations. That seems like an easy change to make which would keep ourselves open to changing this intrinsic in more ways than otherwise would be viable.

The compiler is a long-time user of this intrinsic. The current use of the intrinsic ended up being post-processed (in a very crude way) to produce the desired output. This is the sort of grey area, where I’m not sure whether it still considered "debugging" or not. If we had to do that in the compiler, I’m sure people will end up doing the same with the stable intrinsic and then come back complaining that stuff broke what their paying customers see when its output changes.

an opaque wrapper with certain standard trait implementations

The relevant trait would be Debug or Display. This does not feel very different from a &'static str.

It allows to make changes to the type (making it structural, adding methods to change it output, etc) in a backwards-compatible manner down the road.

For what it’s worth I see no benefit in a wrapper trait but it makes using it harder. The fact that it’s a static str right now is really convenient in certain APIs that already require a str.

As an example the new Fail::name returns a &str

Stabilizing type_name that returns &str does not prevent adding another function later that returns a more complex type.

Returning a type that implements Debug has the advantage that the rule “Debug output is not supposed to be stable” is already well-known, while returning &'static str would require adding another rule explicitly for type_name, and thus increase the likelihood of someone misusing it.

I'd love to have this available for debugging purposes.

While I don't really want to start a bikeshed, I didn't see a discussion of where this should live or what it should be called. (I assume we still don't want to stabilize anything new in intrinsics...)

For the std::error evolution we also want to expose a Backtrace object. I feel like these are related to each other. I would propose to add a std::debug or similar for both this and Backtrace once it lands. Once it's in a debug module I think the name type_name for the function is also not bad.

@Centril Have you had a chance to take a look at the discussion above with respect to any or all of the three separate concerns you've raised?

The question of parametricity seems to be mostly about avoiding having code out there that makes behaviour dependent on the names of types.

This concern can be reduced:

  1. Make the function return a struct, TypeName<T>, with no public methods or fields, _except_ a Debug impl, so format!("{:?}", type_name::<Foo>()) is the only way to get the name.
  2. It's already well-documented that you shouldn't rely on a Debug impl for logic. But further documentation on type_name can emphasise that the output is not consistently formatted, and the implementation may arbitrarily pick any type alias or representation.

Two possible implementations:
https://gist.github.com/peterjoel/a808efe76286a406b05e7d303ba6ef02

Ping @Centril @aturon @pnkfelix @scottmcm @withoutboats

+1 to this feature and @peterjoel's suggested implementation. In the AWS Lambda Runtime, we'd like to be able to report an unwrapped error to the Lambda APIs, and exposing this information would make debugging for our customers easier.

(@sapessi would be able to say more.)

@Centril Specialization of concrete types that do not contain lifetimes is not concerning with respect to soundness, but certainly results in loss of parametricity. There are also many cases where it is absolutely essential to performance. I believe strongly that we will offer this functionality, and soon.

Furthermore, I don't believe that the loss of parametricity offered by stabilizing this API is a significant one. I anticipate most usages of this code will result in things like logging, tracing, and debug print statements, not if type_name == "MySpecificType" { / * do different thing * / }. Exposing this information to debugging tools is incredibly important and, to me, significantly outweighs the risk of people doing obviously bizarre things that break a conceptual property we don't expect to enforce in the future.

I must say I don't get what “loss of parametricity” you are all speaking about:

pub fn type_name<T: Default + Debug>() -> String {
    format!("{:?}", T::default()).split_whitespace().next().unwrap().to_owned()
}

(playground link)

It's very far from being perfect (and this is why we need type_name), but type_name is not doing anything more than an auto-derived auto-required trait that implements the type_name method. Parametricity is a lie. (Yes, I know I'm exaggerating this last sentence, but we already have auto-derived auto-required Sized, so we can't say automatically-added bounds are new to rust)

(Feel free to downvote this comment and not answer if this has already been discussed before to not add to the noise, I must say I haven't read all the previous discussions)

If parametricity is supposed to be a goal I really want to understand why. For me from a user's perspective having parametricity prevents a lot of things like type_name I actually care about and I can't say I ever asked for what parametricity would give me. More debug functionality on the other hand ranks pretty high on the list of things I care about.

type_name is just one thing here. I could image future APIs that would give us greater access to the backtrace and debug info which would completely break all kinds of things we are apparently trying to preserve here.

I do think parametricity is a valuable concept to consider when designing functionality, in that you generally want the functionality provided by an API to be agnostic to the concrete details of the interface implementation it was passed. @Ekleog's function does not violate parametricity for these reasons, in that it does not interact with the underlying value or depend on its implementation details in a way that isn't specified by the interface (Default + Debug) (modulo the bit about expecting that the Debug output contains a non-empty string with the name of the type in the position before the first space). Even when exposing functionality that is non-parametric, it is useful for organizational purposes to only interact with the underlying type in ways that appear to the caller as though the function were interacting through the interface that you promised it. (you see this all the time with code in Java etc. relying on downcasting winding up being a confusing mess because it relies on implementation details of an interface that shouldn't have been publicly visible).

However, I think it's more important to expose good debugging tools and allow developers to implement otherwise impossible optimizations than it is to force developers to not shoot themselves in the foot by writing non-parametric APIs. There're lots of other ways to write APIs that are an unreadable mess, and lots of good ways to write really solid APIs that rely in subtle ways on well-encapsulated breakage of parametricity.

@cramertj

There are also many cases where it is absolutely essential to performance. I believe strongly that we will offer this functionality, and _soon_.

Specialization is in a real sense giving up on "we can have both performance and robustness" in that parametricity is a robustness property that you are trading for performance. It is also anti-modular in its essence. This can be noticed when you try to combine the trait system and the visibility system we have. I would have preferred not to have specialization in the first place but Any is already stable so specialization provides more value than the amount of parametricity we have left does.

I've noted before that once we have stable specialization, any notion of parametricity we had for type variables is gone (tho not for lifetime variables). Once we have an even more permissive type-case than type_name provides for on stable, there's no value in not providing type_name. However, until such time I am unwilling to consent to further loss of parametricity.

I anticipate most usages of this code will result in things like logging, tracing, and debug print statements, not if type_name == "MySpecificType" { / * do different thing * / }.

I don't disagree. However, I also think it will be abused for type-case while specialization isn't stable and it may also cause stability issues due to reliance as noted by @nagisa in https://github.com/rust-lang/rfcs/issues/1428#issuecomment-450551037. Events like https://github.com/rust-lang/rust/issues/58794 do not give me confidence that things not considered guaranteed will be respected as such.

Exposing this information to debugging tools is incredibly important and, to me, significantly outweighs the risk of people doing obviously bizarre things that break a conceptual property we don't expect to enforce in the future.

I also think debugging tools are important; that's why I proposed dbg!(...) and suggested that it be able to use type_name for more convenient debugging. I'd love to use it myself. While I much appreciate your notes in that they take parametricity seriously as a property, and about the mess type-case results in due to instanceof in Java et. al I simply don't agree that it is more important to expose type_name in a way that results in more loss of parametricity.


However, I believe there is a path forward towards having the functionality without further loss of parametricity.

Based on https://github.com/rust-lang/rfcs/issues/1428#issuecomment-450560869:

Returning a type that implements Debug has the advantage that the rule “Debug output is not supposed to be stable” is already well-known, while returning &'static str would require adding another rule explicitly for type_name, and thus increase the likelihood of someone misusing it.

and https://github.com/rust-lang/rfcs/issues/1428#issuecomment-466398958:

The question of parametricity seems to be mostly about avoiding having code out there that makes behaviour dependent on the names of types.

This concern can be reduced:

1. Make the function return a struct, `TypeName<T>`, with no public methods or fields, _except_ a `Debug` impl, so `format!("{:?}",  type_name::<Foo>())` is the only way to get the name.

2. It's already well-documented that you shouldn't rely on a `Debug` impl for logic. But further documentation on `type_name` can emphasise that the output is not consistently formatted, and the implementation may arbitrarily pick any type alias or representation.

Two possible implementations:
https://gist.github.com/peterjoel/a808efe76286a406b05e7d303ba6ef02

I would agree to the following implementation:

#![feature(core_intrinsics)]

use core::{fmt, any::Any, marker::PhantomData, intrinsics::type_name as type_name_impl};

struct TypeName<T: ?Sized>(PhantomData<T>);

impl<T: ?Sized + Any> fmt::Debug for TypeName<T> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.write_str(unsafe { type_name_impl::<T>() })
    }
}

fn type_name<T: ?Sized + Any>() -> TypeName<T> {
    TypeName(PhantomData)
}

fn main() {
    dbg!(type_name::<String>());
}

provided that:

  1. The bug in https://github.com/rust-lang/rfcs/issues/1428#issuecomment-450535964 is fixed such that the output gives coherent results (e.g. not sometimes core and sometimes std).
  2. The Any bound will remain until such time that the language team consents to otherwise (and this should be explicitly noted).
  3. We provide no stability regarding the output of the Debug implementation and say so explicitly.

Such a design I would welcome.

The bug in https://github.com/rust-lang/rfcs/issues/1428#issuecomment-450535964 is fixed such that the output gives coherent results (e.g. not sometimes core and sometimes std).

AFAIK the only general way to fix that is to make it use "absolute" (like e.g. symbol names do) and not "relative" (like error messages) paths.

Also, we have to make sure we're synchronizing the change between miri and rustc_codegen_{ssa,llvm} (ideally the latter should use the former, but, alas).
cc @oli-obk @michaelwoerister

AFAIK the only general way to fix that is to make it use "absolute" (like e.g. symbol names do) and not "relative" (like error messages) paths.

Yep, that's what I would do 👍.

Specialization is in a real sense giving up on "we can have both performance and robustness" in that parametricity is a robustness property that you are trading for performance. It is also anti-modular in its essence.

This has been stated many times now but even after months I cannot come up with a case where parametricity would actually give me robustness. Since its held up here in this thread as a goal I think it would make sense to actually clarify in clear teams what exact problems it solves or prevents in practice.

The proposed TypeName trait sets us down a path where this function is significantly less useful (no longer give a &'static str which would be needed in a few places) and it also sets us further down the road not actually having a conversation why we need this parametricity. This will not be the last debug helper people want (i know I want more) and this would require this fight at every point. Additionally TypeName requires that trait bound which again does not work in many places where this is currently necessary. For instance one of the usecases in this thread was debugging serde where no such trait bound exists.

@Centril

I would agree to the following implementation:

I would favour the _other_ version (a wrapper for a &'static str) since it doesn't have a type parameter, so all TypeNames are the same type, making it more convenient for iteration or storing in collections.

Suppose you are collecting a series of errors and then later formatting them into log messages. In order to store them in a collection or iterate over them, the type parameter forces you to either box the names (as Box<dyn Debug> perhaps) or prematurely convert them to owned Strings. The latter isn't something that we'd want to push people towards.

The talk about parametricity feels like it's in a bizarre stalemate where both sides think their case has been clearly established already and the other side is missing the point somehow. I know I'm still waiting to hear a plausible argument for why parametricity has any practical impact on robustness of Rust code.

My understanding was that the only reason this argument still has any signifiance at all is because when I asked:

What's the motivation for preserving it longer when that type system property is itself only a short term benefit?

@Centril's response was:

I think the property has more value than what type_name provides as long as it exists; I want to ensure that we get something of equal value in return when removing it (specialization). In particular, I want to ensure that it doesn't turn out that specialization cannot happen for technical reasons and then we lost parametricity anyways.

@SimonSapin's FCP comment seems like a clear response to that:

The only real concern has been the loss of parametricity, in case specialization never happens.

At this point, I believe there is well enough desire for specialization that we’ll find a way to make it happen, in one form or another, although it may take time. In the mean time, I don’t think we should block already-implemented useful functionality, just in case we potentially give up on specialization entirely.

Regardless of what happens to specialization, we already have Any, mem::discriminant, size_of, and possibly other existing features that weaken/remove parametricity in Rust.

Finally there is already extensive discussion in the Rust around the removal of the Reflect trait and acceptance of specialization about moving away from parametricity, and whether it is actually useful to Rust.

Given this repeated precedent, I think the onus of proof is on people arguing that we should “retain what little we have left” to show why that is valuable.

and I have to agree. The onus really is on supporters of parametricity at this point to show why it's valuable in practice, and after re-skimming all the post-FCP comments on this thread I just don't see any attempt to do so.

Regarding the alternative implementation, how is using Debug/Any/etc any more parametric than the simple static str API? Is the idea that "the semantics of Debug/Any include violating parametricity" and we want to have fewer escape hatches? If so, we've merely rephrased the question as "what's the practical value of making every obviously parametricity-breaking API go through an existing parametricity-breaking trait?"

I don’t see the point of an opaque type that implements Debug. As long as we provide a way to obtain a string (in this case format!("{:?}", foo)), why not just provide that string in the first place?

std::mem::Discriminant is different. It implements Eq and Hash (which makes it somewhat useful) but not e.g. Into<u64>.

Any: 'static sounds problematic. You could hide the &'static str behind some opaque type though, right? It'd achieve the same goal of limiting runtime impact, no?

#![feature(core_intrinsics)]
use core::{fmt, marker::PhantomData, intrinsics };

pub struct TypeName(&'static str);

impl fmt::Debug for TypeName {
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> fmt::Result {
        f.write_str(self.0)
    }
}

pub fn type_name<T: ?Sized>() -> TypeName {
    TypeName(unsafe { intrinsics::type_name::<T>() })
}

In principle, TypeName might hide an enum with a variant for &'static str and threaten adding a variant for FnOnce() -> String, which prevents destructuring it with unsafe.

@Centril

Specialization is in a real sense giving up on "we can have both performance and robustness" in that parametricity is a robustness property that you are trading for performance. It is also anti-modular in its essence

Yes. It is also necessary. Stopping people from doing practically necessary things that are required to get their jobs done because it allows technical violations of theoretical principles is not okay. Rust relies on the ability to "break the glass in case of emergency." We have a compiler that provides memory safety in common cases, but we still need unsafe. We've always provided a mechanism for folks to break rules in an encapsulated, useful way. This is just such an API.

This is just a summary comment. An opinion comment will follow.

The FCP comment can be found here

The current state is that there are 3 concerns blocking type_name being callable from stable Rust.

is-this-the-right-api?

There has been some discussion around alternative APIs. The alternatives suggested are

  • return &'static str (current status)
  • return a wrapper type around &'static str that just offers a Debug impl
  • return a struct that has the name, path to the item and generic parameters as fields

unclear-guarantees-and-underspecified

Calling type_name from different crates on the same type may result in different results. Possible solutions are

  1. this is fine, it's just debugging
  2. use absolute paths, just like symbols do (https://github.com/rust-lang/rfcs/issues/1428#issuecomment-473183917). So we might end up with paths that the user cannot write due to private modules (but the type may have been reexported via another path). Again, fine, it's just debugging.

There is some fear that this might be incoherent if we go with option 1.

premature-loss-of-remaining-parametricity

This is the big one. Everyone seems in agreement that specialization will become stable at some point in the future. The only worry is that specialization may be unsound and thus not become stable.

It is suggested that type_name becoming stable without specialization becoming stable will make the type system lose parametricity without giving us specialization.

I hope the above was an objective summary, if not, I'll happily amend. Now to the personal opinion part:

is-this-the-right-api?

Not a problem imo, we can stabilize nicer APIs later if we find any strong use cases for it

unclear-guarantees-and-underspecified

seems solved by using absolute paths. @Centril you seem to agree, can you resolve that point?

premature-loss-of-remaining-parametricity

I think if we discover that specialization is unsound, we'll find a way to make it be sound, even if that ends up making certain things awkward to implement. I don't think we'll ever end up with a Rust that has no specialization.

Even assuming that specialization never becomes stable, I don't think there's a good reason to block type_name. There are already low-perf variants of type_name. There is precedent with mem::forget to make something that is 100% safe but rather convoluted into an intrinsic and stabilize it or a wrapper to it. I think we should seriously keep parametricity out of this discussion as we're only talking about a way to have a more precise Debug output.

unclear-guarantees-and-underspecified

I do not understand the incoherence here. A random number generator is not undefined behavior. We could hypothetically even deform all strings returned by type_name by some randomness, thereby making comparisons useless, no?

2. The Any bound will remain until such time that the language team consents to otherwise (and this should be explicitly noted).

Does anyone else on the lang team actually share these concerns? It seems like a significant majority of the team has already consented to this by approving this API without any 'static bound.

Nominating for discussion in the next lang team meeting.

@joshtriplett How did that discussion go?

@Centril this is blocked on you.

@joshtriplett Not entirely clear; I think we need to re-discuss in a current meeting.

We never discussed it because we had many more important items before this.

@Centril Literally everyone other than you in the decision making process has approved this FCP. Are you really going to unilaterally hold this up indefinitely?

@sfackler I've been in a position before of being the one voice of dissent, and I know that can be uncomfortable. We operate by consensus, not by voting. If @Centril has concerns here, I'd like to better understand them and talk them through until we're ready to proceed. I understand your frustration, but I also don't ever want to have to worry whether we've proceeded because we're ready or whether we've proceeded because objections have been stifled.

I've also been in that position before, and I do think it's important to take time to recognize where the dissent is coming from and work to resolve it where possible.

I also think, though, that it's reasonable to expect that members with blocking concerns be responsible for trying to make time for discussion and work to resolve issues as quickly as possible. This has been blocked without further discussion for a month now, and I believe that I and others have responded in detail to @Centril's concerns above.

@Centril if you still have objections, I'd be happy to discuss over Zulip/Discord so we can work to reach consensus.

I don’t see the point of an opaque type that implements Debug. As long as we provide a way to obtain a string (in this case format!("{:?}", foo)), why not just provide that string in the first place?

Debug only guarantees that it can be dynamically formatted; it does not give you a handle to a string of 'static lifetime. Thus, an opaque type (either the TypeName or the TypeName<A> forms) has one feature that &'static str does not: It gives us, the implementors of the Rust standard library, the freedom to change the implementation so that dynamically builds the string, rather than keep it stored in the binary.

This may allow more compact representations of this information. E.g. a deep path (lets say it has C components) that exports many types (lets say T types) could delay constructing the full forms until they are actually requested, thus requiring only size O(C+T) in the binary, rather than O(C*T) that would be required with the interface as I understand it today.

Update: (I don't care enough about this particular detail to block stabilizing this feature based upon it; my box remains checked. But I figured I'd chime in with my own two cents saying that I don't think the API is the ideal one; but I also believe that "the perfect is the enemy of the good", a phrase which works in both directions.)

@cramertj There has been three months of time to do that! How many days, weeks, months, or years, should people wait for this API because of a single person's objections?

It is somewhat concerning to me if we introduce the notion of a procedural filibuster into the decision making process.

Because it's a very difficult and important decision ?

I'm honestly not sure how this is a difficult decision. The question is if parametricity is important more than if this api is important. We already established that this API is very much in demand by people and having it would break parametricity. So I guess the only thing that is missing is a conversation about if we need parametricity not if we need this API.

A change to the proposed API here would definitely make it useless for me.

@sfackler I do not think it is fair to compare this to a filibuster.

This particular issue was nominated for discussion 23 days ago (not three months, though I recognize that was when @Centril registered their concerns), and in the time since then, other matters have still taken higher priority (especially ones related to the release happening this week). In addition, some members of the T-lang team most relevant to this conversation have been absent from the most recent meetings, which is I believe one reason why @joshtriplett said we need to re-discuss.


On a separate note, I ask you to try to not get too heated in your comments.

I understand being frustrated by how slow our process is, and by its inefficiencies. The members of the team are frustrated by them too, and that is something we hope to resolve in the future.

@mitsuhiko wrote:

A change to the proposed API here would definitely make it useless for me.

Maybe I misunderstand your earlier sketch: how does returning an opaque type that implements Debug make the function useless for you? It seemed to me that code is just emitting a diagnostic describing a given type, as are most of the proposed use cases for this feature? Do you require the opaque type to implement Display in addition to Debug?

(I do understand how changing the API to require a trait-bound breaks your use case, which maybe is what you are implicitly referring to. But your comment as written is most immediately interpreted as "any change to the proposed API would make it useless".)

This may allow more compact representations of this information. E.g. a deep path (lets say it has C components) that exports many types (lets say T types) could delay constructing the full forms until they are actually requested, thus requiring only size O(C+T) in the binary, rather than O(C*T) that would be required with the interface as I understand it today.

Can you elaborate on this? I don't understand it. I would have assumed calling type_name would result in just the components of a path (wrapper around &[&'static str] or even a ZST:

struct Foo<T>(PhantomData<*const T>);

impl<T> Debug for Foo<T> {
    // compiler magic
}

are you saying that if we put &'static str directly into Foo, we'd not be able to deduplicate the components? so

// O(C+T)
const FOO: &[&'static str] = &["std", "ptr", "null"];
const Bar: &[&'static str] = &["std", "ptr", "null_mut"];
// O(C*T)
const FOO: &'static str = "std::ptr::null";
const Bar: &'static str = "std::ptr::null_mut";

The former reuses the same bits in the binary for "std" and "ptr", while the latter has no deduplication.

Or am I misunderstanding entirely what you are suggesting?

@oli-obk I’m saying that returning a &’static str directly from type_name precludes optimizations such as the one enabled by the representation you outline there

Maybe I misunderstand your earlier sketch: how does returning an opaque type that implements Debug make the function useless for you?

For this case it works but not for the Failure case where the name API returns a &str. I agree if no trait bound is needed it solves outlined debug message info.

@sfackler

There has been three months of time to do that! How many days, weeks, months, or years, should people wait for this API because of a single person's objections?

It is somewhat concerning to me if we introduce the notion of a procedural filibuster into the decision > making process.

I guess I stated my position above unclearly, because I agree with you. I'd like to work with @Centril and whoever else to get this resolved as quickly as possible, as I personally find it to be a very minor, useful, and unobjectionable addition to the language.

@pnkfelix

I’m saying that returning a &’static str directly from type_name precludes optimizations such as the one enabled by the representation you outline there

Introducing type_name -> &'static str today doesn't preclude adding another API like the one you suggest at a later date, once the dust has settled and we know what we want out of it (in terms of things like path canonicalization, etc.). We could add a type_path -> &'static [&'static str] function, but that isn't what's being asked for or proposed in this thread, and I think that after this amount of time sitting on this proposal it'd be a shame to toss it aside for something that hasn't been asked for nor discussed previously.

For this case it works but not for the Failure case where the name API returns a &str. I agree if no trait bound is needed it solves outlined debug message info.

Are you talking about https://docs.rs/failure/0.1.5/failure/struct.Error.html#method.name ? The docs suggest that it's perfectly reasonable to return None for the name. The regular Debug and Display printing code of failure errors would be unaffected.

I’m saying that returning a &’static str directly from type_name precludes optimizations such as the one enabled by the representation you outline there

As @cramertj repeated and I suggested in https://github.com/rust-lang/rfcs/issues/1428#issuecomment-473372559, this is the trivial API that users will look for and use. The motivation for more complex alternatives does not seem strong enough for me to block this use case.

Additionally: If we at some point make type_name const fn (which seems like a logical thing to do considering the constness of size_of and such), and make the Debug impl on the hypothetical wrapper type impl const Debug (as per https://github.com/rust-lang/rfcs/pull/2632), users can actually write the fn type_name<T>() -> &'static str in user space.

Thus, I suggest we implement the type_name function as suggested originally with a &'static str return type, and if we ever end up needing a better API, the type_name function can be rewritten in pure Rust without needing an intrinsic (since it calls the more complex structured_type_name internally and generates the required string that way).

// user space `type_name`, assuming `structured_type_name` exists
const fn type_name<T>() -> &'static str {
    // this uses the fact that in a const fn, any other const fn call is
    // promotable. Thus, assuming (I am) the formatting machinery can become
    // const evaluable, the code below will get promoted to an anyonymous static
    &format!("{:?}", structured_type_name::<T>())
}

All of the above is modulo the parametricity discussion, which I don't feel qualified to talk about beyond the practical implementation considerations.

@oli-obk @cramertj all I was doing was explaining why the existence of one API implies a cost that another API does not have, since someone said they didn’t see why one might want the opaque type. (Expanding the API later with new methods does not resolve the problem of the static size cost of supporting the API as written.)

But i am just trying to explain why one might favor the alternative API (a revision which I thought @sfackler had previous said they would be willing to consider, but my memory here may be wrong)

I certainly don’t care enough about this detail to block the proposal; my box remains checked.

I've discussed this with @oli-obk and @joshtriplett and will leave a comment outlining a more nuanced approach to type_name. Hopefully it should be positive news for those who want to see this stabilized. Meanwhile I ask everyone to please not put more pressure time-wise. Remember that some of us have many responsibilities and do this work completely unpaid and yet still don't find the time.

// O(C+T)
const FOO: &[&'static str] = &["std", "ptr", "null"];

Note that in addition to the bytes that make up the strings’ contents, this representation also uses four (pointer, length) pairs that occupy 16 bytes each (on a 64-bit plaftorm). It’s not clear to me that this is an optimization at all.

I think TypeId should provide a useful Debug implementation at some point. Perhaps there is room for two APIs here: one to get a type name as a compile-time constant &'static str, and another which requires the 'static bound but allows type names to be obtained for types which can only be identified at runtime?

@Diggsey

... allows type names to be obtained for types which can only be identified at runtime ...

Pardon my ignorance, but in which circumstances are types unknown until runtime? In the case of trait objects, isn't the trait object itself the type? I haven't used Any yet, but it looks like get_typeid is already part of the interface.

IMO a Debug impl on the TypeId that gives a qualified path would be pretty nice. However, it seems to me that there can be conflicting absolute paths, and users would have to be wary of this when attempting any dynamic type hackery. In particular, one should never use the path string as a unique identifier, hash key, or as a test for Java-style downcasting of Traits.

EDIT No, wait I suppose you can't have two dependencies with the same crate name..

We already have impl Debug for TypeId. If these printed anything human readable, and if a 'static bounds suffices, then @Centril's suggestion looks like

dbg!(TypeId::of::<Foo>());
dbg!(foo.get_type_id()); // provided foo: Any or foo = dyn Any + ..

I suppose TypeName with a 'static bound violates parametricity less than TypeId though, as TypeId: Eq, so worth encouraging type name. I've no opinion on whether the 'static bound suffices, but it seemingly covers most use cases mentioned in this thread.

Another option might be item and module level #[require_debug_everywhere] attribute along with T: ?Debug.

@rfcbot concern no-rfc

My take here:

I think we already decided when it comes to parametricity and I stand by that decision. I think we should (ultimately) stabilize some variant of type_name, it is ridiculously useful.

However, when I checked my box, I did not fully realize that we were somehow fcp'ing to stabilize "whatever the heck is presently implemented". I'm not very keen on that, I think we should have some kind of actual RFC that lays out the design, and this should go through the usual stages (perhaps in a faster than usual sort of way, given that the impl work is largely done).

At minimum I would hope to see a "stabilization report" that describes the behavior, shows test cases, etc, as we expect for ordinary features.

You might say "oh, but the existing docs should serve as that kind of documentation", but when I look at the docs for type_name I just see this:

Gets a static string slice containing the name of a type.

This doesn't quite seem sufficient to me.

EDIT: I may well be confused about what is going on here. I haven't read every comment in this thread. But the bottom line is that I have no idea what it even means to "fcp merge" an issue on the rfcs repo, so something, so clearly this isn't the ordinary process.

@rfcbot resolve no-rfc

So, I'm resolving my concern, because (a) I know I won't be able to keep up with this thread and because (b) @cramertj has been making a good case that an FCP concern isn't the best way to get my point across. Specifically, he felt like it was disrespectful of me to the time and effort that @sfackler has put in to kind of hold things up on a technicality. I respect that, and I agree, I know that there's been a lot of effort here.

At the same time, I think this is really not following the usual process and I find it all a bit confusing. I think it's pretty reasonable to expect (as I wrote) at least some kind of write-up of the documented behavior. I realize that if one reads the thread, one can extract the guarantees, but there is great value in seeing it summarized, and everybody agreeing to that summary (otherwise people tend to come up with different views of what the consensus of the thread actually was).

(Anyway, as I wrote above, I am in favor of stabilizing some form of type-name intrinsic, though I'm somewhat agnostic as to its precise form and signature.)

@nikomatsakis Yeah, this FCP is a bit weird in that it's happening on an issue in the RFCs repo as opposed to an RFC itself or tracking issue in rust-lang/rust. My understanding of the proposed API signature and location is the addition of this function to core::any:

/// Returns the name of a type as a string.
///
/// # Note
///
/// This is intended for diagnostic use. The exact contents or format of the string are
/// not specified, other than being a valid name for the type in source code. For example,
/// `type_name::<Option<String>>()` could return the string `"Option<String>"` or the
/// string `"core::option::Option<alloc::string::String>"`, but never the string
/// `"foobar"`.
pub fn type_name<T>() -> &'static str
where
    T: ?Sized,
{}

Notably, there are no Any/'static/Reflect/etc bounds on T.

@sfackler that's roughly what I had in mind, yes, though fwiw, I would not have expected this:

other than being a valid name for the type in source code

I think that's stronger than we might want to guarantee. What if, at some point, we can't generate a valid name for the type without allocation or something? I would have thought we'd make no guarantee at all, and that -- for some future types -- we might return strings like "<type_name invalid on this type>" or something. (Admittedly, I don't have a concrete scenario where this would happen, but I don't see a reason to guarantee much of anything beyond a "best effort" to make this useful to people.)

But probably this has been discussed on the thread already =)

What if, at some point, we can't generate a valid name for the type without allocation or something?

I mean, this is running at compile time, right? The compiler knows the exact type T and can build whatever string it wants.

In any case though, it seems totally fine to weaken the docs to just "some best-effort name/description of the type".

@sfackler

I mean, this is running at compile time, right? The compiler knows the exact type T and can build whatever string it wants.

Yeah, I know, probably unlikely. I was imagining for example that we might want to move to a setup where the compiler is able to erase some of the type parameters and only partially monomorphize. It's true that in such a scenario we might include the static string as part of the vtable or dynamic info that we pass in, or avoid doing that if type_name is called (which is probably what we would do for specialization).

I guess it's also the case that "valid name" is sort of ill-defined -- like, valid in what sense? Is it guaranteed to be accepted by the rust compiler? We clearly don't know lifetime info, so that can't quite be it.

What if a type's string representation is excessively large? Should it be truncated at some point, and how?

In any case it seems like everybody's happy to weaken the guarantees ;)

Oh interesting. Definitely seems best to go with the reasonable best-effort language then.

I still plan to write up a longer comment and I will have time to do so now that the release has shipped... for now, the best-effort language is important as a step towards consensus. For example:

/// # Note
///
/// The function is intended for diagnostic use. The exact contents or format of the string
/// are not specified and may change at any moment. A best effort attempt is made to
/// give sensible and useful outputs.
///
/// FIXME: give examples of sensible outputs and some poorer ones for e.g. closures...

@sfackler core::any is sensible 👍

I would say it’s best to think about “guarantees” about this as a value that more or less looks like a demangled value you would get out of dwarf debug info. We don’t have strong guarantees there either.

I would say it’s best to think about “guarantees” about this as a value that more or less looks like a demangled value you would get out of dwarf debug info.

You could vaguely suggest that this is sorta what it does (i.e. with examples), but using the language of guarantees that is not something we should do in my view as that is a future compatibility hazard.

We don’t have strong guarantees there either.

To be exact, we have, to my knowledge, zero guarantees about name mangling unless specific attributes have been used.

Good to see the details of the concrete proposed change here.

@rfcbot reviewed

Regarding stabilization reports

First, let me agree with @nikomatsakis that there should have been an RFC and in particular:

However, when I checked my box, I did not fully realize that we were somehow fcp'ing to stabilize "whatever the heck is presently implemented". I'm not very keen on that, I think we should have some kind of actual RFC that lays out the design, and this should go through the usual stages (perhaps in a faster than usual sort of way, given that the impl work is largely done).

At minimum I would hope to see a "stabilization report" that describes the behavior, shows test cases, etc, as we expect for ordinary features.

I don't think it is disrespectful in the least. It is what the language team should __always__ require then a stabilization happens (and this is mostly a T-Lang stabilization because type_name is mostly a matter of the core semantics that can only be provided by the compiler). I agree in particular that the stabilization proposal was not well specified since:

  • no tests were provided
  • the module was not specified
  • the guarantees were not specified

Assuming the changes I will ask for are agreed to and implemented, I'd like to see such a report provided either in the stabilization PR itself (my preference, as it is "cleaner") or in this issue. A template on which such a report can be based can be found in https://github.com/rust-lang/rust/pull/57175#issuecomment-450593735. (Some parts are not relevant, e.g. "Divergences from RFC" is not applicable since no RFC was written).

Regarding concerns

To summarize my concerns (some of them I've noted, some not):

  1. The current implementation has bugs as noted by @dtolnay in https://github.com/rust-lang/rfcs/issues/1428#issuecomment-450535964.

    In particular, the behavior violates coherence ("This property states that every different valid typing derivation of a program leads to a resulting program that has the same dynamic semantics") but not in relation to traits.

    Moreover, the behavior also violates referential transparency which is something that is reasonable to expect from a function like this and which will be important if this is to be extended into const fn.

    With regards to the bug, @oli-obk says we can fix by always using absolute paths, and I agree that that is the correct solution. However, this fix should happen before we move into FCP.

  2. The current implementation risks stagnating language evolution.

    The primary reason I wanted the TypeName indirection was to leverage the intuition that Debug outputs cannot be relied upon. This is primarily a concern about language evolution. I find it very risky to freeze the outputs of type_name. Given people's reliance on things we've explicitly declared UB it seems to me easy that people would rely on this (which is never UB). Oliver and Josh have convinced me that we can provide good documentation here to somewhat resolve that concern and it seems that we have since moved in this direction.

    Another concern, as discussed in https://github.com/rust-lang/rfcs/issues/1428#issuecomment-481285316, and in other comments, has been that by returning &'static str, we say that the type name must be known at compile time. If one considers a type such as [u8; dyn N] where N: usize is only known at run-time, then you do not know the full type name until run-time. However, Oliver convinced me that we can employ the same type erasure that we use for lifetimes here. The output is then lossy but that is serviceable for debugging purposes.

    To fully assuage my concerns, as suggested to me by @cramertj, I would like to see the return type changed to impl Debug. Notably, this does not preclude changing the return type to &'static str but provides room to consider optimizations and makes it even clearer that this is for debugging purposes only.

  3. There's the issue of parametricity. I think this is a valuable property for code maintainability. The reason why I wanted T: 'static is that I think people would be unwilling to use it in many cases to get to do non-parametric reasoning in code to match on type_name (especially if it is cheap) and achieve specialization that way. Some say that specialization will happen no matter what, but it has had problems before.

    One problem with non-parametric code is that, as @glaebhoerl has noted in the past, it devaluates the notion of types entirely. From the perspective of a function's caller, I think it is hugely surprising that you instantiate a polymorphic function and that it acts specially depending on what the instantiation was. While one can argue that you can do more evil things such as rm -rf, I argue that a) this is not possible for const fns, b) two wrongs don't make a right; the argument that there are other ways to do bad things is not a good argument for introducing more ways to do bad things.

    Also... Non-parametric reasoning in the form of instanceof (sometimes referred to as "type-case") often leads to less maintainable code as a hack you use when you have painted yourself into a corner. For this reason, using instanceof basically led to your lab assignments getting rejected at my university.

    Josh has suggested using a lint, triggering on match type_name::<T>() { ... } and similar, to discourage such non-parametric reasoning. I think writing such a lint is difficult, but it is an interesting direction of travel. For now, returning impl Debug would go a long way towards assuaging my concerns with respect to code maintainability and make me OK with removing the 'static bound.

  4. Higher ranked types and soundness

    @cramertj notes that losing parametricity for types cannot cause unsoundness. It is true that it cannot with the current set of language features.

    However, do note that lifetime parametricity is something that we do have and which is important for soundness, especially with respect to the lifetime-based higher ranked types that we do have (e.g. for<'a> fn(&'a u8) -> Foo).

    Giving up on parametricity for type variables which are not T: 'static would, as far as I know, also give up on the ability to have type-based higher ranked types (e.g. for<'a, T: 'a> fn(&'a T) -> Foo and for<'a, T: 'a> dyn Fn(&'a T) -> Foo). While there are current impediments to such a feature in the form of existing non-parametric functions such as size_of, those can be embedded into the function pointers and vtables themselves. They are nowhere near as leaky as type_name is.

    Aside: Type erasure is also an optimization technique used by compilers to achieve space savings. It stands to reason that by giving up parametricity, you are making it harder for a Rust compiler (e.g. under some hypothetical optimization flag) to optimize based on this knowledge. Instead, an analysis must be performed to check whether the function is non-parametric or not.

    However, as specialization nears completion and hopefully moves toward stabilization, parametricity for types is broken in any case and so higher ranked types become unworkable.

    Another avenue for regaining higher ranked types may instead be to introduce a new ?Concrete unbound. This has been discussed previously in https://internals.rust-lang.org/t/idea-type-erasure-in-rust-through-parametricity/5902. As GolDDranks notes, and I agree, higher ranked types are an expressivity enabler.

An action plan

@rfcbot resolve is-this-the-right-api?
@rfcbot resolve premature-loss-of-remaining-parametricity
@rfcbot resolve unclear-guarantees-and-underspecified

@rfcbot concern action-plan

Having laid out my concerns, I would like to propose the following action plan:

  • Fix the bug wrt. absolute paths.
  • Unstably add type_name to core::any (+ reexport) with the signature fn type_name<T: ?Sized>() -> impl Debug.
  • Write up a stabilization report in a PR including with tests based on https://github.com/rust-lang/rust/pull/57175#issuecomment-450593735 as a rough template.
  • Merge the stabilization PR.

Fix the bug wrt. absolute paths.

Sure - is @oli-obk the right person to ask about what to change here? Just to make sure I understand, is the issue that the current type_name implementation does something different and worse than what e.g. debuginfo generation does, or that we also need to fix this there?

with the signature fn type_name() -> impl Debug.

Based on my experience working on the standard library for the last couple years, I am very confident that returning impl Debug will do exactly nothing to prevent people from misusing this API compared to returning &'static str. It's just going to make it more annoying to use, and possibly pose a significant impediment to some specific use cases of this function as @mitsuhiko has brought up.

Why do people know that Debug formatting is not stable? Because we document that to be the case. Does that stop people from performing exact matching against Debug output? No; every time we tweak the Debug output of some type, Crater comes back with ~5 or so regressions in the ecosystem mostly from crates that assert the exact format in tests. We ping the authors, they fix their stuff, and we move on.

We are going to document that the formatting of the thing returned by type_name is not stable regardless of if it's impl Debug or &'static str. I guarantee you that either way, every time we change the implementation of type_name, Crater will come back with a regression from someone that was asserting the exact output of type_name in a test. We'll ping the authors, they'll fix their stuff, and we'll move on.

Write up a stabilization report in a PR including with tests based on rust-lang/rust#57175 (comment) as a rough template.

This seems really overwrought, but sure I guess.

Sure - is @oli-obk the right person to ask about what to change here?

Seems like a good start; maybe @eddyb otherwise?

Just to make sure I understand, is the issue that the current type_name implementation does something different and worse than what e.g. debuginfo generation does, or that we also need to fix this there?

If the same issue exists in debuginfo then fixing it there as well doesn't seem like a bad idea. I think having this incoherence in-language is however worse because a polymorphic function instantiated with the same type arguments yield different results based on where it is called. I'm my view this is not just about poor implementation quality. But also, as aforementioned, if and when we consider extending type_name to const fn, then it means that two calls of type_name::<MyType>() may yield different results. I think that violates fundamental properties const fns should have. The reason for doing it now is that otherwise I think it will never get done.

It's just going to make it more annoying to use, and possibly pose a significant impediment to some specific use cases of this function as @mitsuhiko has brought up.

Let's test that hypothesis. As I said, nothing about the return type technically prevents a change to &'static str later on. It may not cover all use cases, but I think it covers most. Moreover, returning it discourages matching on that type_name and that is something I think we should do.

Why do people know that Debug formatting is not stable? Because we document that to be the case.
[...]
I guarantee you that either way, every time we change the implementation of type_name, Crater will come back with a regression from someone that was asserting the exact output of type_name in a test. We'll ping the authors, they'll fix their stuff, and we'll move on.

I don't doubt this will happen. However, we have build up a well-spread knowledge around the fact that Debug outputs are not stable. By using ìmpl Debug, we piggyback on that collective knowledge. In my view that makes a difference in terms of how many regressions will occur.

This seems really overwrought, [...]

This may be a difference in how the libs and lang teams operate. As of late, these reports have become standard in language team stabilizations.

[...] but sure I guess.

Thanks!

To fully assuage my concerns, as suggested to me by @cramertj, I would like to see the return type changed to impl Debug. Notably, this does not preclude changing the return type to &'static str but provides room to consider optimizations and makes it even clearer that this is for debugging purposes only.

I like this. It's the best of both worlds. We get a quick stabilization of type_name and can then have a good look at the &'static str use cases and move to &'static str if there's a good reason to.

Just to make sure I understand, is the issue that the current type_name implementation does something different and worse than what e.g. debuginfo generation does, or that we also need to fix this there?

yea, the type_name intrinsic just uses Ty::to_string. Which might actually be doing the right thing nowadays since @eddyb's awesome pretty printing refactor. Can you check if that point is already resolved? If so, add some tests so we don't regress that.

There is already a user (@mitsuhiko) commenting in this thread who wants to use type_name in an important ecosystem library (failure) but needs to conform to an interface that expects &'static str. We do not need to find use cases, they are already present here.

The return type of this function does not have lang implications; this is a libs team question.

@oli-obk dtolnay's example still behaves the same on nightly today it looks like.

if and when we consider extending type_name to const fn, then it means that two calls of type_name::<MyType>() may yield different results.

But we aren't considering that here. If and when we want to make type_name a const fn, we can deal with that issue.

If the fix here is straightforward, then we can fold it into the stabilization, but it does not seem to me to be a blocker. It's probably desirable to have the same names in all contexts, but every use case I'm aware of wouldn't see that as a hard blocker.

The return type of this function does not have lang implications; this is a libs team question.

Can someone with github permissions please remove the lang label in that case?

We do not need to find use cases, they are already present here.

I misunderstood that use case. I thought it was just expensive to use a String, but in fact the use case was that the public API already expected a &'static str.

If and when we want to make type_name a const fn, we can deal with that issue.

Yes, that's a problem for the future, and since we are explicitly stating that the exact output is unspecified, we can resolve that when the time comes.

every use case I'm aware of wouldn't see that as a hard blocker.

I think having this incoherence in-language is however worse because a polymorphic function instantiated with the same type arguments yield different results based on where it is called.

Note that such a polymorphic function can already yield different results based on its call site by using the address of a local (debug printing a local's address is fine). Additionally I feel like having differences in debug printing inside a no_std crate and outside a no_std crate is ok. I don't think we want to end up printing paths that point into liballoc even though the user printed a libstd type from their perspective. Imo, the current best effort method gives the most expected representation from a user perspective.

We do not need to find use cases, they are already present here.

I understand that not every use case will be satisfied by exposing only impl Debug. This restriction exists however for good reasons. fn fail can continue to return None as the default.

The return type of this function does not have lang implications; this is a libs team question.

It does have lang implications and I disagree that it is solely a libs team question. As Felix has shown, returning &'static str means that you can always materialize a type name at compile time, and moreover it has implications wrt. how much we encourage and discourage non-parametric reasoning.

Yes, that's a problem for the future, and since we are explicitly stating that the exact output is unspecified, we can resolve that when the time comes.

In my view it's not just about const fn; it's also about implementation quality. I think the current one has bugs and I'm not going to agree to stabilizing something with known bugs.

Note that such a polymorphic function can already yield different results based on its call site by using the address of a local (debug printing a local's address is fine).

A const fn?

Additionally I feel like having differences in debug printing inside a no_std crate and outside a no_std crate is ok. I don't think we want to end up printing paths that point into liballoc even though the user printed a libstd type from their perspective. Imo, the current best effort method gives the most expected representation from a user perspective.

It would be one thing if std:: was uniformly used in a single binary (including statics and consts). I would be fine with that. But this is not the case here. Moreover, in my view, such differences are definitely not fine with a const fn.

As Felix has shown, returning &'static str means that you can always materialize a type name at compile time,

The compiler already does this, both for diagnostics directly emitted by the compiler and inserted in debuginfo. Zero people are asking for anything more precise than that output.

I think the current one has bugs and I'm not going to agree to stabilizing something with known bugs.

I do not agree that this is a bug - the different outputs are all valid names for the type. You noted above that "the reason for doing it now is that otherwise I think it will never get done." If no one cares about this enough to invest time in solving it, then why is it so important to fix that you want to prevent people from using this functionality entirely?

Moreover, in my view, such differences are definitely not fine with a const fn.

Good thing this is not a const fn then.

There are a few ways forward here, I'm going to list them and list the problems mentioned with them (opinion post will follow, so please call me out if I add an opinion to thist post).

  1. use &'static str
  2. use impl Debug as the return type
  3. 2 but maybe move to &'static str in the future
  4. 1 but const fn
  5. 2 but const fn
  6. 3 but const fn
  7. 5 but impl Debug implies an impl const Debug

Problems mentioned with 1.

  • varying output is considered a bug (@Centril can you elaborate on this in the not const fn case?)

Problems mentioned with 2.

  • useless for existing use cases
  • all problems from 1.

Problems mentioned with 3.

  • all problems from 2.

Problems mentioned with 4.

  • const fn called with same args must produce same output

Problems mentioned with 5.

  • all problems from 4.
  • useless for existing use cases

Problems mentioned with 6.

  • all problems from 5.

Problems with 7.

  • all problems from 4.

So... I believe that if we are talking about const fn in the future at all, we can just go with the &'static str return type. My reasoning is as follows:

if we choose 5 (const fn + impl Debug) I see no reason not to go with 7 (also have impl const Debug on the return type). If we go with 7, the user can implement their own fn foo<T>() -> &'static str on top of type_name at some point.

const fn foo<T>() -> &'static str {
    &format!("{}", type_name::<T>())
}

So if we choose to keep the door open for const fn type_name in the future, we're basically just blocking the &'static str return type on sufficiently advanced const eval. Also, if we want a more structured return type in the future, we can just implement type_name on top of that at that point.

So, if we are talking const fn in the future, then I see absolutely no reason not to go with &'static str right now. Additionally we can and must block the const fn change on using absolute paths in order to make the output with the same input deterministic.

It would be one thing if std:: was uniformly used in a single binary (including statics and consts). I would be fine with that. But this is not the case here.

It is used uniformly. What you see above is not two calls to type_name within a single crate, but one call in a no_std crate and one in an std crate. Even if these are linked together at some point, that seems like an independent fact. Why does linking some code together make the way they are compiled individually special?

Additionally, calling a random library crate may give you arbitrary output. That this output depends on the type_name intrinsic is an implementation detail. Two calls to type_name within the same crate will always yield the same output.

That said, we can easily "fix" the output to use absolute paths, but imo the output will be less helpful to users than if we keep the current output. This is meant for debugging, and debugging is something humans do. Let's make it easier for humans to work with.

The compiler already does this, both for diagnostics directly emitted by the compiler and inserted in debuginfo.

The context here was that it was claimed that the return type is a T-Libs question. That the compiler does this for diagnostics and for debuginfo is irrelevant. These are not part of the language's specification but with type_name() -> &'static str that changes.

  • varying output is considered a bug (@Centril can you elaborate on this in the not const fn case?)

For const fn I think it's more than a bug; I consider it a soundness issue as well.

That a function as simple as type_name have different results, based on what crate it is in, is in my view hugely surprising. Even if it isn't a const fn, it should act as one.

So... I believe that if we are talking about const fn in the future at all, we can just go with the &'static str return type.

This discounts the other reasons for impl Debug:

  • piggybacking on Debug to more clearly note the for-debugging-only nature
  • discouraging matching on the &'static str

If we go with 7, the user can implement their own fn foo<T>() -> &'static str on top of type_name at some point.

Emphasis on at some point. The format! and format_args! machinery use both trait objects and function pointers baked inside structs. It seems to me that this requires an effect system that is more deeply baked into the compiler than https://github.com/rust-lang/rfcs/pull/2632 comes close to. Thus, I think that the point you are referring to is far off and may not even happen ever (I hope it will). And even if we believe it is likely to happen at some point, this still requires type_name to be const fn. I also don't see why "at some point" is a good justification for relaxing things now. It is a good justification for at that point. I don't agree with similar arguments re. specialization that have been made here saying that "stable specialization will happen at some point".

Why does linking some code together make the way they are compiled individually special?

The inverse question should be answered in my view -- why is the compilation model of linking together independent crates relevant?

That said, we can easily "fix" the output to use absolute paths, but imo the output will be less helpful to users than if we keep the current output. This is meant for debugging, and debugging is something humans do. Let's make it easier for humans to work with.

I disagree that it makes things easier for humans or that it is more useful for debugging. If you are e.g. logging errors then it seems unfortunate that some errors should mention core:: and some std:: and that you should have to implement special logic to de-duplicate things when that difference occurs. Moreover, if you want to act upon the output of type_name, I think it's more useful to get the actual location something is defined in rather than getting re-exports.

The context here was that it was claimed that the return type is a T-Libs question. That the compiler does this for diagnostics and for debuginfo is irrelevant. These are not part of the language's specification but with type_name() -> &'static str that changes.

I don't see how this is true. The intention is this to give access to the debug information available to the compiler, not for this to be specified as part of the language with the exact format.

This discounts the other reasons for impl Debug:

  • piggybacking on Debug to more clearly note the for-debugging-only nature
  • discouraging matching on the &'static str

What I'm saying is that users will end up being able to do these two things even with impl Debug. The docs are discouraging this already. Putting stumbling stones in the way of usability will only enourage hacky workarounds.

For const fn I think it's more than a bug; I consider it a soundness issue as well.

I fully agree, my question was about without type_name being const fn. I explicitly created separate discussion points for with const fn and without.

That a function as simple as type_name have different results, based on what crate it is in, is in my view hugely surprising. Even if it isn't a const fn, it should act as one.

Any sort of debugging code will have surprising results. Usage of the file! macro will give you different results depending on which computer you are compiling your crate on. One may argue that file! is a macro while type_name isn't, but we could implement file! as a function/intrinsic just fine.

The format! and format_args! machinery use both trait objects and function pointers baked inside structs. It seems to me that this requires an effect system that is more deeply baked into the compiler than #2632 comes close to. Thus, I think that the point you are referring to is far off and may not even happen ever (I hope it will).

How long it takes for something to become stable is not relevant to my argument. We could use impl ToString as the return type and the argument would work just as well.

why is the compilation model of linking together independent crates relevant?

If you compile a crate on one computer as a cdylib and have it export a type name, and use it from a different crate compiled on another computer with a possibly different rustc, you don't have any guarantees for equality of the result of printing the same type. But through the cdylib (or e.g. by exchanging info via network) we may end up in a situation where both stringified versions of a type end up in a single runtime program.

If you are e.g. logging errors then it seems unfortunate that some errors should mention core:: and some std:: and that you should have to implement special logic to de-duplicate things when that difference occurs.

Deduplication is only useful if you are automatically processing things.

Moreover, if you want to act upon the output of type_name, I think it's more useful to get the actual location something is defined in rather than getting re-exports.

I thought the argument for deterministic type_name output was that nothing should act upon the result of type_name. Or are you talking about user action, and not codified action? Even so, is it really helpful for the user to get some_crate::Foo if they are using other_crate::bar::Bar which is a reexport of some_crate::Foo, but their dependency list doesn't have some_crate in it.

I'm ok with changing the output to absolute paths, especially since we're not guaranteeing output stability in any way. I just find it surprising to have a different output than what diagnostic messages are giving us. With diagnostics we are trying to give output that is close to what the user expects at the site where they used the type. type_name just gives this ability to users.

Has anyone brought up the possibility of Any using this to provide some debuggability?
Unless we don't want to encumber Any users with one string per type.

I still feel strongly that -> impl Debug would give the language more freedom and discourage, in a good way, against matching on the type_name::<T>() as a substitute for specialization. That said, now that the PR for deterministic output has landed, in the spirit of compromise:

@rfcbot resolve action-plan

(I would still like to see a report in brief on the stabilization PR however before it is merged)

:bell: This is now entering its final comment period, as per the review above. :bell:

From a brief discussion on Discord with @oli-obk , it should actually be trivial to make type_name a const fn under a separate unstable feature(const_type_name).

Some comments mention potential issues with making type_name a const fn, and I think it might be useful to be able to try those examples out on nightly at least.

The final comment period, with a disposition to merge, as per the review above, is now complete.

As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed.

The RFC will be merged soon.

s-soon?

Well there's nothing to merge in this repo; it's not a PR...

It looks like people largely agreed to make type_name() -> &'static str a _non_ const function for now.

We just have to write up the actual RFC for it. With an actual RFC, that discussion can be pointed at this issue as a "pre-discussion" and should be able to go through quicker than a normal RFC.

This does not require an RFC. I just need to get around to finishing up https://github.com/rust-lang/rust/pull/60066

This has now been stabilized as core::any::type_name with &'static str return type

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rust-highfive picture rust-highfive  ·  75Comments

nrc picture nrc  ·  96Comments

pnkfelix picture pnkfelix  ·  160Comments

rust-highfive picture rust-highfive  ·  59Comments

Ekleog picture Ekleog  ·  204Comments