One option in Python is the f-string:
f"{a} {b} {c}"
Which is equivalent to:
str(a) + " " + str(b) + " " + str(c)
What are some other options for formatted string literals, and what are their advantages and disadvantages?
Warning: without any ill-intent, this answer post responds to the question by asking more questions (hopefully useful ones, and hopefully while imparting some useful information or insight).
To start with, it's hard to say whether a particular choice or example will be an advantage or a disadvantage just by looking at a string formatting mechanism in isolation. It will really depend on a mixture of what the goals of your language are (Ex. with respect to mechanics or syntax), and following up on that, how the language works as a whole (Ex. with respect to how it is read and run by a machine), and in some cases, decisions you make that ultimately boil down to arbitrary or personal choices (Ex. with respect to syntax).
For example,
Is it a goal of your language to have familiar syntax to people who program in existing, well-established languages targeting the same or similar problem/application-domains?
Is it a goal of your language to help people who write and solve problems using non-programming languages (Ex. Mathematical notation)?
Is it a goal of your language to provide some definition of "sane basic defaults" / batteries included, or instead to back off and let your users build their own abstractions and tools using the language? (In a sense- variations of opinionation)
Is it a goal of your language to be syntactically rich, or syntactically simple? Both those higher-level directions have valid advantages and disadvantages themselves.
Do you see how it can be really hard for me to point at a syntactic choice a language made, separate it from the context of the language's goals, and then say "this is better in general because <X>"? It would really only make sense for me to say "this made sense to the developers of this language because they wanted their language to achieve its goals, which included <X>".
Broadly speaking (unless the designers of the language didn't care, or weren't given the time to care deeply or design carefully), those kinds of higher design goals are what inform the more concrete choices that are more easily observable at a surface glance.
For example,
How "deeply" is the mechanism ingrained into the language? (whether it has dedicated syntax / syntactic sugar, or doesn't have any particular special syntax and is really just something implemented using the language and lives at the library level of things. Or a mixture).
How much room does the mechanism provide for users of it to customize it based on their user-defined data structures?
What impact does the implementation have on costs in terms of space and time? If you lean to either end of the spectrum of library vs specialized-syntax, what limitations might you encounter due to the way your runtime and object formats (Ex. machine code, bytecode, interpreted script) work?
Are there degrees of self-consistencies in syntactic choices with other related features in the language?
What tradeoff is made between brevity and intuitiveness of mechanics (related to readability)?
The goal of this "big-list" answer post is not really to go very in-depth with how each of these languages work (because that's quite broad, hard for to do with my limited knowledge, and you might get bored), but to try to give you an idea that while there are broad categories we can box things into, the world is a bit wider and more colourful than it might seem from a surface glance, and to give you some ideas of what else there is to think about underneath the level of just syntax. Syntax is just an interface for all of what your language really is when you look beneath that surface, and I assume that's the meat we really care to get at.
I'll group what I know based on a more superficial(?) level of how the literals and values to format are arranged positionally at the source level, and order entries of the group very roughly based on what I know about how they fall on the compiled vs. interpreted spectrum (I'm human and I make mistakes. Corrections are welcome).
There's a style where the "string literal" contains placeholders / slots that describe how to format what fills the placeholder / slot, and then the values that fill the slots are written separately. If I understand correctly, these mostly fall towards the "library-level" end of the spectrum. Ex.
There's a style where the values to format are / can be positionally arranged at the source level inside the "string literal". If I understand correctly, these mostly fall toward the "syntax-level" / "language foundational mechanisms" end of the spectrum. Ex.
Bash parameter expansion and command substitution
And similar in other shells. Ex.
There's more to see if you just look at Wikipedia's String Interpolation page.
And then there are the approaches that don't use "string literals" at all but still are still designed for formatted text:
operator<<, <iomanip>StringBuilder + Object::toStringFrom my (potentially faulty or misguided) observation, looking at a lot of the forms of syntax-level approaches that are commonly used, I see that there's nothing really about the part of the syntax related to the interpolation itself in those forms that really limits the user from putting arbitrary expressions there except for the limitations of what expressions can be written in any other expression-fitting part of the language's grammar. Which brings me back to my point on looking deeper and more holistically than just a single syntactic choice for a single mechanism in isolation.
Here's something that might go unforgotten, but shouldn't: How capable is your language of evolving with respect to particular syntactic choices here? Does your language care about evolving? Because if it does, since we can't see into the future and what programming paradigms may come, and it may be difficult to see what pain points might be found from experience with writing in the language at larger scales, it would be wise to think about this. This is part of why people who design messaging protocols leave reserved bits, or allow their messaging formats for arbitrary extension. Have you allowed for that kind of room for evolution? Or have you boxed yourself out of that? Or boxed yourself out of forms of evolution that could be more ideal for your goals?
And don't just think about your language. Think about the code that users of your language will write. How could your design choices make it easier or harder for them to evolve their code if your language evolves?
These questions really matter. Look back at the list above. Do you notice that some languages have multiple approaches? And in a number of those cases, those options did not all come into being at the same time? Some languages even might want to take multiple approaches, but doing so would cause old code to take on different meaning (dialect bifurcation (think Python 2 and Python 3)).
Lastly, unsolicited, I invite you to take a step back and also ask yourself another question.
Look at your language in a more holistic sense. Aside from any dedicated syntax, dedicated syntactic sugar, or dedicated library things you've decided to provide, in what ways / to what degree does the rest of your language enable its users to devise their own mechanisms for doing what can't be done with what your language gifts them in a neat box wrapped up and tied with a string (pun not intended)? Try using your language to do what it doesn't provide out of the box. Does what you write hold up to the goals of your language? (Ex. (potentially) readability and maintainability)
gimme['en'] = f"Give me the {colour} {object}." gimme['fr'] = f"Donnez moi {definite_article(object)} {object} {colour}.". Yes, it would be more complicated than that, but it makes the process easier.
$\endgroup$
%2$s %1$s to reverse the order in C? I know at least Java supports something like that.
$\endgroup$
print(gimme[language], colour, object), but C wouldn't know whether to say printf(…, colour, object) or printf(…, object, colour), and that's not even considering how to generate the definite article.
$\endgroup$
"\(a) \(b) \(c)"
"""
<body>
<p>\(hello_world_str)</p>
</body>
"""
This syntax is reasonably lightweight and doesn't conflict with the common unformatted string syntax (many languages that use backslash as a string escape symbol will not allow unrecognized escape sequences like \(, and such occurrences regardless are far less common than the literal string characters { and }).
I would like to talk about the old FORTRAN formats at least from historical respect:
WRITE (6,7000) month
WRITE (6,7000) day
7000 FORMAT(15H Hello, world: , I3)
END
Why largely obsolete now, they include some interesting features:
printf, there's often no way for an implementation to include only the features that a program actually uses, but the needs of bespoke formatting functions could be determine statically.
$\endgroup$
Kotlin has pretty similar string interpolation to Python. The difference is that you don't need to specify a special f-string with an 'f' before the string but do need to specify the interpolation with a '$'.
As an example from the Kotlin docs:
val name = "Joe"
println("Hello, $name")
println("Your name is ${name.length} characters long")
This looks neater as you don't need the {} braces around every expression, but you can include them for longer expressions. You also don't need to specify that it is an f-string before using the interpolation.
A lot of examples of format string literals have already been given. I'll just mention two issues here:
As mentioned by Ray Butterworth:
In python one can say:
gimme['en'] = f"Give me the {colour} {object}." gimme['fr'] = f"Donnez moi {definite_article(object)} {object} {colour}."
The problem here is that this only works when declaring all the translations in the code. You can't load f-strings from a file, unless you use eval(), which then opens you up to lots of security issues.
Separating code from translations is very important if you want to outsource translating your code to a team of people that are not coders. For example, with GNU gettext, a file can be created by a tool containing all the original format strings in your code, and then translaters can make a translation file for all those strings for their language. That file containing translations can be loaded at runtime, no recompilation necessary.
Another issue with the Python example from Ray is that it's very slow if you have translations for lots of languages: each format string literal is evaluated as it is added to the dict gimme, even if you are only ever going to use one of them. And you cannot move the format string literals out of the function you are using them if it references local variables.
You mention:
f"{a} {b} {c}"Which is equivalent to:
str(a) + " " + str(b) + " " + str(c)
Being able to specify how a format string literal is interpreted and what it is equivalent to is great. You can then think of doing that in other languages as well, not just interpreted ones! Wouldn't it be interesting if this could be applied to C++? What if:
std::cout << f"Hello {name}, the answer is {6 * 7}!\n";
Was defined to be equivalent to:
std::cout << std::format("Hello {}, the answer is {}!\n", name, 6 * 7);
It's a relatively easy change to the language, and makes use of existing library features to do the heavy lifting. Even better would be to allow the formatting function to be used to be replaced by a custom version (just like new can be replaced by a user-defined function in C++). This would open up new possibilities, for example automatically applying gettext() on the format string to internationalize your program with very little changes.
operator f""(const char* str, std::size_t size, auto&&... args)? Or if C++ had reflection like in @xigoi's answer, maybe you'd only need C++11's user defined literals.
$\endgroup$
If your language either supports procedural macros or allows dynamically accessing variables by name, you can avoid having special syntax for formatted strings at all and instead implement them as a macro/function in the standard library or even an installable package.
An example of the macro approach: Nim's std/strformat
An example of the reflection approach: Arturo's render
Go offers excellent support for string formatting in the printf tradition. So, based on the formatter (%v, %t, %d,%s, etc.) it can handle different types of parameters and print the result as a string. https://gobyexample.com/string-formatting
str.format).
$\endgroup$
Like in shell:
"hello, $name"
name is a variable.
Match expression (to get input since formatting values are stored in capture groups): (.*)\n(\d*)
Substitution expression (format string): My name is $1 and I am $2 years old.