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

Closed
sirkon opened this issue Sep 8, 2019 · 112 comments
Closed

proposal: Go 2: string interpolation #34174

sirkon opened this issue Sep 8, 2019 · 112 comments
Labels
LanguageChange Suggested changes to the Go language Proposal Proposal-FinalCommentPeriod v2 An incompatible library change
Milestone

Comments

@sirkon
Copy link

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
@latitov
Copy link

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
Copy link
Author

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 ianlancetaylor added v2 An incompatible library change LanguageChange Suggested changes to the Go language labels Sep 10, 2019
@ianlancetaylor
Copy link
Contributor

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

@sirkon
Copy link
Author

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link

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
Copy link
Member

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
Copy link

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
Copy link

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
Copy link
Contributor

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
Copy link

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
Copy link
Contributor

@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
Copy link

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
Copy link
Contributor

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
Copy link

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
Copy link

latitov commented Oct 16, 2019

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

@ianlancetaylor
Copy link
Contributor

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
Copy link
Contributor

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.

@runeimp
Copy link

runeimp commented Dec 21, 2019

I'm regularly faced with building text blocks with well over 50 variables to insert. Over 70 in a few cases. This would be easy to maintain with Python's f-strings (similar to C# mentioned above). But I'm handling this in Go instead of Python for several reasons. The initial setup of fmt.Sprintf to manage these blocks is... ok. But god forbid I have to fix a mistake or modify the text in any way at all that involves moving or deleting %anything markers and their position related variables. And manually building maps to pass to template or setting up os.Expand is not a great option either. I'll take the speed (of setup) and ease of maintainability of f-strings over fmt.Sprintf any day of the week. And no, fmt.Sprint would not be hugely beneficial. Easier to setup than fmt.Sprintf in this case. But it loses much of it's meaning visually because your jumping in and out of strings. "My {age} is not {quality} in this discussion" doesn't jump in and out of strings the way that "My ", age, " is not ", quality, " in this discussion" does. Especially over the course of many tens of references. Moving text and references around is just copy and paste with f-strings. Deletions are just select and delete. Because your always within the string. This is not the case when using fmt.Sprint. It's very easy to accidentally (or necessarily) select non-string commas or double-quote string terminations and move them about breaking the formatting and requiring edits to massage it back into place. fmt.Sprint and fmt.Sprintf in these cases is far more time consuming and error prone than anything resembling f-strings.

@Curid
Copy link

Curid commented Jan 29, 2022

#34174 (comment)

func (dt DataType) String() string {
	var output string
	spf := func(format string, a ...interface{}) {
		output += fmt.Sprintf(format, a)
	}

	spf(`ThingID=%6d, ThingType=%q`, dt.ThingID, dt.ThingType)
	spf(`, PersonID=%d, PersonDisplayName=%q`, dt.PersonID, dt.PersonDisplayName)
	spf(`, PersonRoomNumber=%q, DateOfBirth=%s`, dt.PersonRoomNumber, dt.DateOfBirth)
	spf(`, Gender=%q, LastViewedBy=%q`, dt.Gender, dt.LastViewedBy)
	spf(`, LastViewDate=%s, SaleCodePrior=%q`, dt.LastViewDate, dt.SaleCodePrior)
	spf(`, SpecialCode=%q, Factory=%q`, dt.SpecialCode, dt.Factory)
	spf(`, Giver=%s, Manager=%q, ServiceDate=%s`, dt.Giver, dt.Manager, dt.ServiceDate)
	spf(`, SessionStart=%s, SessionEnd=%s`, dt.SessionStart, dt.SessionEnd)
	spf(`, SessionDuration=%d, HumanNature=%q`, dt.SessionDuration, dt.HumanNature)
	spf(`, VRCatalog=%v, AdditionTime=%d`, dt.VRCatalog, dt.AdditionTime)
	spf(`, MeteorMagicMuscle=%v, VRQuest=%q`, dt.MeteorMagicMuscle, dt.VRQuest)
	spf(`, SelfCare=%v, BypassTutorial=%q`, dt.SelfCare, dt.BypassTutorial)
	spf(`, MultipleViewsSameday=%v, MMMCode=%q`, dt.MultipleViewsSameday, dt.MMMCode)
	spf(`,%sMMMVoipCommunication=%q`, MMMCodeTail, dt.MMMVoipCommunication)
	spf(`,%sMMMCombatConditions=%q`, MMMVCTail, dt.MMMCombatConditions)
	spf(`,%sMMMSecurityReporting=%q`, MMMCCTail, dt.MMMSecurityReporting)
	spf(`,%sMMMLanguagesKnown=%q`, MMMSRTail, dt.MMMLanguagesKnown)
	spf(`,%sMMMDescription=%q`, MMMLKTail, dt.MMMDescription)
	spf(`,SaleCodeLatest=%q, HonoraryCode=%q`, dt.SaleCodeLatest, dt.HonoraryCode)
	spf(`, LegalCode=%q, CharacterDebuffs=%q`, dt.LegalCode, dt.CharacterDebuffs)
	spf(`,MentalDebuffs=%q, PhysicalDebuffs=%q`, dt.MentalDebuffs, dt.PhysicalDebuffs)
	spf(`,CharacterChallenges=%q`, dt.CharacterChallenges)
	spf(`,CharacterChallengesOther=%q`, dt.CharacterChallengesOther)
	spf(`,CharacterStresses=%q`, dt.CharacterStresses)
	spf(`,RelationshipGoals=%q`, dt.RelationshipGoals)
	spf(`, RelationshipGoalsOther=%q`, dt.RelationshipGoalsOther)
	spf(`,RelationshipLobsters=%q`, dt.RelationshipLobsters)
	spf(`,RelationshipLobstersOther=%q`, dt.RelationshipLobstersOther)
	spf(`,RelationshipLobsterGunslingerDoublePlus=%q`, dt.RelationshipLobsterGunslingerDoublePlus)
	spf(`,RelationshipLobsterGunslingerPlus=%q`, dt.RelationshipLobsterGunslingerPlus)
	spf(`,RelationshipLobsterGunslingerGains=%q`, dt.RelationshipLobsterGunslingerGains)
	spf(`,PersonAcceptsRecognition=%q`, dt.PersonAcceptsRecognition)
	spf(`,PersonAcceptsRecognitionGunslinger=%q`, dt.PersonAcceptsRecognitionGunslinger)
	spf(`,BenefitsFromChocolate=%v`, dt.BenefitsFromChocolate)
	spf(`, DinnerForLovelyWaterfall=%v`, dt.DinnerForLovelyWaterfall)
	spf(`, ModDinners=%q, ModDinnersOther=%q`, dt.ModDinners, dt.ModDinnersOther)
	spf(`,FlexibleHaystackList=%q`, dt.FlexibleHaystackList)
	spf(`, FlexibleHaystackOther=%q`, dt.FlexibleHaystackOther)
	spf(`,ModDiscorseSummary=%q`, dt.ModDiscorseSummary)
	spf(`, MentallySignedBy=%q, Overlord=%q`, dt.MentallySignedBy, dt.Overlord)
	spf(`, PersonID=%d,FactoryID=%q`, dt.PersonID, dt.FactoryID)
	spf(`, DeliveryDate=%s, ManagerID=%q`, dt.DeliveryDate, dt.ManagerID)
	spf(`, ThingReopened=%v`, dt.ThingReopened)

	return output
}

@eaglebush
Copy link

I don't know if this is good or not. I think if there is function to get all declared variables like PHP's get_defined_vars(), string interpolation could be implemented as a package and package authors could implement it themselves.

@ianlancetaylor
Copy link
Contributor

Unlike PHP, Go is a compiled language, so there is no support for anything similar to get_defined_vars.

@beoran
Copy link

beoran commented Mar 15, 2022

We could add a built in function definedVars() map[string]any that does roughly the same, though.

@ianlancetaylor
Copy link
Contributor

The PHP function returns all variables defined anywhere. That is infeasible in Go.

@beoran
Copy link

beoran commented Mar 16, 2022

Yes, but just the variables visible at that point from the current function could be possible. Maybe functionVars() would be a better name then.

@au-phiware
Copy link

au-phiware commented Apr 10, 2022

I think if there is function to get all declared variables like PHP's get_defined_vars(), string interpolation could be implemented as a package and package authors could implement it themselves.

Reading through this thread I had the same thought (less PHP).

Getting back to the XY problem, it seems to me that @runeimp biggest frustration is the distance between where a value is specified in a format string (be it fmt.Sprintf, os.Expand, or otherwise) and where a value is provided (I.e. positional parameter or map). I believe the argument that positional parameters introduce cognitive load upon the reader of the code is sound (personally I find it easy to write, but reading such code is a different matter). I'm not clear as to why using a map is difficult. My guess would be that building a set of local variables is more natural than building a map (or struct) or converting one or more structs into a single map (or struct). Then when it comes to producing a new string using your preferred formatting function all the values must restated as either positional arguments, map values or struct fields, which can be tedious and error prone. A (builtin) function that could capture the variables that are currently in scope obviates this construction of local variables into some other form.

My proposal would be thus:

// capture variables within the current scope and copy their values into I.
func capture(i interface{})

If i is of type map[string]interface{} then everything is copied into the map. If it is a struct address then only those variables that match the struct's fields are copied. Anything else results in a compile error.

E.g.

var m map[string]interface{}
s := struct { Foo int }{}
foo := 1
capture(m)
capture(&s)
println(foo, "==", m["foo"], "==", s.Foo)

More attention would need to be paid to handling package variables and mapping variable names to field names (use a special tag?).

I don't know if running user code in the compiler is desirable but a struct method could be called which provides access to local variables from a different scope. E.g.

// Capture is called by the compiler to populate a struct's fields. 
func (a *someStruct) Capture(resolve func(string) interface{}) error {
	a.Foo = resolve("foo").(int)
	return nil
}

Where any error or panic results in a compiler error!

@Roemer
Copy link

Roemer commented Aug 18, 2022

I really like C#s approach for string interpolation.

  1. Strings need a prefix ($"") so that they are interpolated -> Fully backwards compatible
  2. It provides good readability ($"Hello {name}")
  3. It has formatting embedded: {datetime:HH:mm} to show hours:minutes or {num:0.00} to show two decimals

There are a ton more features: csharp string interpolation formating

@sanan-go

This comment was marked as off-topic.

@latitov
Copy link

latitov commented Aug 26, 2022

Hi guys. It's now 3 (three!) years as I receive updates on this thread in my mailbox. So many, so different, ideas. Many of them are unexpected, many are very smart. So many use cases and situations. It's deep night now, and instead of going home, I read this whole thread, very diligently, from the top down. Why? Because I support my own dialect of Go, and now it's time to finally add this thing there too (as there's little hope in mainstream Go :) So, I thought it'd be good idea to see what people came up with in these three years.

