-
Notifications
You must be signed in to change notification settings - Fork 17.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: Go 2: string interpolation evaluating to string and list of expressions #50554
Comments
Would this proposal also cover |
I didn't initially think about the multiline strings but yes, I think the proposal should cover them too. |
Thank you for kick-starting this discussion again, and for your proposal! A few thoughts. First... I would want to see having %v be an unspecified default. This would essentially allow the assumption of value-based string output, with a possible override if the type needs to be specified. It's also consistent with the existing fmt intent:
So in your example:
could be
to get the same results. Second... if this were done as a Third... I'm a little unclear on what this does to Fourth... is it going to be an issue to parse out Honestly I'd like to see string interpolation fully at the language level over something sprintf-based. Can you share the reason you see it being better this way? Is this proposal different to simply overcome the resistance of making it a first class feature of the language itself, as mentioned in the referring proposal? ** or an interface array that derives a string is likely better than a |
I don't think that works in the context of this proposal. The proposal is specifically not saying anything at all about format specifiers, which is a good thing. A Note that it doesn't work to use a
It doesn't do anything.
Yes, it absolutely would be an issue, which is why it is good that this proposal doesn't require that.
It's not simple. See all the discussion at #34174. |
Because the curly braces may contain any expression, we need to specify the order of evaluation. I suppose the simplest is to say that expressions within curly braces are evaluated as individual operands as described at https://go.dev/ref/spec#Order_of_evaluation. It's perhaps unfortunate that this doesn't a mechanism to do simple string interpolation, as in One possibility would be that |
Changing the base language by introducing a new kind of literal that explodes a string in a way that is closely tailored to a specific standard library function ( It's basically baking a Sprintf-like "macro" in the language that expand a value into something else for the IMO a base language feature (especially at a level this low: we're talking about a new kind of literal, and literals are the lowest, most basic "pieces" of a language in the grammar hierarchy) should make sense in every context, and be generally useful, to be worth adding. |
I disagree that it makes little sense in every other context (see the sql statement as an example), but I agree that it is somewhat limited in its usage. Although I think some kind of language feature is necessary to elegantly solve the problem outlined in my proposal. |
Since the special kind of string really only makes sense if it's used with a function of a specific signature maybe we can go about this differently and have a special kind of function call with a regular string literal. Rough sketch: Something like This would be similar to javascript's tagged template literals https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates There could be functions in fmt that's like Sprint/Sprintln but with the correct signature. That wouldn't work with the Printf formatting but the idea could be extended to allow passing extra info to the tag function so that something like |
If at all possible I would like to preserve at least this aspect of the proposal: It is a drop in replacement for manually providing the arguments to the |
That's certainly understandable and admirable. My main concern is that it seems like it would be somewhat hard to use correctly since you have to manually pair formatting directives with the interpolation points. Go vet could find them when you're working with fmt but not in general. That's much less of an issue when you can tell the function where the holes are supposed to be by passing a []string instead of a string with the holes already cut out. |
I understand your point, but I don't see how this could be resolved elegantly. We could use |
Yeah, you either have to leave a special character where there was a cut or return an I don't think adding something that only really works with Printf is worth changing the language. I'm not sure my suggestion is worth it, either, but it would provide something that can be used for string interpolation while still being fairly general (there's no restriction that the tag need return a string, for example). |
I actually really like this proposal in its current state. Specifically, it means that I don't need to re-learn things like formatting specifiers. The only thing I don't like (as Ian mentioned) is that we're adding something in the spec that's tailored to a library function. I'm personally not a fan of prefixed strings (ie I'd also like to mention that this proposal bears some resemblance to @bradfitz's suggestion, granted I think the language omitting the identifiers makes a lot more sense: #34174 (comment) |
My experience with string interpolation has been that you have to tediously change it back to format strings when you want to support translations into different languages (i18n). So even in programming languages that support string interpolation I developed the habit of writing user-facing strings (this includes error and log messages) as format strings by default. |
For SQL statements I'd prefer to use a string templating function that is aware of the syntax within the string, one which applies the correct escaping and quoting depending on context and the data types of the arguments to avoid injection vulnerabilities, like |
@deanveloper I'd be totally fine with replacing the prefixed string with the backslash brace syntax. And I forgot to mention it in the original post, but yes Brad's suggestion was indeed the inspiration for it. @fzipp The SQL statement is actually injection safe. The |
I wrote a short example of what a Sprint/Sprintf would look like if the hole position and optional tags are passed in: https://go.dev/play/p/6VBSpkADqmH @deanveloper The problem with I prefer the syntactic form I sketched above over the lexical form since you would always need a function to interpret the results so 99.999% of the time you'd end up writing
The js tagged template literals have been used for i18n: https://i18n-tag.kolmer.net/docs/index.html#currency-formatting Note that it passes formatting directive in the string portion after the interpolation points as there is not a facility for in-band formatting directives in js. The first example from the link i18n`Hello ${ name }, you have ${ amount }:c in your bank account.` would be i18n"Hello { name }, you have { amount : c } in your bank account." using the version I sketched above. |
Thanks for putting this proposal together, overall the broad strokes seem really good. I do wonder whether we're giving up a little too much in service of reusing the existing In particular, because the compiler completely strips all information about where and how the values were embedded in the string...
I think resolving these requires either passing the full unparsed string to the target function or packaging more information (integer offsets, maybe expression strings) into the parsed data slice. Either would probably require making a new family of |
A gadget that would do this seems macro-like, to munge around syntax, and I could imagine a lot of different hygiene rules. The simplest hygiene rule might be, just look up string variables named like fields in scope.
It seems like almost immediately, for a little bit of flexibility, one wants some way to call the |
In terms of simplicity, a newcomer could not understand why is possible to use IMO tagged template, |
I would be ever so happy if emailBody := $`Hello {name},
The amount owed is $%.02f{amount}. You have {days} days to pay.
Otherwise the amount of $%.0f{amount} will begin to gain interest
at 184%% per month.
` was just translated to emailBody := fmt.Sprintf(`Hello %s,
The amount owed is $%.02f. You have %d days to pay.
Otherwise the amount of $%.0f will begin to gain interest
at 184%% per month.
`, name, amount, days, amount) before compilation. This simple translation would answer all my string interpolation dreams. No need to specially prepare anything for Common code to both code blocks name := "The Dude" // Defaults to string
amount := 13.42007 // Defaults to float64
days := 5 // Defaults to int |
I put together a little prototype: https://github.com/Cookie04DE/gof |
A problem with this approach is that in some sense each argument must be mentioned twice: once with the format character and once with the name. That applies to any use of this style of interpolation: since the curly braces disappear in the final version of the string, there always to be something in the string, outside of the curly braces, that marks where the argument should go. If you accidentally omit the We could make this even more specific to Or we could make this slightly less specific to Or perhaps we could use a predeclared builtin function. func interpolate(s string, marker string) (string, []any) |
Perhaps an alternative proposal could be that you have to specify a replacement string for the expressions you insert. Like this: |
I don't think a comma would be the best choice, because a Go expression can include a comma. But I think we could use a colon.
This would become
Then we would give an error for a curly brace in one of these strings without a colon. Or perhaps if there were no colon we could replace it with @bradfitz suggests that we could use single quote instead of @griesemer observes that |
A colon would indeed be better suited as a separator. I have two slight concerns using Changing the return types of format strings to no longer be |
I suggest to use this syntax
I find it clearer compared to this one
We can disallow a single quoted string with no embedded expressions. So As previously suggested, this expression can be used where an expression list can be used. The compiler expands its embedded expressions. For example As a special case, if an embedded expression is convertible to a string, according to the Go specification, the verb can be omitted and the expression is converted to a string, possibly with
and the compiler converts it to
Embedded expressions with and without a verb can be used in the same single-quoted string. For instance
is compiled as
Since expressions without a verb are not expanded, if
Some examples adapted from this proposal and other proposals:
|
Upon further reflection, using a single quote seems to obscure and easy to miss. Since the main use of this would be for The idea of supporting any string conversion if there is a missing colon might be troublesome, as Go permits converting |
It seems acceptable to me. It might be allowed to omit the colon even for integers but then I understand that someone might wonder why for strings and integers it can be omitted and for other types not. |
Let's assume for a moment that we can change how the
you have to write
that is, expressions are interleaved with formatting strings and verbs are placed at the beginning of the string following the expression. If the verb is not present for an expression,
Let the compiler split the following string according to the expressions in parentheses
in this way
So, putting the two together, you could write
This solution would have the following advantages
If compatibility is guaranteed only for programs that correctly call Below is an example that can be used with an extended version of the
and with an extended
|
Seems to me that |
@ianlancetaylor Using
For example, the following call
is expanded as
|
I don't think that works? Suppose |
@magical Verbs and arguments are first parsed as they are now. If there are too many arguments the first extra argument is formatted with
In this case, |
@gazerro I think I misunderstood. You seem to be suggesting that we compile |
Perhaps it would be useful to consider a simpler approach: #57616 . |
Per the discussion in #57616 this is a likely decline. Leaving open for four weeks for final comments. You can a similar effect using |
No further comments. |
golang/go#50554 rebuild
Author background
Experienced.
Java, C#, Kotlin, JavaScript
Related proposals
proposal: Go 2: string interpolation #34174 proposes string interpolation similar to other languages like C# and Kolin.
Instead of formatting on the language level I propose to add a language features that increases readability and maintainability but leaves the actual formatting up to the user's choosing.
Proposal
What is the proposed change?
I propose to add "format strings" which are expressions that return a
string
and[]interface{}
. They are intended to be used withfmt.Printf
and friends. They accept expressions and return the string without the expressions and the results of those in a slice in the order they appeared in.Who does this proposal help, and why?
Consider the following example:
emailBody := fmt.Sprintf("Hello %s. The item %s you bookmarked on %s is available now. Consider purchasing now (%v) since there are only %d left.", name, product, date, whatever, amount)
Can you tell at a glance which expression would go inside the brackets?
If you can: Great; but I would expect many people to take longer.
Now what if we wanted to change the email? Imagine we wanted the bracket and content to be at the end of the message. Now we have to change the actual string but also take care to remove the correct expression on the right and place it at the end; we have to change two places and keep them in sync. This adds cognitive overhead and is an easy source of bugs (although the tooling helps here).
Consider how the email looks with the new syntax:
emailBody := fmt.Sprintf($"Hello %s{name}. The item %s{product} you bookmarked on %s{date} is available now. Consider purchasing now (%v{whatever}) since there are only %d{amount}} left")
Moving the brackets and content is very easy now and you can tell quickly what gets printed here.
Please describe as precisely as possible the change to the language.
I propose to add the following new Go expression:
$""
The runes between the two quotation marks are to be treated like a string inside the regular quotation marks (
""
) with the exception of the opening and closing curly braces ({
and}
). In between those there has to be a valid Go expression that evaluates to a single value (statements aren't allowed).If a literal open or closed curly bracket is needed the following two escape sequences are usable:
\{
and\}
. These are invalid anywhere else.I call these formatting strings (FS's for short).
FS's are expressions that can be either be passed as arguments to a function or be assigned to variables.
As function arguments:
The function has to have the following two types as its parameters where the FS is inserted:
string
and[]interface{}
orstring
and...interface{}
. If the last parameter is variadic the FS has to be the last argument and no further may follow.As variable assignment:
The FS has to be assigned to two variables of the following types:
string
and[]interface{}
.The returned string contains everything inside the quotation marks besides the curly brackets and their contents. For example: For
$"Hi, I am %s{name} and %d{age} years old"
it is"Hi, I am %s and %d years old"
, while the slice contains the values ofname
andage
.The slice is never nil but can be empty if no expressions were provided.
It would include the new quotation marks, the two types they evaluate to and explain in which contexts they may be used. As well as the requirements for their use in arguments and variable assignments.
But most importantly it wouldn't need to even mention the
fmt
package, since the actual formatting isn't done by the language itself. This also adds flexibility since the processing code doesn't need to be thefmt
package.Take this sql statement as an example:
dbConn.QueryContext(context.Background, $"SELECT name FROM app_user WHERE email = $1{email} AND profile_type = $2{profileType}")
.Instead of manually writing the format for the sprintf family of functions, we can use format strings which help maintain readability and are easier to modify. They behave just like normal strings with the exception that you can add expressions inside curly braces in them. Because the expressions are right next to where they are used you can easily copy and paste or move them without worrying about accidentally affecting the other expressions.
Yes.
fmt.Printf("Hello Gopher %s, you are %d years old and you're favorite food is %s", name, age, favoriteFood)
fmt.Printf($"Hello Gopher %s{name}, you are %d{years} old and you're favorite food is %s{favoriteFood}")
There are no such features at language level.
Costs
It would make Go slightly harder to learn since every new feature has to be learned and understood.
The cost is the added complexity in the compiler and the tooling.
All of them, since the change introduces new syntax which need to be recognized. Gofmt also needs to format the expressions inside the curly braces.
The compiler needs to be able to recognize the new quotes and treat the text inside the curly braces as Go expressions instead of pure text.
There is no extra runtime cost since the functionality is identical to the existing way.
Since the new syntax is merely cosmetic a transpiler could simply convert it to the old one and compile the result with the current compiler similar to how generic code was handled previously.
https://github.com/Cookie04DE/gof
The text was updated successfully, but these errors were encountered: