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: string interpolation #34174

Open
sirkon opened this issue Sep 8, 2019 · 19 comments
Open

proposal: string interpolation #34174

sirkon opened this issue Sep 8, 2019 · 19 comments

Comments

@sirkon
Copy link

@sirkon sirkon commented Sep 8, 2019

Introduction

For ones who don't know what it is:

Swift

let multiplier = 3
let message = "\(multiplier) times 2.5 is \(Double(multiplier) * 2.5)"
// message is "3 times 2.5 is 7.5"

Kotlin

var age = 21

println("My Age Is: $age")

C#

string name = "Mark";
var date = DateTime.Now;

Console.WriteLine($"Hello, {name}! Today is {date.DayOfWeek}, it's {date:HH:mm} now.");

Reasoning of string interpolation vs old school formatting

I used to think it was a gimmick but it is not in fact. It is actually a way to provide type safety for string formatting. I mean compiler can expand interpolated strings into expressions and perform all kind of type checking needed.

Examples

variable := "var"
res := "123\{variable}321" // res := "123" + variable + "321"
return errors.New("opening config file: \\{err}") // return errors.New("opening config file: " + err.Error())
var status fmt.Stringer
…
msg := "exit status: \{status}" // msg := "exit status: " + status.String()
v := 123
res := "value = \{v}" // res := "value = " + someIntToStringConversionFunc(v)

Syntax proposed

  • Using $ or {} would be more convenient in my opinion, but we can't use them for compatibility reasons
  • Using Swift \(…) notation would be compatible but these \() are a bit too stealthy

I guess {…} and \(…) can be combined into \{…}

So, the interpolation of variable variable into some string may look like

"<prefix>\{variable}<suffix>"

Formatting also has formatting options. It may look like

"<prefix>\{variable[:<options>]}<suffix>"

Examples of options

v := 123.45
fmt.Println("value=\{v:04.3}") // value=0123.450
v := "value"
fmt.Println("value='\{v:a50}'") // value='<45 spaces>value'

etc

Conversions

There should be conversions and formatting support for built in types and for types implementing error and fmt.Stringer. Support for types implementing

type Formatter interface {
    Format(format string) string
}

can be introduced later to deal with interpolation options

Pros and cons over traditional formatting

Pros

  • Type safety
  • Performance (depends on the compiler)
  • Custom formatting options support for user defined types

Cons

  • Complication of a compiler
  • Less formatting methods supported (no %v (?), %T, etc)
@agnivade agnivade changed the title String interpolation may be? proposal: string interpolation Sep 8, 2019
@gopherbot gopherbot added this to the Proposal milestone Sep 8, 2019
@gopherbot gopherbot added the Proposal label Sep 8, 2019
@latitov

This comment has been minimized.

Copy link

@latitov latitov commented Sep 9, 2019

Why can't we use just ${...}? Swift already has one syntax, JavaScript and Kotlin another, C# yet another, also Perl... Why invent one more variation? Wouldn't it be better to stick to the one most readable and already existing?

@sirkon

This comment has been minimized.

Copy link
Author

@sirkon sirkon commented Sep 9, 2019

Why can't we use just ${...}? Swift already has one syntax, JavaScript and Kotlin another, C# yet another, also Perl... Why invent one more variation? Wouldn't it be better to stick to the one most readable and already existing?

Because we may have existing strings with ${…}. I do, for instance.