Here's the small digest, for those of you reading this far down the thread. Looks like Go as it is now, plus the ideas and packages presented above, ALREADY has all the tools to solve the problem, in several ways.

Solution 1:

  1. Look at this https://github.com/sirkon/go-format, it already has all the functionality implemented -- but not the ease of syntax, but follow with me...

  2. define these shortcuts:

type MSA map[string]any
// or type MSA format.Values
FS := format.Printm      // or do this with preprocessor, or other way, e.g. as global var

after this, you can write this code:

myStr := FS("${name} $count ${weight|1.2}", MSA{
	"name": "name",
	"count": 12,
	"weight": 0.79,
}

Look at that lib, it's has much more to use. And see also the https://src.eruta.nl/beoran/ginterpol.

Solution 2:

Some genius from Go dev team added os.Expand() into the standard lib. Look at it. Combined with #34174 (comment), and with the shortcuts from Solution 1, you get an excellent Solution 2, ready to use.

Solution 3:

This is what I am going to implement in my dialect. It's different in that it solves a much broader problem of embedding DSL, it's close in spirit to the https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates
. Here's what it'll look like:

name := "You"
 DSL1(`Hello ${name}${p}`, p => "!")

name := "You"
fmt.Print(IT:`
    Hello ${name}!
`)

Where "IT" and "DSL1" are keys, by which particular DSL handlers are registered by user programmer.

Thanks everyone for great analytics and great ideas, and have a good day (and or night)!

@finnatsea

This comment was marked as off-topic.

@gonzojive
Copy link

gonzojive commented Nov 5, 2022

My use case for this revolves around writing code generators in both Go and Kotlin. Kotlin has string interpolation, and Go has fmt.Sprintf() and the text/template package.

For a large textual template, I always use "text/template." In the Go case, every time I have a code generator to write, I need to relearn the text/template` syntax, which is an un-intuitive domain specific language with no editor auto completion or static checks.

In the Kotlin case, there is no need for "text/template" because Kotlin itself can be used to interpolate the template with variable names, type names, etc. I do not re-learn the template formatting language every 6 months when I update the template.

My argument is that widespread use of "text/template" in Go and the absence of a need for that library in Kotlin is compelling evidence that string interpolation is useful.

--
A counterargument might be that most of what I'm doing with string interpolation and text/template could be achieved with simple concatenation:

var code = `var ` + varName + ` = foo()`

is pretty close in convenience to

var code = `var ${varName} = foo()`

Maybe it's not a big enough win for changing the language. However, I think the subtle things matter in programming languages, and I find first class string interpolation in other languages preferable to old school Sprintf and "foo" + bar + "baz."

@evandrojr
Copy link

Until Go 2.0 does not arrive I have created my own package for string interpolation:

https://github.com/evandrojr/string-interpolation

Simple string interpolation for golang. Interpolates anything in an easy way.

No need to pass the format parameters %d, %s, %t... anymore!

It has 3 methods: Print, Println and Sprint:

Example:

esi.Print("Print ", 10, " ", 7, " interpolates anything ", true, " ", 3.4e10) 

Output:

Print 10 7 interpolates anything true 3.4e+10

@DeedleFake
Copy link

@evandrojr

fmt.Print(), fmt.Println(), and fmt.Sprint() already work similarly. The only difference is that they insert spaces automatically in certain situations. For example,

fmt.Print("Print ", 10, 7, " interpolates anything ", true, 3.4e10)

is the equivalent of your code above.

@evandrojr
Copy link

evandrojr commented Nov 20, 2022

@evandrojr

fmt.Print(), fmt.Println(), and fmt.Sprint() already work similarly. The only difference is that they insert spaces automatically in certain situations. For example,

fmt.Print("Print ", 10, 7, " interpolates anything ", true, 3.4e10)

is the equivalent of your code above.

@DeedleFake
Thank you for your answer, I didn't know that!

To be more precise the equivalent are:

esi.Println("esi.Println ", 10, " ", 7, " interpolates anything ", true, " ", 3.4e10)
fmt.Print("fmt.Print ", 10, 7, " interpolates anything ", true, 3.4e10, "\n")
fmt.Println("fmt.Println", 10, 7, "interpolates anything", true, 3.4e10)

I think it causes confusion the difference of Println and Print since Println always add a space between the arguments and Print adds a space only in some situations. I like my solution since it is deterministic because it never adds spaces between the arguments.

I will try to do also:

i:= 7
esi.Print("esi.Print {i}")

Output:

esi.Print 7

I have just asked this question in SO:

https://stackoverflow.com/questions/74508523/golang-get-the-value-using-the-a-text-that-represents-the-name-of-a-variable-i

@Node0
Copy link

Node0 commented Dec 16, 2022

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

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?

I find @alvaroloes solution close to optimal.

This proposed solution is quite elegant. Reduces probability of bugs, and includes formatting capabilities that are quite obvious at first glance. The %{} notation gives a nod to C with the formatting symbols after the % and before {}.
if we took the next logical step and typed the interpolated and typed placeholders ala %d{someNumber} or typed + formatted %02d{someNumber} we will have successfully fused much of the good of sprintf with modern interpolation (a sort of "spiritual successor" if you will), finally, including evaluation of function calls allows the quite common case of pre-processing before forming the string, something like %{myFunc(data)} and typed + formatted %08s{myFunc(data)}. Coming from an sprintf/C inspiration, it also feels the most C-like in spirit and that's a very Go sort of design aesthetic.

If the output of the called function or the variable type doesn't match the given format string type, throw a compile time error. This would enforce type checking while bringing modern interpolation capabilities to the language.

I've used Python3 to generate entire project scaffolds (code generation via template strings etc) and right now, I'd never dream of writing a project scaffold generator in Go, perhaps this is for the best in terms of what the language is meant for. Having said that... There are so many aspects of Go that are obviously ergonomic in their design that it almost feels... out of place not to have modern interpolation (yes even if with a historically C-flavor) in the language, it feels like a lacuna for a 21st century programming language.

In short I also support @alvaroloes , @alphabettispaghetti , and others' input on this matter.

As much as I'm glad that @sirkon opened this issue, and got the conversation going.
I vote 'nay' on the \{status} form of proposed string (and function) interpolation.
If we're going to avoid ${} for existing compat reasons, then it makes way more sense to leverage the
existing muscle memory of all the C programmers and adopt a %<number><type>{<token>|<functionCall>} form.
e.g. %.02f{someFloatingPointNumber} or %05d{upToFivePlacesInteger} or just punt and cast to string %s{foo}

It will make Go way more productive and attract interesting projects to the language.

@ianlancetaylor
Copy link
Contributor

Perhaps it would be useful to consider a simpler approach: #57616 .

@ianlancetaylor
Copy link
Contributor

Per the discussion in #57616 this is a likely decline. Leaving open for four weeks for final comments.

You can a similar effect using fmt.Sprint, with custom functions for non-default formatting. So it can already be done in Go, it just looks a bit different. fmt.Sprint("This house is ", measurements(2.5), " tall") where measurements is a function that returns strings like "two feet six inches".

@ianlancetaylor
Copy link
Contributor

No further comments.

@ianlancetaylor ianlancetaylor closed this as not planned Won't fix, can't repro, duplicate, stale Apr 12, 2023
@celesteking

This comment was marked as abuse.

@golang golang locked as resolved and limited conversation to collaborators Feb 8, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
LanguageChange Suggested changes to the Go language Proposal Proposal-FinalCommentPeriod v2 An incompatible library change
Projects
None yet
Development

No branches or pull requests