String interpolation / template literals like JS?


#1

At $dayjob I do quite a bit of JS, and although I often don’t like it (the types! where are my types?!), there are couple of things I really enjoy.

I was wondering if Rust could ever get something like https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals and does it make sense.

Basically I would like

`${num_bottles} bottles, standing on the wall`

to be

format!("{} bottles, standing on the wall", num_bottles)

It could be used as an alternative to to_string(). And also, could help with string literals which create &str and require transforming into String. A simple

`foobar`

would create a String, instead of &str that "foobar" is.

Would it be against the “operators should be cheap/not allocate”?


#2

This is relatively close to being possible to implement as a macro outside the standard library, see https://github.com/nemo157/interp-rs for a prototype that is hopefully still working on nightly (there are a few limitations in the current parser, but I don’t have any current usecases for this so I’m not planning to do any more work on it)


#3

I don’t think that would be the biggest problem. There are already operators which allocate (exactly around String: the implementation of += potentially needs to grow the buffer).

Rather, since format!() already exists, this would duplicate functionality. Moreover, it would duplicate said functionality which is implemented in the standard library (although with the help of a not-yet-stable compiler extension) in a way that modifies the core language itself. I don’t think that something which can be implemented more or less decoupled from the language should be a language feature.

I see that it is shorter, but it seems like a marginal gain. (Incidentally, I would have preferred if the fmt! macro hadn’t been renamed to format! because it feels like an ad-hoc change making an often-used name twice as long as it was. But alas, we’re now 1.0 so it’s not going to be changed back to something shorter.)


#4
format!("{num_bottles}", num_bottles = 99);

is the closest thing supported currently.


#5

I actually wouldn’t mind this in a library. That’s even better, if you ask me. :slight_smile:

However, I am having troubles figuring out how exactly would this looks like. Any examples?


#6

I’d like to echo this. There’s no reason to add special syntax for this (and, personally, I think backticks make for an awful delimiter). I might imagine, though, that a fairly not-insane extension to format! is a rule

format!("foo {bar} baz") => { format!("foo {bar} baz", bar = #bar) }

where #bar is, I believe, the current 2.0 macro syntax for “make this expression intentionally unhygenic”.

That said, this rule cannot be achieved without compiler support, and only supports the trivial case where you want to interpolate an identifier rather than some complex expression (I argue that interpolating anything more complex than an identifier is a Bad Idea.)


#7
"foo {"bar"} baz"

This may seem contrived in the case of format! since you can just take the literal string outside the interpolation marks, but imagine a source code interpolation macro instead.


#8

Source code interpolation macros exist and they are doing just fine without language support.

By the way, I don’t see why this problem (which IMO is a non-problem) would be special to source code. Any custom type can require escaping or in fact any other kind of formatting, and it’s already easy to produce differently-formatted strings form the same (potentially complex) expression via different traits (Debug vs. Display comes to mind). The mechanism for this already works perfectly feasibly today, even without an interpolation-like feature.


#9

@dpc I could see something like (tagged) template strings be very useful for mixing DSLs (such as HTML, SQL) inside Rust code.

Using procedural macros in its current form has some limitations here, unfortunately (ref).

Perhaps the answer is not template strings per se, but I def think it’d be interesting to enumerate Rust’s shortcomings in this area, and gather examples of how these are overcome in other languages!


#10

There’s a single example in the doc tests. Basically the syntax is the trivial {} delimited expressions, currently with just a basic parser that doesn’t support nested blocks or strings.

let who = "World";
assert_eq!(
     interp!("Hello { who }!").to_string(),
     "Hello World!");

Rather than outputting a string directly it outputs an object that can be turned into a string or written to {io,fmt}::Writers (I was also experimenting with ways to pass around applied but not emitted templates with this library).

Adding this rule to format would take compiler support since format is implemented in the compiler, but implementing it as an alternative proc-macro is trivial once proc_macro_hygiene is stabilized (this allows a proc-macro to expand to an expression instead of just an item, probably this is even implementable today using proc-macro-hack but I haven’t checked).

I strongly disagree, being able to use trivial function calls without needing to put their results into a local variable is very useful when using something like this as a simple templating language, e.g. for generating markdown in something like a blogging engine:

posts.map(|post| interp!(r#"
    ## { post.title.titlecase() }

    { post.summary() }
"#))

#11

You’re going to wind up with deeply nested data-in-code-in-data if you do this. It no longer becomes possible to look at a string and think “ah yes this whole thing is going to get dumped into rodata modulo fmt::Arguments slicing that I don’t need to think about”, and instead can contain arbitrary code. You have sacrificed crucial readability for small writeability gains.


#12

Fwiw, I’ve created a similar macro (using unstable proc_macro_hygiene). https://crates.io/crates/interpolate

I actually created it a while back while I was wondering if a Scala-like approach of using an s-prefix on string literals could imply basic interpolation (and would feel similar to the r-prefix for other string literals). I found the macro helped me get a sense of how it might work, and ultimately made me feel like the interaction with existing format-based macros is awkward, which is also why the crate added it’s on println alternative, which I mostly consider a deal-breaker as it splinters such basic functionality. Ultimately, I found that I just wanted existing format-based macros to do this without the redundant named arg, e.g., println!("Hello {name}").


#13

But if num_bottles is already defined somewhere on top this code will not look so elegant:

format!("{num_bottles}", num_bottles = num_bottles);

Is there any reason why something like field init shorthand isn’t allowed in format!() ?

format!("{num_bottles}", num_bottles);

I think this could be a good improvement


#14

It might be doable. The current syntax is like that probably because it’s easier to parse when = clearly separates identifier from expression.