And \{ is not allowed right now.

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Sep 10, 2019

This doesn't seem to have a big advantage over calling fmt.Sprintf.

@sirkon

This comment has been minimized.

Copy link
Author

@sirkon sirkon commented Sep 10, 2019

This doesn't seem to have a big advantage over calling fmt.Sprintf.

Yes, it doesn’t besides full type safety, better performance and ease of use. Formatting done right for a language with static typing.

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Sep 10, 2019

As far as I can tell this proposal isn't any more type safe than using fmt.Sprintf with %v. It's essentially identical with regard to type safety. I agree that a careful implementation could have better performance in many cases. I'm agnostic on ease of use.

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Sep 10, 2019

In order to implement this we would have to write, in the language spec, the exact formatting to use for all types. We would have to decide and document how to format a slice, an array, a map. An interface. A channel. This would be a significant addition to the language spec.

@latitov

This comment has been minimized.

Copy link

@latitov latitov commented Sep 11, 2019

I think it's one of those questions where both ways have the right to exist, and it's just up to the decision makers to decide. In Go, the decision has been already made, quite a long time ago, and it works, and it's idiomatic. That's fmt.Sprintf with %v.

Historically, there are languages where interpolation inside a string has been present from the very beginning. Notably it's Perl. That's one of the reasons why Perl became so popular, because it was a super-convenient compared to sprintf() in C et al. And the %v wasn't invented then yet. And then there are languages where the interpolation was present, kind of, but was inconvenient syntactically, think of "text" + v1 + "text". JavaScript then introduced backtick-quoted literals, which are multi-line, and support interpolation of arbitrary expressions inside ${...}, which was huge improvement compared to "text" + v1 + "text". The Go too has backtick multi-line literals, but without ${...}. Who copied whom I don't know.

I don't agree with @ianlancetaylor that support of this will need substantial effort. In fact, fmt.Sprintf with %v does exactly this, doesn't it? It looks for me like a different syntax wrapper to exactly the same thing under the hood. Am I right?

But I do agree with @ianlancetaylor that using fmt.Sprintf with %v is as convenient. It's also exactly the same in length on the screen, and, what is IMHO very important - is already idiomatic for Go. It kind of makes Go Go. If we copy-implement every other feature from every other language, then it will no longer be Go, but every other language.

There's one more thing. From my long experience with Perl, where string interpolation was present from the very beginning, I can say that it's not that perfect. There's a problem with that. For simple and trivial programs, and probably for 90% of all programs, string interpolation of variables works just fine. But then, once in a while, you get a var=1.99999999999, and want to print it as 1.99, and you can't do that with standard string interpolation. You either need to do some conversion beforehand, or... Look into the docs, and re-learn long forgotten sprintf() syntax. Here is the problem - using string interpolation allows you to forget how to use sprintf()-like syntax, and probably it's very existence. And then when it's needed - you spend too much time and effort to do simplest things. I am talking in Perl context, and it was a decision of language designer(s) to do so.

But in Go, a different decision has been made. The fmt.Sprintf with %v is already here, it's as convenient, as short as interpolated strings, and it's idiomatic, in that it's in the docs, in examples, everywhere. And it doesn't suffer from the problem of eventually forgetting how to print 1.99999999999 as 1.99.

Introducing proposed syntax will make Go a little more like Swift and/or more like JavaScript, and some may like that. But I think this particular syntax will not make it better, if not a little worse.

I think that existing way to print things should stay as it is. And if someone needs more - then there are templates for that.

@mvdan

This comment has been minimized.

Copy link
Member

@mvdan mvdan commented Sep 11, 2019

If part of the argument here is safety at compile time, I don't agree that's a compelling argument; go vet, and by extension go test, have been flagging incorrect uses of fmt.Sprintf for a while.

It's also possible to optimize for performance today via go generate, if you really wish to do so. It's a tradeoff that's not worth it most of the time. I feel like the same applies to greatly expanding the spec; the tradeoff is generally not worth it.

@kaey

This comment has been minimized.

Copy link

@kaey kaey commented Sep 11, 2019

Allowing function calls inside interpolated strings would be unfortunate - too easy to miss.
Not allowing them is another unnecessary special case.

@conilas

This comment has been minimized.

Copy link

@conilas conilas commented Sep 13, 2019

If part of the argument here is safety at compile time, I don't agree that's a compelling argument; go vet, and by extension go test, have been flagging incorrect uses of fmt.Sprintf for a while. It's also possible to optimize for performance today via go generate, if you really wish to do so. It's a tradeoff that's not worth it most of the time. I feel like the same applies to greatly expanding the spec; the tradeoff is generally not worth it.

But type safeness should be guaranteed by the compiler, not by tooling. This is semantics; it is like saying that there should be a tool for verifying where you forgot to null check instead of having optionals or explicitly declared nullable values.

Apart from that - the only way for this to be safe is with dependent types. String interpolation is just more syntatic sugar for the same stuff as fmt.Sprintf and, although I'm all in favor of some good sugar, the whole go community seems not to be.

@bradfitz

This comment has been minimized.

Copy link
Member

@bradfitz bradfitz commented Sep 17, 2019

Or perhaps something like modifying the language to just work better with fmt.Printf & friends.

Like if fmt supported something like %(foo)v or %(bar)q, then say that if a string literal containing %(<ident>) is used in a call to a variadic func/method, then all the referenced symbols are appended to the variadic list automatically.

e.g. this code:

name := "foo"
age := 123
fmt.Printf("The gopher %(name)v is %(age)2.1f days old.")

would really compile to:

name := "foo"
age := 123
fmt.Printf("The gopher %(name)v is %(age)2.1f days old.", name, age)

And fmt could just skip over the unnecessary (name) and (age) bits.

That's a pretty special case language change, though.

@alvaroloes

This comment has been minimized.

Copy link

@alvaroloes alvaroloes commented Sep 19, 2019

For me, type-checking as a reason to include string interpolation in the language is not that compelling. There is a more important reason, which is not depending on the order in which you write the variables in fmt.Printf.

Let's take one of the examples from the proposal description and write it in Go with and without string interpolation:

  • Without string interpolation (current Go)
name := "Mark"
date := time.Now()

fmt.Printf("Hello, %v! Today is %v, it's %02v:%02v now.", name, date.Weekday(), date.Hour(), date.Minute())
  • Equivalent functionality with string interpolation (and mixing the @bradfitz comment, needed to express the formating options)
name := "Mark"
date := time.Now()

fmt.Printf("Hello, %{name}! Today is %{date.Weekday()}, it's %02{date.Hour()}:%02{date.Minute()} now.")

For me, the second version is easier to read and modify and less error-prone, as we do not depend on the order in which we write the variables.

When reading the first version, you need to re-read the line several times to check it is correct, moving your eyes back and forth to create a mental mapping between the position of a "%v" and the place where you need to put the variable.

I've been fixing bugs in several applications (not written in Go, but with the same issue) where the database queries had been written with a lot of '?' instead of named parameters (SELECT * FROM ? WHERE ? = ? AND ? != false ...) and further modifications (even inadvertently during a git merge) flipped the order of two variables 😩

So, for a language whose goal is to ease maintainability in the long term, I think this reason makes it worth it to consider having string interpolation

Regarding the complexity of this: I don't know the internals of Go compiler, so take my opinion with a grain of salt, but Couldn't it just translate the second version showed in the above example to the first one?

@bradfitz

This comment has been minimized.

Copy link
Member

@bradfitz bradfitz commented Sep 25, 2019

@ianlancetaylor pointed out that my sketch above (#34174 (comment)) isn't strictly backwards compatible, as there might be rare programs where this would change their behavior.

A backwards compatible variation would be to add a new type of "formatted string literal", prefixed by, say, an f:

e.g. this code:

name := "foo"
age := 123
fmt.Printf(f"The gopher %(name)v is %(age)2.1f days old.")

would really compile to:

name := "foo"
age := 123
fmt.Printf(f"The gopher %(name)v is %(age)2.1f days old.", name, age)

But then the double f (one in Printf followed by the f before the new type of string literal) would be stuttery.

@alphabettispaghetti

This comment has been minimized.

Copy link

@alphabettispaghetti alphabettispaghetti commented Oct 10, 2019

I also don't understand the inner workings of the compiler, so I (perhaps foolishly) also assume that this could be implemented in the compiler, so that something like
fmt.printf("Hello %s{name}. You are %d{age}")

would compile to its equivalent current formulation.

String interpolation has the obvious benefit of higher readability (A core design decision of Go) and also scales better as the strings that one deals with become longer and more complicated (another core design decision of Go). Please notice also that using {age} gives the string context that it otherwise wouldn't have if you were only skim reading the string (and of course ignoring the type that was specified), the string could have ended "You are tall", "You are [at XXX location]", "You are working too hard" and unless you put in the mental energy to map the format method to each instance of interpolation it isn't immediately obvious what should go there. By removing this (admittedly small) mental hurdle the programmer can focus on the logic rather than the code.

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Oct 10, 2019

The compiler implements the language spec. The language spec currently says nothing at all about the fmt package. It doesn't have to. You can write large Go programs that don't use the fmt package at all. Adding the fmt package documentation to the language spec would make it noticeably larger, which is another way of saying that it makes the language that much more complex.

That doesn't make this proposal impossible to adopt, but it is a large cost, and we need a large benefit to outweigh that cost.

Or we need a way to discuss string interpolation without involving the fmt package. This is pretty clear for values of string type, or even []byte type, but much less clear for values of other types.

@alanfo

This comment has been minimized.

Copy link

@alanfo alanfo commented Oct 11, 2019

I'm not in favor of this proposal partly because of what @ianlancetaylor said above and partly because, when you try to interpolate complex expressions with formatting options, any readability advantage tends to go out of the window.

Also it's sometimes forgotten that the ability to include variadic arguments in the fmt.Print (and Println) family of functions already enables a form of interpolation. We can easily reproduce some of the examples quoted earlier with the following code which, to my mind, is just as readable:

multiplier := 3
message := fmt.Sprint(multiplier, " times 2.5 is ", float64(multiplier) * 2.5)

age := 21
fmt.Println("My age is:", age)

name := "Mark"
date := time.Now()
fmt.Print("Hello, ", name, "! Today is ", date.Weekday(), ", it's ", date.String()[11:16], " now.\n")

name = "foo"
days := 12.312
fmt.Print("The gopher ", name, " is ", fmt.Sprintf("%2.1f", days), " days old\n.")
@latitov

This comment has been minimized.

Copy link

@latitov latitov commented Oct 16, 2019

Another reason to still add and have it in the language: #34403 (comment)

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Oct 29, 2019

We find @alanfo 's comments in #34174 (comment) to be convincing: you can use fmt.Sprint to do a simple sort of string interpolation. The syntax is perhaps less convenient, but any approach at this would require a special marker for variables to be interpolated in any case. And, as noted, this permits arbitrary formatting of the values to be interpolated.

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Nov 26, 2019

As noted above there is an existing way to approximately do this that even allows for formatting of the individual variables. Therefore, this is a likely decline. Leaving open for four weeks for final comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.