-
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: spec: add simple string interpolation similar to Swift #57616
Comments
This comment was marked as resolved.
This comment was marked as resolved.
Would interpolation be permitted in const strings? |
This comment was marked as resolved.
This comment was marked as resolved.
@icholy Without thinking about it too hard I would say yes, but that the expressions must be constant expressions. That would mean that they can't be function calls, only references to other constants. Not sure how useful that would be in practice. |
One effect of this change would be to invalidate packages like https://pkg.go.dev/github.com/google/go-safeweb/safesql which rely on string literals not containing arbitrary expressions evaluated at runtime. |
@cespare Interesting point. We would want to say that the result of string interpolation is a non-constant value of type I added that to the proposal. |
My initial instinct was to be concerned about the lack of ability to customize the formatting of the built-in types, but:
One smaller concern I have is that this proposal does not treat interpolation the same as string concatenation per the usual Go rules. Terraform gets away with treating this as string concatenation because there's an implicit conversion in its language from numbers and booleans to strings. It feels a little awkward that Go string interpolation would not follow the same rules as string concatenation, but not the end of the world. It might be interesting to consider the implications of changing the Overall I think I like this and I expect it'll improve the readability of lots of examples of |
@ianlancetaylor @cespare I am all for keeping it simple and for this proposal to make the result of an interpolated string be a non-constant value of (Again, it may not be worth the trouble even if's not causing security issues, but I can imagine some uses for constant interpolated strings. For instance, |
@ianlancetaylor Can we make If we do this, it's a) easy to specify the exact behavior (we just need to explain As an aside, if |
@griesemer Thanks, changed as you suggest. |
@ianlancetaylor The proposal, independent of its merits, does not explain the problem it's solving that can't be solved today, or why it is worth adding another way to format strings in a language that already has several (Printf, Println, Sprint, String method, Error method, templates, ...) In short, the proposal is underjustified and underdefined. I also don't understand when this evaluation happens. Is it at compile time? If so, that's a totally new idea and problem space for the compiler. If not, how can the compiler issue an error about evaluation? Maybe the definition is just unclear, as already mentioned. Speaking just for myself, this is the kind of idea that may seem attractive but doesn't feel like the style of the existing language. Instead it feels just the like the borrowing that it is. I also find it very odd to ask the compiler to break apart strings to look for evaluable expressions. The proposal, if I understand it - and I'm not quite sure I do - boils down to syntactic sugar too sweet for me. |
What if function returns multiple values:
For a P.S: I'm not familiar with Swift. |
@robpike Also speaking just for myself, there are many situations where the existing approach of format strings is error prone. For instance, the type checker (compiler, really) has > 200 errorf(pos, "arguments to copy %s and %s have different element types %s and %s", x, &y, dst.elem, src.elem) would become error(pos, "arguments to copy \(x) and \(&y) have different element types \(dst.elem) and \(src.elem)") There are many such cases. In this case, the 2nd version is more compact, easier to read in my mind, and less error-prone (it's easier to check the right argument is used in the right place). Another space where we often print multiple values is tests. I've seen many instances where we have confused argument order because the tests only print in case of an error and so the problem is easily overlooked. You are correct that this is syntactic sugar. In the simplest form, the compiler could replace the above call with something like error(pos, fmt.Sprintf("arguments to copy %s and %s have different element types %s and %s", x, &y, dst.elem, src.elem)) If we want constant strings for constant expressions, the compiler would need to generate the string values for those constant expressions. This is not really a problem because a) it already must compute the exact constant values, and b) it already knows how to create the exact string representations for constant values for the types which can have compile-time constants (the only types accepted in this proposal). Thus, another translation scheme could be (using the example above): error(pos, "arguments to copy " + x.String() + " and " + (&y).String() + " have different element types " + dst.elem.String() + " and " + src.elem.String()) I'm sympathetic to the notion that breaking apart strings seems odd (how does one grep for them?). But one could also look at these strings differently: a Just to be clear, I am neither strongly in favor nor against this proposal. But we have in Go made concessions for syntactic sugar when a feature is pervasive in use. The different forms for assignments come to mind. But also the various loop forms. And, I think it's a (perhaps sad) reality that much of modern computing is string processing. We have essentially one formatting mechanism (used the same way in many many functions), and it's not even part of the language proper, but defined by the "fmt" package. It's detail specification is non-trivial, either. Maybe a simple 90% solution, which I think this is, for an operation that is very common in many programs, deserves a second look. |
As an observation: Under the proposal as currently written, adding a type X int
const (
x1 X = iota
x2
x3
)
var y [len("\(x1)")]byte would become invalid, after adding a |
FWIW I'm leaning against this, but not strongly. I currently tend to use |
Were other characters beside
|
@robpike String interpolation is a feature of many programming languages because of the convenience it affords over Printf-like formatting. As others have said, string interpolation is also less error prone than Printf-like formatting. While I agree this is syntactic sugar, for many people coming from Python or Ruby, or Swift, of course, this proposal will make Go significantly more accessible. I also like that this proposal is based on prior art in another programming language, and is backwards compatible. |
The main justification, in my opinion, is that string interpolation is much less error prone than variadic arguments - it's easier to read if you pass a lot of arguments during string creation and you don't need to remember the context after you read the format string. It also solves rare but unpleasant bug of not passing enough variables. In the absence of "generic/template variadic arguments" I think its a next best thing. |
This comment was marked as resolved.
This comment was marked as resolved.
Why don't people (including me) use fmt.Println and fmt.Sprint more? The examples:
are equivalent to:
If it is because you can't control the format, neither can you with this interpolation. If it is bad PR, why? What does this syntax do that fmt.Sprint doesn't? |
@aarzilli Println adds spaces. But yes, it is even shorter:
|
@aarzilli That is a good question. The problem is that these function add spaces and or newlines. Perhaps a new function that doesn't do that and just concatenates it's arguments, calling String() if needed could be an alternative to this proposal. It could be a built in function or a new function in the fmt package. |
@beoran FWIW |
@DmitriyMV thanks, but now I am wondering why I never used those functions. This might actually be a documentation problem. |
I didn't notice until now that fmt.Println/fmt.Sprintln/fmt.Fprintln and fmt.Print/fmt.Sprint/fmt.Fprint have different opinions regarding the insertion of spaces. |
Just to reiterate: if |
@robpike It's still something people need to remember. For example it does not insert a space between two arguments of type |
And while it's a personal opinion, I find it really awkward to read trying to keep track of which quotes signal the start or end of a string literal and which commas separate arguments and which are in a string. It seems very error-prone and eye straining. Syntax highlighting can help, but even still it's less than ideal. fmt.Sprint("This ", a, " is not, I repeat ", italics("not"), ", easy to read, in my opinion.")
// vs.
"This \(a) is not, I repeat \(italics("not")), easy to read, in my opinion." Even with the function call, the second is far better. Ideal? Maybe not. But better. I was on the fence until I saw @griesemer's example, and now I think string interpolation is the way to go of these two options, even with the overlap. |
If you need a formatter that doesn't add the extra blanks between non-strings, I don't understand why writing the 7 lines of code it takes is a problem. If string interpolation is important (and not a one-off case) in a program, those 7 lines are easily justified. If string interpolation is all that's happening, perhaps a template package is more suitable. Your particular example can be written with a custom I understand that one might prefer one notation over the other - but one notation we have and the other we don't. Simple string interpolation doesn't provide any fundamental new capability to the language, nor does In short, I have yet to hear a convincing reason as to why this justifies a language change. |
@ianlancetaylor Yes and that's why I am arguing to add a function that does the right thing and ideally is builtin for better visibility and teachability. @DeedleFake fair enough, but the function form can be spread over several lines for readability. F("This ", a,
" is not, I repeat ", italics("not"),
", easy to read, in my opinion.") This looks good enough for me, and like this we don't have to add any new concepts to Go. |
@griesemer I agree that no language change is needed. But if this G function were available in fmt, or even better, also as a built in function, under whatever name seems best, this would be a lot more visible and easy to teach to new Go programmers, or to old ones who didn't realize this was possible, like me. If this idea should be a separate proposal, then I will file it.
|
Maybe there's room in Built-in functions are here to provide functionality that is not otherwise available in Go, and to a lesser extent, functionality that is so pervasive that we want to make it available w/o the need for an import (but this latter set of functions is much more questionable). For instance, A string formatter doesn't fit the bill in my mind. On top of that, converting to a string (which is what such a function would do) is an extremely complex operation that we don't typically want to hardwire into the language. |
For what is worth, I just tried out the `print` and `println` builtins, and
while `println` works like fmt.Println (i.e. it adds spaces between
arguments), `print` doesn't add any space, not even between numbers, so it
does pretty much what this "formatter" function should do (unfortunately it
can't really be used for regular cases, even if you want to just print the
arguments, since those built-ins print to stderr instead of stout).
…-- Raffaele
On Mon, Jan 23, 2023 at 3:24 PM Robert Griesemer ***@***.***> wrote:
Maybe there's room in fmt for a Sprint like function that doesn't add
blanks, but I don't have any particular opinion on that. If my code was in
frequent need for such a function, I'd write my local version which does
exactly what I want it to do.
Built-in functions are here to provide functionality that is not otherwise
available in Go, and to a lesser extent, functionality that is so pervasive
that we want to make it available w/o the need for an import (but this
latter set of functions is much more questionable). For instance, append
could now (with generics) be written in Go (but for the special string
handling). new is present for historical reasons. I could see a point
being made for min and max because they are so pervasive but we don't
have those either at the moment.
A string formatter doesn't fit the bill in my mind. On top of that,
converting to a string (which is what such a function would do) is an
extremely complex operation that we don't typically want to hardwire into
the language.
—
Reply to this email directly, view it on GitHub
<#57616 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAANESGYIXFV3PD2CAVGPGTWT4HMJANCNFSM6AAAAAATRLXBAU>
.
You are receiving this because you commented.Message ID:
***@***.***>
|
@Merovius I meant the statement as a colorful, lite hearted jab, to illustrate that we all have very different talents and perspectives. I'm sorry if you found it to be too much. It was meant to bring levity to what might seem a tense discussion. My apologies. |
Based on the discussion above, this does not add enough to the language over |
Why don’t we have a function specific for this kind of use case why must it be |
@Patrickmitech what would the function do differently than |
@aarzilli i see no benefit of this use if it still use Reflection, Go compiler can replace a piece of code with another go code ( or something like that) having the compiler to scan Go file and check if That specific function is called and replaces it with something that doesn’t use In Addition: A function like func AFunction(w io.Writer, str string) {} is ok |
@Patrickmitech Note that there is no intrinsic reason we couldn't teach the compiler about |
Am I wrong or is "the discussion above" actually showing there is little benefit or is it actually showing lots of comments by people that have never actually used string interpolation (or barely used at all), or are too set-in-their-ways saying it's not needed? "It ain't broke so don't fix it" is often a valid statement. And I genuinely appreciate how spartan Go is. But by any logic like that we certainly didn't need the Go language itself. There were many languages that existed before it that can effectively do the jobs that Go is designed towards. But I personally think, that is a poor argument. The design and usage of Go has many obvious and many, many often unseen or under-appreciated benefits. Just look at any "Go vs X-lang" article that was obviously written by a fan of the other language. I liked the concepts of the Go language I read about. But man did I have a different appreciation for it after a month or two of usage. It was absolutely worth the investment of time for me. Very similar to the feeling I had using String Interpolation after it was added to Python 3.6. I had already realized I miss it Python when transitioning to it from shell scripting and PHP development (which had always had variable expansion at minimum). But man was I happy! Of course I started my programming journey mainly on the front-end where strings are king. I then got into back-end where strings were very common, in web servers and frameworks. By the way, the only significant languages I could find that didn't have string interpolation based on some variation of |
@runeimp There also have been a couple of pretty concrete issues and downsides mentioned for adding string interpolation. For example I mentioned here that under this proposal, adding a Or I mentioned here, that interpolated string literals either require more escaping than Overall, from what I can tell, the argument against string interpolation have been fairly concrete. Even the argument that you can mechanically replace any interpolation by an almost identical call to And the only counter to all of these has been an IMO vague "if you'd used string interpolation more, you'd see its value". Which might very well be true. But if that value can't be verbalized and stands against fairly concrete arguments, it doesn't really help make a decision. We can hardly have everyone involved in making the decision use a different language for a year or so, to verify that they feel differently afterwards. So it would be genuinely helpful, if this argument in favor could be made a little bit more concrete. |
Here is a concrete argument in favor of this proposal: Type checking. In theory, generics can create a more restrictive type, but they don't allow us to express the set of types from the proposal, as I'm not sure I find this argument super persuasive and you could also make it the other way around, but it's concrete. |
@Merovius thank you for your prior response and the additional post with a rational for string interpolation. But here is my response to your prior post: I don't intend we would implement without any consideration. The arguments thus far against have been lacking in my opinion. I'm sorry for not engaging in them better. I really just didn't have the time and don't want to counter them much simply because they don't seem to carry enough weight to bother with the backlash I'll inevitably encounter and then feel obliged to further respond. But I'll give it a shot here.
|
|
@Merovius my responses
|
I've been thinking about this proposal for a while. And I think the one thing that hasn't been discussed to my satisfaction is thought processes, ease-of-use, and cognitive loads. So I'll put this out there, just in case it's not too little too late. There was a sub-discussion above in this direction I'd like to use for context:
There are a few things going on here. First, I think this is a technical usability issue. When I'm trying to pull a string together with variables, I'm generally trying to compose it as a sentence. I can "learn" to think about string composition as passing arguments to a function, but that's cognitive load. If I do it enough, this will even become intuitive (see robpike's confusion above on why everyone doesn't understand this. 😁) This point is acknowledged and supported by the existence of Second, the fmt library can be confusing. There are so many calls that sort of do the same thing but not quite. Many of the function names are similar and can be easy to mix up. For beginners and experts alike, as we have seen in the comments on this proposal. Third, memorization. Some people think this way, others do not. The further I go in my career as a dev the more contexts and languages I need to juggle. So I rely less and less on memorization -- Instead I rely on concepts and tooling to help me get there. Having to know that I see string interpolation as a helpful tool to combat cognitive load and ease of use. Thinking about what you're doing is better than thinking about how to get something done. It's easy to argue that this load is minimal in this case, and that it's not too much to ask to get more experience with Go. And... the load adds up. Asking someone to learn to think about code is one thing. Mixing that learned code thinking with our brain's language processing for string composition is going to be a higher cognitive load because you're mixing contexts. I personally think this language reasoning is why string interpolation is important to people. It's not about it being code-level easier or more efficient. It's about the reasoning space your brain is in when composing output. You're not really calling a function so much as composing a sequence. Mentally re-ordering that sequence to fit what the language asks as a variadic function call is (arguably) wasted mental effort. Few are likely to assume fmt.Sprint() even exists, let alone be sure about what it does without education. Most will understand what string interpolation is going to accomplish out of the box. Thanks Ian for helping foster this discussion. |
Sorry, I was mistaken. Rust does have a limited form of String Interpolation Announcing Rust 1.58.0 - Captured Identifiers in Format Strings | Rust Blog added in January of last year. fn main() {
let answer: u8 = 42;
println!("The answer to Life, the Universe, and Everything? {answer}");
let msg = format!("The answer to Life, the Universe, and Everything? {answer}");
println!("{}", msg);
} |
Thanks for the further comments. We understand that people familiar with other languages would like to see string interpolation in Go. However, simple string interpolation as proposed here doesn't pull its weight in the language, compared to what is already available in the standard library (specifically |
I propose a simple version of string interpolation similar to that in Swift. This is a simpler version of #34174 and #50554.
Proposal
In string literals we permit a new escape sequence
\(
. A valid\(
must be followed a expression and a trailing)
. Any unquoted(
in the expression must be balanced by a)
so that the compiler can locate the)
that terminates the\(
.The expression is evaluated in the context of the string literal. If it is not valid, the compiler reports an error. Multiple
\(
expressions in the string literal are evaluated from left to right.The expression must evaluate to a single value. If the type of that value has a method
String() string
, that method is called to get a string value. Otherwise the expression must be a string, integer, floating-point, complex, or boolean value. Values of other types will be reported as an error.string
.The original string
"prefix\(expression)suffix"
is then replaced by the expression("prefix" + expressionAsString + "suffix")
.Examples
Formatting
Integer values are converted to strings in the obvious way.
Floating-point values are converted to strings with the equivalent of
strconv.FormatFloat(f, 'g', prec, -1)
. This will need to be spelled out in the spec.Complex values are converted to strings by converting the real and imaginary parts to
R
andI
as with any float, and producingR+Ii
orR-Ii
.Boolean values are converted to either
"true"
or"false"
.Values of other types are not permitted.
Commentary
String interpolation is useful to avoid mistakes in complex formatting, as discussed in the earlier issues.
The earlier issues were caught up in complexity due to the desire to control formatting. However, 1) we already have
fmt.Sprintf
for arbitrary formatting; 2) even with simple string interpolation we can do formatting by writing code like"The average of \(len(values)) is \(strconv.FormatFloat(average, 'f', 32, 2))"
.The syntax
\(EXPR)
is borrowed from Swift.The type of the result ensures that interpolating variable values into a string can't produce an untyped string constant, but can only produce a non-constant string value. This will avoid implicit type conversions.
This proposal is backward compatible as currently using
\(
in a string causes a compilation error.The text was updated successfully, but these errors were encountered: