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.


#15

I meant something like interpolating strings as literals into generated HTML, or SQL, or CrazyHipsterLang code (whose syntax may even be entirely incompatible with Rust’s tokenization of macro input), not interpolating tokens into Rust syntax trees.

Besides, having dedicated syntax for interpolating expressions inside strings (instead of each crate inventing their own scheme) would make interpolation easier to support by syntax highlighters, code completion, refactoring tools, and so on.


#16

That sounds like a perfect candidate for a DSL implemented as a function-like procedural macro to me, so I still don’t see the need for core language support in these cases. In fact, I am currently designing a strongly typed domain-specific query language embedded in Rust and its implementation is a (now stable) function-like proc macro.


#17

Because writing just num_bottles like that already has a meaning: the value of num_bottles: format!("{}", num_bottles). Having that meaning change because of the "{num_bottles}" in the format string seems wrong.


#18

But consider case if there would be multiple values:

let s = format!("string {a}, {b}, {c};", 
    a = a,
    b = b,
    c = c,
);

Why do we need assignment of variables to itself here? For me it’s cleaner in the following form:

let s = format!("string {a}, {b}, {c};",  a, b, c);

because it somehow associates with the same leitmotif as in struct and tuple initialization:

let (a, b, c) = (a(), b(), c());
let x = Y { a, b, c };

#19

Struct initialization didn’t allow ordinary values, so a or b or c without a field name didn’t already have a meaning. format! does allow ordinary values.


#20

Not sure if I understand you correctly but let x = Y { a, b, c }; is valid code and struct initialization allows either ordinary and named values:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=aa5002070add8bc5bad2d14613f5b6c5