7
$\begingroup$

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?

$\endgroup$
0

9 Answers 9

15
$\begingroup$

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).


From 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)

$\endgroup$
10
  • $\begingroup$ note to self: TODO: add some rust (the language) info. I forgot, and right now, I need to take a rest. $\endgroup$
    – starball
    Commented May 21, 2023 at 8:19
  • $\begingroup$ meta note: in an effort to generalize my thoughts here, since there are so many questions like the one here, I've factored some of the "core"out and put it here $\endgroup$
    – starball
    Commented May 21, 2023 at 10:23
  • 1
    $\begingroup$ Another factor is i18n, where word order in messages can be different. In C, having a format string for each language doesn't work. In python one can say: 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$ Commented Jun 10, 2023 at 13:52
  • 1
    $\begingroup$ Can’t you say %2$s %1$s to reverse the order in C? I know at least Java supports something like that. $\endgroup$
    – Bbrk24
    Commented Jun 10, 2023 at 15:25
  • 1
    $\begingroup$ @starball, because when used, Python would say 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$ Commented Jun 11, 2023 at 13:05
6
$\begingroup$

Swift Style

"\(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 }).

$\endgroup$
1
4
$\begingroup$

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:

  • The formatting string (number formats and surrounding text) is defined separately from the statement that prints the values. It can be far apart (it was common to have them at the end of the program). This reduces the "visual noise".
  • The formatting string is referenced by the label, 7000 in this example (6 is the output channel, the terminal) and can be shared by multiple print (write) statements. If the same or related values are printed, the format only needs to be defined once.
  • All printable characters can be used in the message without any escaping, providing the length of the text (15 characters) instead. While it is very error prone to count and adjust these counters, reading is easy.
  • I3 is the number format (integer, field width of 3). You can read here about the possible format specifiers.
$\endgroup$
1
  • $\begingroup$ An advantage of this approach is that it facilitates the use of code generation tools to generate bespoke code which handles formatting. When using something like 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$
    – supercat
    Commented Nov 5, 2023 at 20:22
3
$\begingroup$

Kotlin Style

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.

$\endgroup$
1
  • $\begingroup$ This, or something like it, is also used in Perl IIRC. $\endgroup$ Commented Jul 12, 2023 at 23:18
2
$\begingroup$

A lot of examples of format string literals have already been given. I'll just mention two issues here:

Internationalization (i18n)

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.

Equivalence

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.

$\endgroup$
1
  • $\begingroup$ Hm, 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$
    – G. Sliepen
    Commented Jul 13, 2023 at 11:31
2
$\begingroup$

No built-in syntax

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

$\endgroup$
1
$\begingroup$

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

$\endgroup$
2
  • 1
    $\begingroup$ OP is specifically asking about formatted string literals (such as Python's f-strings), rather than string formatting in general (such as Python's str.format). $\endgroup$
    – Olive
    Commented May 20, 2023 at 11:21
  • 2
    $\begingroup$ f-strings is the better replacement of python's previous both %-formatting and str.format() $\endgroup$
    – user514
    Commented May 21, 2023 at 15:24
1
$\begingroup$

Shell style

Like in shell:

"hello, $name"

name is a variable.

$\endgroup$
1
$\begingroup$

RegEx style (similar to shell)

I’ll just use PCRE2 regex for this example.


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.

Try it online!

$\endgroup$
3
  • 1
    $\begingroup$ How does this answer the question? $\endgroup$
    – mousetail
    Commented Jul 13, 2023 at 11:33
  • $\begingroup$ @mousetail It's a way of formatting $\endgroup$ Commented Jul 13, 2023 at 11:34
  • 1
    $\begingroup$ This question is about syntax options for formatting $\endgroup$
    – mousetail
    Commented Jul 13, 2023 at 11:35

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .