Remix.run Logo
Mawr 9 days ago

It's fascinating how differently languages approach the string formatting design space.

- Java's been trying to add f/t-strings, but its designers appear to be perfectionists to a fault, unable to accept anything that doesn't solve every single problem possible to imagine: [1].

- Go developers seem to have taken no more than 5 minutes considering the problem, then thoughtlessly discarded it: [2]. A position born from pure ignorance as far as I'm concerned.

- Python, on the other hand, has consistently put forth a balanced approach of discussing each new way of formatting strings for some time, deciding on a good enough implementation and going with it.

In the end, I find it hard to disagree with Python's approach. Its devs have been able to get value from first the best variant of sprintf in .format() since 2008, f-strings since 2016, and now t-strings.

[1]: https://news.ycombinator.com/item?id=40737095

[2]: https://github.com/golang/go/issues/34174#issuecomment-14509...

umanwizard 8 days ago | parent | next [-]

> Go developers seem to have taken no more than 5 minutes considering the problem, then thoughtlessly discarded it: [2]. A position born from pure ignorance as far as I'm concerned

There are a million things in go that could be described this way.

unscaled 8 days ago | parent [-]

Looking at the various conversations involving string interpolation, this characterization is extremely unkind. They've clearly spent a lot more than 5 minutes thinking about this, including writing their own mini-proposals[1].

Are they wrong about this issue? I think they are. There is a big difference in ergonomics between String interpolation and something like fmt.Sprintf, and the performance cost of fmt.Sprintf is non-trivial as well. But I can't say they didn't put any thought into this.

As we've seen multiple times with Go generics and error handling before, their slow progress on correcting serious usability issues with the language stem from the same basic reasons we see with recent Java features: they are just being quite perfectionist about it. And unlike Java, the Go team would not even release an experimental feature unless they feel quite good about it.

[1] https://github.com/golang/go/issues/57616

mananaysiempre 8 days ago | parent | next [-]

> There is a big difference in ergonomics between String interpolation and something like fmt.Sprintf

On the other hand, there’s a difference in localizability as well: the latter is localizable, the former isn’t. (It also worries me that I see no substantive discussion of localization in PEP 750.)

Mawr 7 days ago | parent | prev [-]

I just expect better from professional language designers. To me, the blindingly obvious follow up to the thought "We understand that people familiar with other languages would like to see string interpolation in Go." [1] is to research how said other languages have gone about implementing this and to present a brief summary of their findings. This is table stakes stuff.

Then there's "You can [get] a similar effect using fmt.Sprint, with custom functions for non-default formatting." [2]:

- Just the fact that "you can already do this" needs to be said should give the designers pause. Clearly you can't already do this if people are requesting a new feature. Indeed, this situation exactly mimics the story of Go's generics - after all, they do not let you do anything you couldn't do before, and yet they got added to Go. It's as if ergonomics matter, huh.

Another way to look at this: if fmt.Sprint is so good it should be used way more than fmt.Sprintf right? Should be easy to prove :)

- The argument crumbles under the load-bearing "similar effect". I already scratched the surface of why this is wrong in a sibling post: [3].

I suspect the reason for this shallow dismissal is the designers didn't go as far as to A/B test their proposal themselves, so their arguments are based on their gut feel instead of experience. That's the only way I can see someone would come up with the idea that fmt.Sprint and f-strings are similar enough. They actually are if all you do is imagine yourself writing the simplest case possible:

    fmt.Sprint("This house is ", measurements(2.5), " tall")

    f"This house is {measurements(2.5)} tall"
Similar enough, so long as you're willing to handwave away the need to match quotation marks and insert commas and don't spend time coding using both approaches. If you did, you'd find that writing brand new string formatting statements is much rarer than modifying existing ones. And that's where the meat of the differences is buried. Modifying f-strings is trivial, but making any changes to existing fmt.Sprint calls is painful.

P.S. Proposing syntax as noisy as:

    fmt.Println("This house is \(measurements(2.5)) tall")
is just another sign the designers don't get it. The entire point is to reduce the amount of typing and visual noise.

[1]: https://github.com/golang/go/issues/57616#issuecomment-14509...

[2]: https://github.com/golang/go/issues/34174#issuecomment-14509...

[3]: https://news.ycombinator.com/item?id=43651419

pansa2 7 days ago | parent [-]

> Proposing syntax as noisy as […] is just another sign the designers don't get it.

Are you objecting to the use of `\(…)` here instead of `{…}`? Because of the extra character or because of the need to nest parentheses?

nu11ptr 8 days ago | parent | prev | next [-]

Value types anyone? I have zero doubt it is tough to add and get right, esp. to retrofit, but it has been so many years that I have learned/discarded several new languages since Java... and they STILL aren't launched yet.

mjevans 8 days ago | parent | prev | next [-]

Go(lang)'s rejection makes sense.

A format function that arbitrarily executes code from within a format string sounds like a complete nightmare. Log4j as an example.

The rejection's example shows how that arbitrary code within the string could instead be fixed functions outside of a string. Safer, easier for compilers and programmers; unless an 'eval' for strings is what was desired. (Offhand I've only seen eval in /scripted/ languages; go makes binaries.)

paulddraper 8 days ago | parent | next [-]

No, the format function doesn't "arbitrarily execute code."

An f/t string is syntax not runtime.

Instead of

    "Hello " + subject + "!"
you write

    f"Hello {subject}!"
That subject is simple an normal code expression, but one that occurs after the opening quote of the literal and before the ending quote of the literal.

And instead of

    query(["SELECT * FROM account WHERE id = ", " AND active"], [id])
you write

    query(t"SELECT * FROM account WHERE id = {id} AND active")
It's a way of writing string literals that if anything makes injection less likely.
mjevans 8 days ago | parent | next [-]

Please read the context of my reply again.

The Rejected Golang proposal cited by the post I'm replying to. NOT Python's present PEP or any other string that might resolve magic variables (just not literally eval / exec functions!).

zahlman 8 days ago | parent | next [-]

As far as I can tell from the linked proposal, it wouldn't have involved such evaluation either. It seems like it was intended to work fundamentally the same way as it currently does in Python: by analyzing the string literal ahead of time and translating into equivalent explicit formatting code, as syntactic sugar. There seem to have been many misunderstandings in the GitHub discussion.

mjevans 8 days ago | parent [-]

In that case, I might have misunderstood the intent of those examples.

However the difficulty of understanding also illustrates the increased maintenance burden and language complexity.

eviks 8 days ago | parent [-]

Unless workarounds to a missing feature have a higher maintenance burden like in this case, and you can't avoid it via learning

mjevans 8 days ago | parent [-]

Go's preferred way would probably be something like compute the aliased operations on the line(s) before, then reference the final values.

E.G. Adapting https://github.com/golang/go/issues/34174

    f := 123.45
    fmt.Fprintln("value=%08.3f{f}") // value=0123.450
    fmt.Fprintln("value=%08.3f", f) // value=0123.450
    s := "value"
    fmt.Fprintln("value='%50s{s}'") // value='<45 spaces>value'
    fmt.Fprintln("value='%50s'", s) // value='<45 spaces>value'
The inline {variable} reference suffix format would be less confusing for situations that involve _many_ variables. Though I'm a bit more partial to this syntax with an immediately trailing %{variable} packet since my gut feeling is that special case would be cleaner in a parser.

    fmt.Fprintln("value=%08.3f%{f}") // value=0123.450
    fmt.Fprintln("value='%50s%{s}'") // value='<45 spaces>value'
paulddraper 8 days ago | parent | prev [-]

The proposal cited Swift, Kotlin, and C# which have similar syntax sugar.

The proposal was for the same.

chrome111 8 days ago | parent | prev [-]

Thanks for this example - it makes it clear it can be a mechanism for something like sqlc/typed sql (my go-to with python too, don't like orms) without a transpilation step or arguably awkward language API wrappers to the SQL. We'll need linters to prevent accidentally using `f` instead of `t` but I guess we needed that already anyways. Great to be able to see the actual cost in the DB without having to actually find the query for something like `typeddb.SelectActiveAccount(I'd)`. Good stuff.

WorldMaker 8 days ago | parent | next [-]

The PEP says these return a new type `Template`, so you should be able to both type and/or duck type for these specifically and reject non-Template inputs.

paulddraper 8 days ago | parent | prev [-]

It is a different type.

You can verify that either via static typechecking, or at runtime.

miki123211 8 days ago | parent | prev | next [-]

In many languages, f-strings (or f-string like constructs) are only supported for string literals, not user-supplied strings.

When compiling, those can be lowered to simple string concatenation, just like any for loop can be lowered to and represented as a while.

zahlman 8 days ago | parent | next [-]

In case there was confusion: Python's f-string functionality in particular is specific to string literals. The f prefix doesn't create a different data type; instead, the contents of the literal are parsed at compile time and the entire thing is rewritten into equivalent string concatenation code (although IIRC it uses dedicated bytecodes, in at least some versions).

The t-string proposal involves using new data types to abstract the concatenation and formatting process, but it's still a compile-time process - and the parts between the braces still involve code that executes first - and there's still no separate type for the overall t-string literal, and no way to end up eval'ing code from user-supplied data except by explicitly requesting to do so.

the_clarence 8 days ago | parent [-]

There is no compile time in python

zahlman 8 days ago | parent | next [-]

Yes, there is.

Python source code is translated into bytecode for a VM just like in Java or C#, and by default it's cached in .pyc files. It's only different in that you can ask to execute a source code file and the compilation happens automatically before the bytecode-interpretation.

`SyntaxError` is fundamentally different from other exceptions because it can occur during compilation, and only occurs at run-time if explicitly raised (or via explicit invocation of another code compilation, such as with `exec`/`eval`, or importing a module). This is also why you can't catch a `SyntaxError` caused by the invalid syntax of your own code, but only from such an explicit `raise` or a request to compile a source code string (see https://stackoverflow.com/questions/1856408 ).

pansa2 8 days ago | parent | prev [-]

Yes there is, when it compiles source code to bytecode.

mjevans 8 days ago | parent | prev | next [-]

My reply was to the parent post's SPECIFIC example of Golang's rejected feature request. Please go read that proposal.

It is NOT about the possibility of referencing existing / future (lazy / deferred evaluation) string literals from within the string, but about a format string that would literally evaluate arbitrary functions within a string.

unscaled 8 days ago | parent [-]

The proposal doesn't say anything about executing code in user-supplied strings. It only talks about a string literal that is processed by the compiler (at which point no user-supplied string can be available).

On the other hand, the current solution offered by Go (fmt.Sprintf) is the one who supports a user-supplied format String. Admittedly, there is a limited amount of damage that could be done this well, but you can at the very least cause a program to panic.

The reason for declining this feature[1] has nothing to do with what you stated. Ian Lance Taylor simply said: "This doesn't seem to have a big advantage over calling fmt.Sprintf" and "You can a similar effect using fmt.Sprint". He conceded that there are performance advantages to string interpolation, but he doesn't believe there are any gains in usability over fmt.Sprintf/fmt.Sprint and as is usual with Go (compared to other languages), they're loathe to add new features to the compiler[2].

[1] https://github.com/golang/go/issues/34174#issuecomment-14509...

[2] https://github.com/golang/go/issues/34174#issuecomment-53013...

NoTeslaThrow 8 days ago | parent | prev [-]

What's the risk of user supplied strings? Surely you know their size. What else is there to worry about?

NoTeslaThrow 8 days ago | parent | prev | next [-]

> A format function that arbitrarily executes code from within a format string

So, a template? I certainly ain't gonna be using go for its mustache support.

bcoates 8 days ago | parent | prev [-]

No, it's exactly the opposite--f-strings are, roughly, eval (that is, unsanitary string concatenation that is presumptively an error in any nontrivial use) to t-strings which are just an alternative expression syntax, and do not even dereference their arguments.

rowanG077 7 days ago | parent [-]

f-strings are not eval. It's not dynamic. It's simply an expression that is ran just like every other expression.

bcoates 6 days ago | parent [-]

Right, and then if you do literally anything with the output other than print() to a tty, it’s an escaping/injection attack.

any_func(f"{attacker_provided}") <=> eval(attacker_provided), from a security/correctness perspective

saagarjha 4 days ago | parent [-]

How is this any different from any_func(attacker_provided)

cherry_tree 8 days ago | parent | prev | next [-]

>Go developers seem to have taken no more than 5 minutes considering the problem, then thoughtlessly discarded it

The issue you linked was opened in 2019 and closed with no new comments in 2023, with active discussion through 2022.

cortesoft 8 days ago | parent | prev | next [-]

Then there is Ruby, which just has beautiful string formatting without strange decorators.

bshacklett 8 days ago | parent [-]

That tracks. Ruby followed in the footsteps of Perl, which had string manipulation as a main priority for the language.

thayne 8 days ago | parent | prev | next [-]

That issue has a link to another Issue with more discussion: https://github.com/golang/go/issues/57616.

But as is all too common in the go community, there seems to be a lot of confusion about what is proposed, and resistance to any change.

1980phipsi 8 days ago | parent | prev | next [-]

D had a big blow up over string interpolation. Walter wanted something simple and the community wanted something more like these template ones from Python (at least from scanning the first little bit of the PEP). Walter eventually went with what the community wanted.

gthompson512 8 days ago | parent [-]

This led to the OpenD language fork (https://opendlang.org/index.html) which is led by some contributors who had other more general gripes with D. The fork is trying to merge in useful stuff from main D, while advancing the language. They have a Discord which unfortunately is the main source of info.

throwaway2037 8 days ago | parent | prev | next [-]

I promise, no trolling from me in this comment. I never understood the advantage of Python f-strings over printf-style format strings. I tried to Google for pros and cons and didn't find anything very satisfying. Can someone provide a brief list of pros and cons? To be clear, I can always do what I need to do with both, but I don't know f-strings nearly as well as printf-style, because of my experience with C programming.

Mawr 8 days ago | parent | next [-]

Sure, here are the two Go/C-style formatting options:

    fmt.Sprintf("This house is %s tall", measurements(2.5))

    fmt.Sprint("This house is ", measurements(2.5), " tall")
And the Python f-string equivalent:

    f"This house is {measurements(2.5)} tall"
The Sprintf version sucks because for every formatting argument, like "%s", we need to stop reading the string and look for the corresponding argument to the function. Not so bad for one argument but gets linearly worse.

Sprint is better in that regard, we can read from left to right without interruptions, but is a pain to write due to all the punctuation, nevermind refactor. For example, try adding a new variable between "This" and "house". With the f-string you just type {var} before "house" and you're done. With Sprint, you're now juggling quotation marks and commas. And that's just a simple addition of a new variable. Moving variables or substrings around is even worse.

Summing up, f-strings are substantially more ergonomic to use and since string formatting is so commonly done, this adds up quickly.

throwaway2037 8 days ago | parent [-]

    > Not so bad for one argument but gets linearly worse.
This is a powerful "pro". Thanks.
theptip 8 days ago | parent | prev [-]

    _log(f”My variable is {x + y}”)
Reads to me a lot more fluently to me than

    _log(“My variable is {}”.format(x+y)) 
or

    _log(“My variable is {z}”.format(z=x+y))
It’s nothing too profound.
blami 6 days ago | parent [-]

I am not very familiar with Python. How do you localize (translate) first one?

wzdd 4 days ago | parent [-]

You don't with f-strings because they're substituted eagerly. You could with the new t-strings proposed here because you can get at the individual parts.

oliwarner 8 days ago | parent | prev | next [-]

It's especially weird how hard people have to fight for string interpolation given it has had implementations since the 1970s.

Even PEP 498 (fstrings) was a battle.

bjourne 7 days ago | parent [-]

Superficially f-strings reminds you of php and everyone remembers how awful that was. But Python's implementation is leagues better and we also have better tooling (ie smart parsers) for handling fstrings.

lynndotpy 8 days ago | parent | prev | next [-]

For all its other problems, f-strings make Python such a pleasure to work with. C# has something similar IIRC.

dionian 9 days ago | parent | prev [-]

Looks great - unlike java which is somehow recommending the format:

STR."Hello \{this.user.firstname()}, how are you?\nIt's \{tempC}°C today!"

compared to scala

s"Hello ${this.user.firstname()}, how are you?\nIt's ${tempC}°C today!"

STR."" ? really?

paulddraper 8 days ago | parent | next [-]

Yeah, I hate to bikeshed, but this is the worst syntax possible without being a full-out prank.

nsonha 8 days ago | parent | prev [-]

also a syntax for braces that looks like escaping

dionian 2 days ago | parent [-]

Yeah, that almost bothers me more than "STR."