Announcing Big `S` 1.0 - Rust's missing `String` literal

Hey. Sometimes we need a String but all we have is a string literal (an
&'static str). I mean, I think we’ve all seen eye-searing code filled with
"this".to_string(), "that".to_owned(), or String::from("theother").

OMG somebody just do something about it.

Introducing S, the three-char solution to Rust’s stupidest papercut.

Check it out:

do_something_lame("this".to_string(), "that".to_string(), "theother".to_string());

:roll_eyes:

use big_s::S;

do_something_rad(S("this"), S("that"), S("theother"));

:+1:

API docs: https://docs.rs/big_s/

11 Likes

Maybe we just need a procedura macro that turns all s: String arguments into s: Into<String>. :smiley:

1 Like

Why the argument of S require 'static lifetime?
To make it clear that S is expected to be used with string slice literal?

I also created a single letter function, B, for byte string literals, but for a different reason: byte string literals have type &'static [u8; N] (where as normal string literals have type &'static str). This leads to all sorts of inconvenience when using them.

3 Likes

Don’t you think you’re being a bit melodramatic?

6 Likes

“Stupidest papercut” may be overly melodramatic, but I found the same thing annoying, so I wrote my own S(…). In my case it’s just a function

#[allow(non_snake_case)]
pub fn S<T: fmt::Display>(s: T) -> String { s.to_string() }

The compiler will nag you to death if you forget to allow non_snake_case. Oops. I’m being overly melodramatic.:smiley:

3 Likes

Yes.

Totally.

13 Likes

Has there been a proposal for a S"Foo" syntax yet?

3 Likes

Oh yeah, many times:

2015: https://internals.rust-lang.org/t/pre-rfc-custom-string-literals/5037
2017: https://internals.rust-lang.org/t/pre-rfc-string-literals-through-prefixes/2928
2018: https://internals.rust-lang.org/t/string-from-d/8528

And probably way, way more than that.

AFAIK the most important non-obvious detail covered in these threads is that adding new prefixes is technically a breaking change for reasons involving macros that I don’t understand, though editions/epochs probably solve that.

I even proposed making string literals just be String in some cases, with no prefix involved at all, which is still my preferred solution: https://internals.rust-lang.org/t/pre-rfc-allowing-string-literals-to-be-either-static-str-or-string-similar-to-numeric-literals/5029

IIUC, there’s no deep reason why we haven’t done any of these, other than no one considered “the string literal problem” to be a top 5 ergonomic headache during the ergonomics initiative, so most of that consensus-building effort went towards other language changes like editions/epochs, the module system reform, and non-lexical lifetimes (which is pretty hard to disagree with).

5 Likes

Here’s an example:

macro_rules! foo {
    (S$x:literal) => {
        println!("ABC");
    },
}

If we make a macro like that, then adding the S"..." syntax will break it, making it an error because it would be ambiguous which we’re referring to with the S, and macros are generally very stubborn about these kinds of things so wiggle room is not readily available. Take the following example:

macro_rules! foo {
    (b$x:expr) => {
        println!("Hi");
    }
}

fn main() {
    foo!(b"abc");
}

This fails to compile because the macro decides to believe that b is part of the literal and not of the syntax, while the following does work:

macro_rules! foo {
    (y$x:expr) => {
        println!("Hi");
    }
}

fn main() {
    foo!(y"abc");
}

And therefore, if S"..." strings are introduced, then that will mean that any macros that depended on S$x:literal matching S"abc" will be broken because the S"abc" will now try to match a literal.

1 Like

While this is true, it’s also true that breaking changes like this have happened before post rustc 1.0, and the impact was managed by building (a subset of) all crates on crates.io and seeing what percentage of crates actually breaks.

1 Like

Instead, we could make the brackets of macros optional, if they only contain 1 variable or literal.

Examples:

S!"Hello, World!"
log!r"C:\temp"
panic!"message"
vec!5
dbg!my_string

Since S! is not a prefix, and therefore not part of the literal, this would be not a breaking change.

There’s a somewhat similar feature in javascript: Tagged templates

2 Likes

Consider this code:

macro_rules! foo {
    (! $blah:ident) => {
        3
    };
}

fn main() {
    foo!(!some_ident);
}

Given that this code compiles now, something like this should then be allowed by rustc, but would be really confusing if used:

some_macro!!!!!!!!!"hello"

I proposed this only for a single identifier or literal. In your example, you would have to add brackets.

EDIT: It would be possible to also allow an expression, including prefix operators like +, -, &, &mut, * and !. But writing foo!!x would be considered bad practice, and rustfmt should add parentheses like foo!(!x).

That would result in a rule that enlarges the complexity of the language in a very non-straightforward way by creating an exception of the form “X is the case, except when Y is the case”.

Personally I’m not a fan of such rules. Languages like C++ and Scala are full of them, and it makes it that much harder to keep everything straight due to increased incidental complexity of the Rust language (as opposed to essential complexity i.e. the complexity necessarily involved in solving some concrete problem).

As it is I believe that Rust has already used up most if not all of its complexity budget (i.e. how complex a language can become before it is so large the programmer doesn’t even know for sure which rules apply and in which order) on things like generics, ownership + borrowing, traits + trait objects, and macros.

Built-in prefixes and suffixes (r#ident, r#"String"#, b'c', 5u8) also work only with single identifiers or literals. I don’t think that this rule would be hard to understand.

Also, it’s similar to how brackets are used in math:

5 * x       // parens not required
5 * (x + 2) // parens required

foo!x       // parens not required
foo!(x + 2) // parens required
2 Likes

The difference is that there you can add as many matching delimiters you want and it will always work exactly the same i.e. r#"foo"# results in the same string as r###"foo"###.

With your macro proposal that doesn’t seem to be the case.