Howto `str_or_stringify!("1") == "1" && str_or_stringify!(1) == "1"`?

I need to get the str representation of a literal. This shall be either the str itself, or if it's something else, like a number, that stringified. Both of these variants implement that logic. (Off topic: I actually have a robust check for raw str, not shown here.)

This can only ever return a str, due to the check. But the compiler doesn't realize that. If fed a number, it can not go into the $id returning branch. Yet, then it complains about incompatible types.

macro_rules! str_or_stringify {
    (1 $id:literal) => {{
        use std::any::Any;
        if std::any::TypeId::of::<str>() == $id.type_id() {
            $id
        } else {
            stringify!($id)
        }
    }};
    (2 $id:literal) => {
        match stringify!($id) {
            str if str.as_bytes()[0] == '"' as u8 => $id,
            str => str
        }
    };
}

How can I trick the compiler into accepting this logically sound code? Or, how can I get my desired outcome?

(I currently have a clunky work around, which slices off the quotes. But it can't deal with embedded \ or \n, which would require copying, i.e. not a str any more. Plus slicing is inexplicably not const, so I emulate it with [u8].split_at and from_utf8.)

There's probably a simpler way to implement this, but you can use a trick called autoref specialisation.

First, let's introduce a helper trait that'll let us get the string value of something.

pub trait StringLiteral {
    fn string_literal(&self) -> &'static str;
}

Next, I've got a generic struct that holds both the value and the stringify!()'d version.

pub struct SpecialisationHelper<T> {
    value: T,
    stringified: &'static str,
}

impl<T> SpecialisationHelper<T> {
    fn new(value: T, stringified: &'static str) -> Self {
        SpecialisationHelper { value, stringified }
    }
}

Now here is where the autoref specialisation comes in. We define a general implementation on &SpecialisationHelper<T> which returns self.stringified and add an implementation on SpecialisationHelper<&'static str> which will return the value directly.

impl StringLiteral for SpecialisationHelper<&'static str> {
    fn string_literal(&self) -> &'static str {
        self.value
    }
}

impl<T> StringLiteral for &SpecialisationHelper<T> {
    fn string_literal(&self) -> &'static str {
        self.stringified
    }
}

From here, all that's left is to wrap it up in a nice macro and try it out.

macro_rules! str_or_stringify {
    ($id:literal) => {
        (&SpecialisationHelper::new($id, stringify!($id))).string_literal()
    };
}

fn main() {
    dbg!(str_or_stringify!("Hello, World!"));
    dbg!(str_or_stringify!(42));
    dbg!(str_or_stringify!(3.14_e4));
}
   Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 1.17s
     Running `target/debug/playground`
[src/main.rs:35] str_or_stringify!("Hello, World!") = "Hello, World!"
[src/main.rs:36] str_or_stringify!(42) = "42"
[src/main.rs:37] str_or_stringify!(3.14_e4) = "3.14_e4"

(playground)

This implementation won't work in const contexts because it uses trait methods, but your first version uses TypeId::of() which is also non-const, so maybe that's okay for your situation.

1 Like

Thank you, Michael – amazing effort!
The TypeId::of() was an oversight, while experimenting. Another case of where const trait methods are missing. How could a type id conceptually not be const (at least within the same compiler version?)
I do want this to be const, to be able to feed it to constcat::concat!().

Edit: I wonder if just your helper wouldn't be enough. Instead of new, directly an as_str method. I'll try that.

I believe it’s desired for it to be const, but the feature is currently unsound (not that I understand the details of that issue).

No need for a macro.

fn str_or_stringify(x: impl ToString) -> String {
    x.to_string()
}

fn main() {
    dbg!(str_or_stringify(1));
    dbg!(str_or_stringify("1"));
}

Yes, of course. Alas String is neither static nor const. Something must own it and lend it wisely. Not easy in a complex macro (of which the above is just a small part.)

Nor do I understand it in depth. But it seems to be more about what the TypeId should be in similar cases, rather than whether the trait method returning it can be const.

I believe this isn't possible in const without proc macros, at the moment. But there's no need to write your own proc macro — a lesser known feature of the paste! macro is the ability to use string literals in the [<ident>] paste context. So

macro_rules! weak_stringify {
    ($tok:tt) => { ::paste::paste! {
        stringify!([< $tok >])
    }};
}

fn main() {
    dbg!(weak_stringify!(1)); // "1"
    dbg!(weak_stringify!("1")); // "1"
}

... but this only works for "identifier like" strings. For other strings, you'll need to write that (simple, no need for syn) proc macro yourself.

2 Likes

I have given up for now. Since so many of str's method aren't const, I've written a little helper function, which does both const analysis and slicing off start and end. If the input is a str, alas that implicitly makes it a raw string.

I've used it in RFC RustDOT: graph! { A -- A -- B; A -- C; } · Issue #613 · petgraph/petgraph · GitHub, which stringifies all these 5 different IDs:

let g /* UnGraph<&'static str, ()> */ = graph! {
    1 -- { 2 3 } -- A -- "A" -- Bb -- r#Bb; // loops on A and Bb
};

With constcat you can optionally wrap each ID's weight aka label in prefix and postfix:

let g /* UnGraph<&'static str, ()> */ = graph! {
    node [label="[" id "]"];
    1 -- { 2 3 } -- A -- "A" -- Bb -- r#Bb; // loops on A and Bb
};

A different question but related: As nodes can appear multiple time in a graph, my macro has a map of them keyed by stringified ID. As the macro can't keep track of when is each node's 1st occurrence, each time I do map.entry($id).or_insert_with(|| graph.add_node(_graph!(helper for default weight for $id))). This works great.

The following, without the weight line, would just assign "a" as a weight the 1st time that node is seen. However I can assign any expr as a weight, and type inference will pick it up. E.g.

graph! {
    a [ weight = (1, 2.5, false) ];
    a -- a; // loop on a
}

Then the loop line kills it. Now "a" already exists, so entry will find and return it and or_insert_with never gets called. However the compiler sees it. And the return type str doesn't match the tuple, so it fails. So far I found that as a work-around I can change the node-weight default (shorthand for Default::default()). I can't always do this, otherwise all nodes would just have the empty string. What a cludge:

graph! {
    node [ weight = default ]; // Never used, but makes `or_insert_with` type safe
    a [ weight = (1, 2.5, false) ];
    a -- a; // loop on a
}

As a better work-around I want to wrap it in some magic, where it'll be the str if that's the expected type, else a blanket implementation of Default::default(). Alas I can't figure out how to handle a specific case differently. This complains that the 2nd impl conflicts with the 1st one:

trait StrOrDefault<T: Default> {
    fn str_or_default(_str: &'static str) -> T { Default::default() }
}
impl<T> StrOrDefault<T> for T {}
impl StrOrDefault<&'static str> for &'static str {
    fn str_or_default(str: &'static str) -> &'static str { str }
}

I thought that's what blanket implementations are good for. How do I deal with that?

Looks like a case for specialization on nightly.

Wow, yes it does. So sad! I love Rust for things being really thought out in depth. And yet, at the same time it's terrible to have such a useful feature still so unclear after 8 years. And even min-specialization seems not to be moving.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.