Skip to content
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

Closed
ianlancetaylor opened this issue Jan 5, 2023 · 126 comments
Closed

proposal: spec: add simple string interpolation similar to Swift #57616

ianlancetaylor opened this issue Jan 5, 2023 · 126 comments
Labels
Milestone

Comments

@ianlancetaylor
Copy link
Contributor

ianlancetaylor commented Jan 5, 2023

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.

  1. If the expression has a string type, it is used as is.
  2. If the expression is a constant expression it is converted (as described below) to an untyped string constant.
  3. Otherwise the expression is converted (as described below) to a non-constant value of type string.

The original string "prefix\(expression)suffix" is then replaced by the expression ("prefix" + expressionAsString + "suffix").

Examples

    // Prints something like "Ken Thompson is 79 years old".
    fmt.Println("\(person.Name()) is \(person.Age()) years old")
    // Prints something like "The time is 2023-01-04 16:22:01.204034106 -0800 PST".
    // Note: calls the String method of time.Time.
    fmt.Println("The time is \(time.Now().Round(0))")

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 and I as with any float, and producing R+Ii or R-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.

@johanbrandhorst

This comment was marked as resolved.

@icholy
Copy link

icholy commented Jan 5, 2023

Would interpolation be permitted in const strings?

@ianlancetaylor

This comment was marked as resolved.

@ianlancetaylor
Copy link
Contributor Author

ianlancetaylor commented Jan 5, 2023

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

@cespare
Copy link
Contributor

cespare commented Jan 5, 2023

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.

@ianlancetaylor
Copy link
Contributor Author

ianlancetaylor commented Jan 5, 2023

@cespare Interesting point. We would want to say that the result of string interpolation is a non-constant value of type string. It would not be an untyped string constant. As such, it could not be passed to a function like safesql.New.

I added that to the proposal.

@apparentlymart
Copy link

My initial instinct was to be concerned about the lack of ability to customize the formatting of the built-in types, but:

  • You already addressed that in the proposal and showed that it's possible to call functions like strconv.FormatFloat and fmt.Sprintf inside the interpolation sequences, if needed.

  • In my day job I work on HashiCorp Terraform whose own language makes essentially the same tradeoff: interpolation like "blah-${foo}" works just like string concatenation but you can write "blah-${format("%05d", foo)}" to get formatted numbers. (format is essentially equivalent to Go's fmt.Sprintf).

    That compromise seems to have paid off in practice. A few times I've seen folks ask how to get formatted numbers in string interpolation, perhaps betraying that it isn't immediately intuitive that it's possible to use arbitrary functions in there, but folks seem to understand and accept it as a suitable answer once informed about it.

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 + operator when applied to a string and a non-string to follow the same conversion rules for consistency, but I don't think it's a deal-breaker.

Overall I think I like this and I expect it'll improve the readability of lots of examples of fmt.Sprintf in codebases I maintain, so 👍 (also recorded on the original comment, for ease of vote-counting) on reflection, despite my initial concerns.

@griesemer
Copy link
Contributor

@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 string type. But I don't understand the comments #57616 (comment) and #57616 (comment): If an interpolated string does only contain constant expressions (which by definition must be evaluated at compile-time) why would it be a problem if the resulting string is constant (of type string, or maybe even untyped string)? Such a string cannot ever change its value at run-time.

(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, var digits[len("\(1 << 63)")]byte // buffer for uint64 to string conversion.)

@griesemer
Copy link
Contributor

griesemer commented Jan 5, 2023

@ianlancetaylor Can we make "s1\(x1)s2\(x2) ... sn\(xn)" behave exactly like "s1" + F(x1) + "s2" + F(x2) + ... + F(xn), where F is the spec-defined string conversion function for the permissible expression types? (I'm not sure if that's the proposals' intent already, or maybe I misunderstand @apparentlymart 's #57616 (comment) (his reference to the "smaller concern")).

If we do this, it's a) easy to specify the exact behavior (we just need to explain F in the spec), and b) we don't need some new special representation for interpolated strings in the implementation (syntax tree). The parser can trivially transform the string into a binary expression (of + operations) as needed.

As an aside, if F is defined to return a constant for constant expressions (and string or untyped string as desired), the resulting constant-ness and type of the interpolated string follows directly from existing rules, ensuring that we don't bake it some non-orthogonal inconsistency.

@ianlancetaylor
Copy link
Contributor Author

@griesemer Thanks, changed as you suggest.

@robpike
Copy link
Contributor

robpike commented Jan 5, 2023

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

@Dentrax
Copy link

Dentrax commented Jan 5, 2023

What if function returns multiple values: func foo() (bar, baz, error), the following should resulting in compile error?

fmt.Println("\(foo())")
# compile error
# or print "true my-string <nil>" respectively

For a struct{} or any case, should compiler apply %+v or something?

P.S: I'm not familiar with Swift.

@griesemer
Copy link
Contributor

@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 fmt.Printf-equivalent calls for error messages, many of which take multiple (> 2) verb arguments, all of which support the Stringer interface. For instance

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 \( in a (double-quoted) string ends the string temporarily, and a new string starts with the (appropriate) ); and the string eventually needs to end in ". That is a double-quoted string containing \(x) really is two strings in this view. (Whether this is a useful view I don't know.)

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.

@Merovius
Copy link
Contributor

Merovius commented Jan 5, 2023

As an observation: Under the proposal as currently written, adding a String() string method to a defined type with underlying type being "simple" would become a breaking change. For example, this code

type X int
const (
    x1 X = iota
    x2
    x3
)
var y [len("\(x1)")]byte

would become invalid, after adding a String() string method to X. It's a contrived case, but I think it's a surprising pitfall. Interestingly, it arises because we want to avoid the pitfall of breaking safesql usecase.

@Merovius
Copy link
Contributor

Merovius commented Jan 5, 2023

FWIW I'm leaning against this, but not strongly. I currently tend to use person.Name()+" is "+strconv.Itoa(person.Age())+" years old" for these simple interpolations. The mechanism proposed is a bit nicer, but not by that much. Overall, I feel a bit like @robpike, that this is going a tad too far and doesn't feel like it fits the style of Go.

@mibk
Copy link
Contributor

mibk commented Jan 5, 2023

Were other characters beside \( also considered?
Personally, I find braces more clear, especially since () might appear quite often in the interpolated expression.

fmt.Println("\{person.Name()} is \{person.Age()} years old")
fmt.Println("The time is \{time.Now().Round(0)}")

error(pos, "arguments to copy \{x} and \{&y} have different element types \({dst.elem} and \{src.elem}")

@beoran
Copy link

beoran commented Jan 5, 2023

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

@DmitriyMV
Copy link

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.

@gnojus

This comment was marked as resolved.

@aarzilli
Copy link
Contributor

aarzilli commented Jan 5, 2023

Why don't people (including me) use fmt.Println and fmt.Sprint more? The examples:

    fmt.Println("\(person.Name()) is \(person.Age()) years old")
    fmt.Println("The time is \(time.Now().Round(0))")

are equivalent to:

fmt.Println(person.Name(), " is ", person.Age(), " years old")
fmt.Println("The time is ", time.Now().Round(0))

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?

@fzipp
Copy link
Contributor

fzipp commented Jan 5, 2023

@aarzilli Println adds spaces. But yes, it is even shorter:

fmt.Println("\(person.Name()) is \(person.Age()) years old")
fmt.Println(person.Name(), "is", person.Age(), "years old")

fmt.Println("The time is \(time.Now().Round(0))")
fmt.Println("The time is", time.Now().Round(0))

@beoran
Copy link

beoran commented Jan 5, 2023

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

@DmitriyMV
Copy link

@beoran FWIW fmt.Sprint and fmt.Print do not add space and newlines.

@beoran
Copy link

beoran commented Jan 5, 2023

@DmitriyMV thanks, but now I am wondering why I never used those functions. This might actually be a documentation problem.

@aarzilli
Copy link
Contributor

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.

@griesemer
Copy link
Contributor

Just to reiterate: if fmt.Sprint is not what one wants for whatever reason, it's trivial to write a function that does exactly what one wants.

@ianlancetaylor
Copy link
Contributor Author

@robpike It's still something people need to remember. For example it does not insert a space between two arguments of type string, but it does insert a space between two arguments that are not strings but that have a String() string method. I agree that it usually does the right thing, but it can still trip people up.

@DeedleFake
Copy link

DeedleFake commented Jan 23, 2023

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.

@griesemer
Copy link
Contributor

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 Formatter (or G is you like) and it's 6 chars longer than if you had string interpolation. If you use the G function, you don't need to remember anything about extra blanks. And, depending on use case, the F approach is more compact (and arguably more readable) than string interpolation.

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 fmt.Sprint (and custom functions) do suffer from the problem that one needs to match up formatting verbs with arguments. Why not use what we have?

In short, I have yet to hear a convincing reason as to why this justifies a language change.

@beoran
Copy link

beoran commented Jan 23, 2023

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

@beoran
Copy link

beoran commented Jan 23, 2023

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

// Like F but no extra blanks between non-string arguments.
func G(args ...any) string {
	var buf strings.Builder
	for _, arg := range args {
		fmt.Fprint(&buf, arg)
	}
	return buf.String()
}

@griesemer
Copy link
Contributor

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.

@raff
Copy link

raff commented Jan 24, 2023 via email

@runeimp
Copy link

runeimp commented Jan 27, 2023

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

@ianlancetaylor
Copy link
Contributor Author

Based on the discussion above, this does not add enough to the language over fmt.Sprint to justify adding. Therefore, this is a likely decline. Leaving open for four weeks for final comments.

@Pat3ickI
Copy link

Pat3ickI commented Feb 7, 2023

Why don’t we have a function specific for this kind of use case why must it be fmt.Println() or fmt.Sprintf() ?

@aarzilli
Copy link
Contributor

aarzilli commented Feb 7, 2023

@Patrickmitech what would the function do differently than fmt.Sprint? (note fmt.Sprint not fmt.Sprintf)

@Pat3ickI
Copy link

Pat3ickI commented Feb 7, 2023

@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 reflect

In Addition: A function like

func AFunction(w io.Writer, str string) {}

is ok
The number of fmt.Println() or fmt.Sprint() in codebase is huge adding a more generic feature to it kinda feel a bit of an overkill

@Merovius
Copy link
Contributor

Merovius commented Feb 7, 2023

@Patrickmitech Note that there is no intrinsic reason we couldn't teach the compiler about fmt.Sprint to avoid reflection in the same cases. Whether we want to do that is a different question, but I don't think that alone would be a reason to add a new predeclared function. Your AFunction exists in the form of io.WriteString and/or fmt.Fprint.

@runeimp
Copy link

runeimp commented Feb 14, 2023

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 sprintf were C, C++, Java, Rust, and Zig. Three ancient languages (no disrespect but C is possibly older than me and I've been a legal adult for quite some time), one (Java) which I understand would be hard pressed to be able to add proper string interpolation, and two new ones. The other 20 or more languages I looked at already had or added in the last decade or so full expression interpolation for the vast majority of them or variable expansion at the minimum for some. Those numbers are not a coincidence. It's not about adding what is "hot". It's about adding features that many, many others find useful.

@Merovius
Copy link
Contributor

@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 String() string method might be a breaking change, as it changes the const-ness of interpolating strings. The alternative would be to not call String() in interpolated strings, or to never make them constants, both of which make them significantly less useful.

Or I mentioned here, that interpolated string literals either require more escaping than fmt.Sprint, or need to become more complex to parse (and might not be backwards compatible).

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 fmt.Sprint is actually pretty concrete. It's "here is how to do it in string interpolation, here is how to do it without, these two don't seem appreciably dissimilar".

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.

@Merovius
Copy link
Contributor

Here is a concrete argument in favor of this proposal: Type checking. fmt.Sprint takes any, which arguably is too broad. It includes struct types without a String method, for example. Or functions. Arguably, for a user-presented string, you might not want that - you might want them to always get a well-defined, helpful output.

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 fmt.Stringer can't appear as part of a union element. They also would require the arguments to be homogeneous, which obviously is counterproductive. If we allowed to use general interfaces as sum types, that would help, but still not allow us to use fmt.Stringer as part of the union.

I'm not sure I find this argument super persuasive and you could also make it the other way around, but it's concrete.

@runeimp
Copy link

runeimp commented Feb 15, 2023

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

  1. The String() string issue while interesting it is not compelling to me as I can't see that ever truly being a problem. Someone who wants to read the length of any string, const or interpolated, likely has a reason for it and is unlikely to cause a problem. It is either fine or no worse than checking an exported variable that might have gotten clobbered by something external to the package. Maybe I'm missing some other aspect of your example that is too subtle for me to realize in my very tired state. If there is some concern that is some aspect of that which is buried deep in the Go internals please let me know.
  2. Quoting Hell this is tricky to explain as I just don't have the brain power to come up with a better set of examples at the moment. I do appreciate you likely never have had experienced the quoting issues to the levels of horror I've had to deal with many times. I believe someone else pointed out issues intermixing JavaScript in HTML to which I agree completely but can't find the comment to reference. Here are some light examples to consider.
    pronoun := "She"
    speaker := "Charlotte"
    message := ""
    
    // A few static examples
    
    // String wrapped in double-quotes
    message = "... and I quote, \"She said 'They who do not dare, fail!' in low voice.\" when speaking of Charlotte's antics"
    // A mild example of what could be a hot mess in more extreme cases of which I've seen many
    
    // String wrapped in backticks
    message = `... and I quote, "She said 'They who do not dare, fail!' in low voice." when speaking of Charlotte's antics`
    // THANK YOU GO! for using backticks for easy mixing of single and double quotes within the same string.
    
    // Dynamic Examples
    
    // fmt.Sprintf: Messy, though not too bad. But if the string had more
    // escapes and placeholders to deal with it could easily become a
    // hot mess to deal with very quickly
    message = fmt.Sprintf("... and I quote, \"%s said 'They who do not dare, fail!' in low voice.\" when speaking of %s's antics", pronoun, speaker)
    
    // fmt.Sprint or Sprintln: In my experience building strings with smaller
    // chunks that have varying interspersed quotes is quite error prone and
    // no better than concatenation or strings.Builder though Builder would
    // most likely be the fastest performer
    message = fmt.Sprint("... and I quote, \"", pronoun, " said 'They who do not dare, fail!' in low voice.\" when speaking of ", speaker, "'s antics")
    
    // C# style string interpolation and backticks
    message = $`... and I quote, "{pronoun} said 'They who do not dare, fail!' in low voice." when speaking of {speaker}'s antics`
    /*
    	Painfully easy to get right. Do I even need to explain how to use this
    	formatting even if the example didn't have the variables at the top?
    
    	- The $ (for C#) or F (for Python) let the compiler/interpreter know
    	  they are dealing with string interpolation right off regardless of
    	  the quotes used.
    	- The variable reference or expressions in curly braces represent
    	  code. Everyone expects stuff wrapped in curly braces to be code of
    	  some sort. Curly braces are valid English language delimiter but
    	  are rarely used and as they already represent code of some sort
    	  escaping them is no great shock as to intent.
    */
    
    // C# style string interpolation and double-quotes
    message = $"... and I quote, \"{pronoun} said 'They who do not dare, fail!' in low voice.\" when speaking of {speaker}'s antics"
    // A little messy but still painfully easy to get right
  3. Complex Parsing: Maybe? There has been some surface level speculation for sure. Were there any deep dives into the possibilities I missed? Ideally any new feature would take minimal effort to add and maintain. I have great faith in the Go team that, should they take this on, they will work out a possibly novel and foundational way of solving the implementation. I personally doubt Pandora's box is hidden in this feature as so many languages have had little issue implementing it to my knowledge. So not yet a strong argument to not implement in my opinion.
  4. fmt.Sprintf is already present: Yes, obviously but it does no provide the excellent programmer experience that good string interpolation does. This is not a good argument, in my opinion. As noted prior Go itself was not necessary for the world of programming. But it does provide a better programmer experience than the others in many cases. Especially when dealing with an Internet focused mindset. This feature improves the Go programmer experience and allows for better string building that is far less buggy. Just look at my examples above.
  5. Vagueness: It is all about cognitive load and how bullet proof it is to use. I've explained this, but no one cares or has the ability to imagine it if they've never experienced the differences. It is a paradigm shift that must be experienced to truly appreciate. I can confidently say using string interpolation improves my productivity with complex strings by a minimum of 50%. No one cares who's never experienced it as they haven't given it a fair chance yet so obviously "He's just exaggerating and in reality it only improves efficiency less than 5%".
  6. Polling (my thoughts): Has anyone taken a poll to see how many Go programmers miss string interpolation they've experience in almost any other languages? That would show the desire that already exists for the feature and likelihood of immediate adoption by that group of developers. If it's just 10% of people that have used string interpolation in other languages, then fine. Skip it. But if it is over 90% I don't think it can be ignored.
  7. Resistance (my thoughts): A thread were the majority of speakers have only ever used the handful of languages that don't have string interpolation will always be a very one sided discussion. Humans don't need microwaves, hot plates, ovens or stove tops to cook. Good luck trying to take those tools from a modern chef. But try to convince ancient cultures who have no modern technology currently to allow people to install a stove with associated gas or electric infrastructure into their grass huts when they have perfectly serviceable fire pits with spits and racks, you're going to see some serious push back.

@Merovius
Copy link
Contributor

@runeimp

  1. Sure, I also don't think it's necessarily a big deal.

  2. That example under this proposal:

    fmt.Sprint("... and I quote, \"", pronoun, " said 'They who do not dare, fail!' in low voice.\" when speaking of ", speaker, "'s antics")
    // vs.
    "... and I quote, \"\(pronoun) said 'They who do not dare, fail!' in low voice.\" when speaking of \(speaker)'s antics"
    

    Sure, I can see that this example is a bit more readable using string interpolation. It seems fairly contrived to show an issue, but there isn't anything wrong with that, of course - it's what I asked for.

    FWIW Sprintf at least has %q, which solved >90% of the cases using \" in a string literal for me or in reviews and it does so better. I think I've just rarely had to try to dynamically assemble prose in Go like this. And would probably, when I reached this level of complexity, reach for text/template instead of anything we discussed so far.

    But fair enough.

  3. To be clear, that is dependent on the question of whether or not we "fall out" of the string literal on an opening \(. That is, I specifically suggested that we could alleviate the quoting problem of string interpolation by making it valid to write "foo \(m["bar"]) baz", instead of having to write "foo \(m[\"bar\"]) baz".

    That's not necessarily "hard to parse", but it would make old parsers hiccup, as they'd parse it as <string literal> <identifier> <string literal> instead of just <string literal>.

    If we don't do this and accept the quoting issue of string interpolation, then this doesn't matter, of course.

  4. Note again, that this was about fmt.Sprint, not fmt.Sprintf. Please be judicious in your distinction between them, even if it is hard to tell at a glance whether the f is there or not.

    Whether or not string-interpolation provides a significantly better experience is exactly what we are debating. So ISTM whether or not that's a good argument depends heavily on first establishing that's the case. So no, I don't believe this is a bad argument at all. I think it provides a good point of comparison. For example, in 2. you used it as a good point of comparison to highlight a case where it works worse. And with 3., I highlighted a case where it works better.

    As to "just because a similarly powerful alternative exist doesn't mean we can't add more convenience", you are right. But taken on its own, to its logical extreme, this would mean that we add every feature to Go, as long as it makes the experience more convenient. That's not how Go (or really, any language) is developed, historically. It's a question of tradeoffs and for that we have to quantify these things, even if they are hard to quantify. So to decide whether or not to add a feature, we really have to evaluate how much more convenient it makes things, compared to the costs it adds. Even then, that won't be an objective measure, usually.

    That's why concrete comparisons to fmt.Sprint and other alternatives are important. They can give an impression of not just if something adds convenience, but how much convenience it adds.

    In that spirit, again thanks for your example in 2.

  5. I don't know what else to say to this. ITSM to feel ignored in a sense that I can't really appreciate - from what I can tell, people have engaged with you on this and nobody has ignored what you are saying. I'm not trying to invalidate your experience, I just also don't think it is true to say that nobody cares.

  6. Go is generally not developed by polls. They can inform what is considered, but just because many people say they want a specific thing, does not mean it's necessarily added. Especially given that polls tend to be fairly non-specific.

    As an example, consider error handling. It has consistently ranked high in polls and surveys as a problem area. But, multiple attempts at improving it have then also been roundly rejected by the community when they actually got proposed.

    Sometimes it's easy to say something in a poll which then turns out to have unexpected problems when looked at concretely. That's what these issues and proposal discussions are for - looking at a concrete way to add a feature and whether that is worth it.

  7. See 4 and 5. FTR I don't think that nativist analogy actually works - I think if you gave modern stoves to ancient people, they'd be amazed by it, but what's the point in speculation - and I also think it is pretty patronizing as a comparison for this discussion. The people you are talking to aren't "ancient relics who just haven't experienced the wonders of modern string interpolation". Please do not make this comparison, but treat everyone in this discussion as equally experienced and invested in making Go a better language.

@runeimp
Copy link

runeimp commented Feb 16, 2023

@Merovius my responses

  1. Agree
  2. Yes, %q is great. Thank you Go!
  3. Cool
  4. My bad. The post took several hours to write and I lost track with my train of thought a bit. I meant to start that point off with something like

    fmt.Sprint is problematic as illustrated in point 2. But as fmt.Sprintf is technically string interpolation (a string with placeholders that are replaced or expanded) ...

  5. Granted "no one cares", etc. was probably a poor choice of words but I wanted to expand beyond "It is all about cognitive load and how bullet proof it is to use." and ended up just illustrating my frustration with this whole process. Though I do feel the frustration is valid.
  6. I'm well aware and I'm not suggesting polls should automatically decide anything. If Go did do that poll and your findings are along the lines of 60% of your user base had prior exposer to string interpolation and over 90% of them really wanted it in the language then when the decision is made for or against both the team and community it is well informed about the actual context of proposition.
  7. I'm patronizing no one. If I offended anyone please accept my apology. This was an analogy that I thought might help get the point across regarding how difficult this discussion is due to the issues of trying to explain something as subtle as the value of string interpolation.

@slycrel
Copy link

slycrel commented Feb 25, 2023

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:

...
beoran: I have been using Go for a decade now and I didn't realize what Sprint could be used for. A built-in function would be more visible.
griesemer: No worries. I've been using Go for even longer and I also didn't see at first that fmt.Sprint is all we need in this case - it's not a very commonly used function because usually one tends to grab the formatting version (fmt.Sprintf). I especially like ianlancetaylor's suggestion - there's really no need for more in my mind (again, for simple formatting).
beoran: I agree that we can do it like ianlancetaylor says, but still as we both realized, the problem is visibility. No one uses Sprint for simple formatting, but we should. Just that no one thinks about using that function.
griesemer: Visibility is better achieved with documentation and examples.
robpike: I don't understand the claim that no one uses Sprint for simple formatting. First, I do, so it's not no one, and there must be others. Second, that's all it does. And third, there's nothing else as easy to use that does it.
I'm not disagreeing, I'm saying I don't understand the claim. If you want to turn an int or a float into a string, what else would you do? Are you implicitly saying people use Sprintf but not Sprint?
...

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 + for strings and also %v in the fmt library.

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 fmt.Sprint() is the best function out of the many in the fmt package just isn't going to make my memorization list unless I'm solving that problem often. And even then, if another tool I already know does almost the same job, I'll use that instead. (see also + and fmtSprintf() here)

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.

@runeimp
Copy link

runeimp commented Feb 28, 2023

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);
}

@ianlancetaylor
Copy link
Contributor Author

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 fmt.Sprint). Our decision remains unchanged. Closing.

@ianlancetaylor ianlancetaylor closed this as not planned Won't fix, can't repro, duplicate, stale Mar 1, 2023
trajan0x added a commit to synapsecns/sanguine that referenced this issue Feb 26, 2024
@golang golang locked and limited conversation to collaborators Feb 29, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests