Closed sirkon closed 1 year ago
@ianlancetaylor Thanks for the clarification. I appreciate the desire to not break backwards compatibility with something as fundamental as the fmt
package or the type system. I was just hoping there might be some hidden (to me) possibility that might be an option along those lines somehow. 👼
I really like the Ian way to implement this. Wouldn't generics help with the fmt.Print
issue?
contract printable(T) {
T string, map[string]string // or the type Brad suggested "runtime.StringMap"
}
// And then change the signature of fmt.Print to:
func Print(type T printable) (str T) error {
// ...
}
This way, the Go 1 compatibility should be preserved.
For Go 1 compatibility, we can't change the type of a function at all. Functions are not only called. They are also used in code like
var print func(...interface{}) = fmt.Print
People write code like this when making tables of functions, or when using hand-rolled dependency injection for tests.
I have the feeling that strings.Replacer (https://golang.org/pkg/strings/#Replacer) can almost do string interpolation, just missing the interpolation identifier (e.g. ${...}) and the pattern processing (e.g. if var i int = 2
, "${i+1}" should be mapped to "3" in the replacer)
Yet another approach would have a built-in function, say, format("I am a %(foo)s %(bar)d") that expands to fmt.Sprintf("I am a %s %d", foo, bar). At least, that's fully backwards compatible, FWIW.
From a language design perspective, it would be peculiar to have a builtin function expand to a reference to a function in the standard library. To provide a clear definition for all implementations of the language, the language spec would have to fully define the behavior of fmt.Sprintf
. Which I think we want to avoid.
This probably won't make everyone happy but I think the below would be the most general. It's broken up into three parts
map[string]interface{}
map[string]interface{}
when calling it"name": name,
or "qual.name": qual.name,
Taken together that would allow something like
fmt.Printm("i: {i}; j: {j}", {i, j})
// which is equivalent to
fmt.Printm("i: {i}; j: {j}", map[string]interface{}{
"i": i,
"j": j,
})
That still has the duplication between the format string and the arguments but it's a lot lighter on the page and it's a pattern that's easily automated: an editor or tool could automatically fill in the {i, j}
based on the string and the compiler would let you know if they're not in scope.
That doesn't let you do computations within the format string which can be nice, but I've seen that overdone enough times to consider it a bonus.
Since it applies to map literals in general it can be used in other cases. I often name my variables after the key they'll be in the map I'm building.
A downside of this is that it can't apply to structs since those can be unkeyed. That could be rectified by requiring a :
before the name like {:i, :j}
and then you could do
Field2 := f()
return aStruct{
Field1: 2,
:Field2,
}
Do we need any language support for this? As go is now, it can look like this, either with a map type or with a fluid, more type-safe API:
package main
import (
"fmt"
"strings"
)
type V map[string]interface{}
func Printm(format string, args V) {
for k, v := range args {
format = strings.ReplaceAll(format, fmt.Sprintf("{%s}", k), fmt.Sprintf("%v", v))
}
fmt.Print(format)
}
type Buf struct {
sb strings.Builder
}
func Fmt(msg string) *Buf {
res := Buf{}
res.sb.WriteString(msg)
return &res
}
func (b *Buf) I(val int) *Buf {
b.sb.WriteString(fmt.Sprintf("%v", val))
return b
}
func (b *Buf) F(val float64) *Buf {
b.sb.WriteString(fmt.Sprintf("%v", val))
return b
}
func (b *Buf) S(val string) *Buf {
b.sb.WriteString(fmt.Sprintf("%v", val))
return b
}
func (b *Buf) Print() {
fmt.Print(b.sb.String())
}
func main() {
Printm("Hello {k} {i}\n", V{"k": 22.5, "i": "world"})
Fmt("Hello ").F(22.5).S(" world").Print()
}
https://play.golang.org/p/v9mg5_Wf-qD
Ok, it's still inefficient, but it looks like it is not a lot of work at all to make a package that supports this. As a bonus, I included a different fluid API that might be said to simulate interpolations somewhat as well.
The "map string" proposal from @ianlancetaylor (although I personally prefer "value/variable string" with a v"..." syntax) also allows non-formatting use cases. For example, #27605 (operator overloading functions) largely exists because it is difficult today to make a readable API for math/big
and other numeric libraries. This proposal would allow the function
func MakeInt(expression map[string]interface{}) Int {...}
Used as
a := 5
b := big.MakeInt(m"100000")
c := big.MakeInt(m"{a} * ({b}^2)")
Importantly, this helper function can coexist with the more performant and powerful API that currently exists.
This approach allows the library to perform whatever optimizations it wants for large expressions, and might also be a useful pattern for other DSLs, since it allows custom expression parsing while still representing values as Go variables. Notably, these use cases are not supported by Python's f-strings because the interpretation of the enclosed values is imposed by the language itself.
@HALtheWise Thanks, that is pretty neat.
I wanted to comment to show a little support for this proposal, from the stance of a general developer. I've been coding with golang for over 3 years professionally. When I moved to golang (from obj-c/swift) I was disappointed that string interpolation was not included. I've used C and C++ for over a decade in the past, so printf wan't a particular adjustment, other than feeling like going backwards a little -- I've found that it does indeed make a difference with code maintenance and readability for more complex strings. I've recently done a little bit of kotlin (for a gradle build system), and using string interpolation was a breath of fresh air.
I think string interpolation can make string composition more approachable for those new to the language. It's also a win for technical UX and maintenance, due to the reduction of cognitive load both when reading and writing code.
I am glad that this proposal is getting real consideration. I look forward to the resolution of the proposal. =)
If I understand correctly, the @ianlancetaylor proposal is:
i := 3
foo := m"twice i is %20{i * 2}s :)"
// the compiler will expand to:
foo := map[string]interface{}{
"": "twice i is %20{i * 2}s :)",
"i * 2": 6,
}
After that, a print function will handle that map, parse entire template again, and take few advantages of the pre-parsed template
But if we expand m"str" to a function?
i := 3
foo := m"twice i is %20{i * 2}s :)"
// the compiler will expands to:
foo := m(
[]string{"twice i is ", " :)"}, // split string
[]string{"%20s"}, // formatter for each value
[]interface{}{6}, // values
)
This function has the following signature:
func m(strings []string, formatters []string, values []interface{}) string {}
This function will perform better because, to take more advantage of the pre-parsed template, and much more optimizations could be done similar as Rust does with the println!
function.
What I'm trying to describe here is very similar to the Tagged functions of the Javascript, and we could discuss whether the compiler should accept user functions to format string ex:
foo.GQL"query { users{ %{expectedFields} } }"
bla.SQL`SELECT *
FROM ...
WHERE FOO=%{valueToSanitize}`
@rodcorsi If I'm reading your suggestion correctly, it requires building fmt.Printf
formatting into the language proper, because the compiler will have to understand where %20s
starts and ends. That is one of the things I was trying to avoid.
Also note that my suggestion is not at all tied to fmt.Printf
formatting, and can be used for other kinds of interpolation as well.
I would be opposed to treating m"..."
as expanding to a function call, because it obscures what's actually going on, and adds what's effectively a second syntax for function calls. It does generally seem reasonable to pass a more structured representation than a map, to avoid needing matching reimplementations of the parsing behavior everywhere. Perhaps a simple struct with a slice of constant string sections, a slice of strings for things in braces, and an interface slice?
m"Hello {name}" ->
struct{...}{
[]string{"Hello ", ""},
[]string{"name"},
[]interface{}{"Gopher"}
The second and third slices must be the same length, and the first must be one longer. There are other ways to represent this as well to encode that constraint structurally. The advantage this has over a format that directly exposes the original string is that there's a looser requirement to have a correct and performant parser in the function that consumes it. If there's no support for escaped characters or nested m-strings, it probably isn't a big deal, but I'd rather not need to reimplement and test that parser, and caching it's result could cause runtime memory leaks.
If "formatting options" is a frequent desire of things using this syntax, I could see there being a place for them in the spec, but I'd personally go with a syntax like m"{name} is {age:%.2f} years old"
where the compiler just passes everything after the :
on to the function.
Hello I wanted to comment on this to add support to this proposal. I been working with many different languages in the past 5 years (Kotlin, Scala, Java, Javascript, Python, Bash, some C, etc) and I'm learning Go now.
I think string interpolation is a must have in any modern programming language, the same way as type inference is, and we have that in Go.
For those arguing that you can accomplish the same thing with Sprintf, then, I don't understand why we have type inference in Go, you could accomplish the same thing writing the type right? Well, yes, but the point here is that string interpolation reduce a lot of the verbosity you need to accomplish that and is more easy to read (with Sprintf you have to jump to the argument list and the string back and forth to make sense of the string).
In real life software, this is a much appreciate feature.
It is against the Go minimalist design? No, it's not a feature that allow you to do crazy things or abstractions that complicate your code (like inheritance), is just a way of writing less and add clarity when you read the code, which I believe isn't against what Go is trying to do (we have type inference, we have the := operator, etc).
Coming from other languages (TypeScript/JavaScript, Java, Python, PHP, some of others) and forced to use go (because of interest in kubernetes autoscaler) I don't understand the claim "easy". It's not helpful to have a bunch of (sometimes complex and hard to understand) workarounds because of missing language features. And I don't mean my code as a beginner, but code from professional go developers. More precise and honest claim for "easy" (or in some other projects: "lightweight") is "limited". Don't get me wrong, "limited" can be a desired property. But I'm not sure about general purpose programming languages.
Thank to @alanfo I found a workaround to have at least some readability for multiline strings:
func interpolate(template string, variables map[string]string) string {
f := func(ph string) string {
return variables[ph]
}
return os.Expand(template, f)
}
func main() {
fmt.Println(interpolate(
`The gopher ${name}
is ${days} days old.`,
map[string]string{
"name": "blub",
"days": fmt.Sprintf("%2.1f", 12.312),
},
))
}
The similar (but TBH not the same floating point format - I'm too lazy - exercise for the reader) thing in TypeScript:
const name = 'blub'
const days = 12.312
console.log(`The gopher ${name}
is ${days.toFixed(1)} days old.`)
In fact, with library like outdent you get a very nice indentation of code and don't need that the lines after first starts at beginning of the line to not to have white spaces at beginning:
const indent = outdent({trimTrailingNewline: false})
function example() {
const name = 'blub'
const days = 12.312.toFixed(1)
const template = indent`
The gopher ${name}
is ${days} days old.
`
console.log(template)
}
Grr I has always missed this in go. I've found a reddit topic dated one year ago and thought cool, these guys devs must've added something to the language already. Sigh..
Just a little experience report: I am currently building a User Interface library and needed string interpolation as a facility for the clients of the library.
Goal was to have the string variables storable in a map[string]string, modifying the map values would recompile the string. Didn't have time to implement it so I took a shortcut using fmt.Sprintf although it's really brittle, requiring to pass the string variable names separately.
Thought of using text/template but it looked a bit complex and I wouldn't be able to use it anyway for compilation with tinyGo as it is not supported.
So, just to say that I too, wish for some kind of string interpolation.
(i could probably implement a simple string parser but I'm not very proficient with parsers, I'd rather leave it to someone that knows how to do it properly) May not need to be a language thing.. Just need the parser to return a maptype from parsing a templated string, map which would have a method that returns the complete string depending on the map value mutations.
@atdiar my https://github.com/sirkon/go-format may be helpful then. In this form — format.FormatX
shortcuts are for "static" configurations and are not a good match for user input.
PS it doesn't look like built-in formatting would be helpful in your case.
PPS Created the ticket just for fun. Didn't expect then it would gain so many reactions LOL.
Haskell has libraries and language extensions for string interpolation. It's not a type thing.
It is :)
You see, Haskell type system (and syntax) allows to implement formatting at library level. Not possible with Go. So it IS a type thing :)
@atdiar my https://github.com/sirkon/go-format may be helpful then. In this form —
format.FormatX
shortcuts are for "static" configurations and are not a good match for user input.PS it doesn't look like built-in formatting would be helpful in your case.
Looks a bit too complex.
Basically, given a templated string:
" Hi, my name is $(firstname) !"
I want to use it to configure a map type.
type Interpolater struct{
Template string
Params map[string]string
}
func(i Interpolater) SetTemplate(s string) error {...}
// String returns the string result
func(i Interpolater) String() string{...}
func(i Interpolater) Set(param string, value string)
I could naively write the string template parsing function by iterating through the string characters and substracting the param names. But people seem to use full-fledged parser/lexers for such tasks so I left it for later. There must be something I don't know or an issue I am unaware of with the naive string character iteration method.
PPS Created the ticket just for fun. Didn't expect then it would gain so many reactions LOL.
The fact this "simple" feature got into such a big debate just proves how important it is for development usability and ergonomics.
For devs used to C and C-like languages, it's acceptable to simply use fmt.Sprintf
, even if it leads to more code and more brain consumption for a side task like string interpolation.
For those who don't use C-like languages or came from other backgrounds, the lack of a proper string interpolation syntax (something considered "basic" for any modern language) is odd, at least.
I understand the argument that Go must keep a simple and lean language, but the question here is: How much do we want to tradeoff between simpleness and usability?
I mean, if we really care only about simpleness, should Go even bother to have arithmetic operators in the first place? Why not simply add needed functions to math
package and use it instead? Then everyone could create wrapper functions themselves to help with those operations if they use them a lot, this should not be concern of the language syntax... See how it makes no sense? That's how everyone else feels about lack of string interpolation in a 2000s language.
As a library, that would give something akin to this I guess: https://play.golang.org/p/RGhnrwsm5a2
package main
import (
"fmt"
"strings"
)
func Parse(input string, tokenstart string, tokenend string) []string {
result := make([]string, 0)
ns := input
startcursor := strings.Index(ns, tokenstart)
if startcursor == -1 {
return result
}
ns = ns[startcursor:]
ns = strings.TrimPrefix(ns, tokenstart)
endcursor := strings.Index(ns, tokenend)
if endcursor < 1 {
return result
}
tail := ns[endcursor:]
result = append(result, strings.TrimSuffix(ns, tail))
subresult := Parse(strings.TrimPrefix(tail, tokenend), tokenstart, tokenend)
result = append(result, subresult...)
return result
}
type TemplatedString struct {
Template string
delimstart string
delimend string
Parameters map[string]string
}
func NewTemplateString(t string) TemplatedString {
params := make(map[string]string)
delimstart := `${`
delimend := `}`
paramnames := Parse(t, delimstart, delimend)
for _, name := range paramnames {
params[name] = "[undefined]"
}
return TemplatedString{t, delimstart, delimend, params} // By default, the template parameters are stored between `${` and `}`
}
func (t TemplatedString) SetParamDelimiters(start string, end string) TemplatedString {
t.delimstart = start
t.delimend = end
return t
}
func (t TemplatedString) SetParameter(name string, value string) {
_, ok := t.Parameters[name]
if ok {
t.Parameters[name] = value
}
}
func (t TemplatedString) String() string {
res := t.Template
for k, v := range t.Parameters {
p := t.delimstart + k + t.delimend
res = strings.ReplaceAll(res, p, v)
}
return res
}
func main() {
t := NewTemplateString("My ${name} is the best. I love him ${number} !")
t.SetParameter("name", "pet")
t.SetParameter("number", "3000")
fmt.Print(t.String())
t = NewTemplateString("Your ${name} is the best too. I love him ${number} !")
t.SetParameter("name", "pet")
fmt.Print(t.String())
}
Golang should have variable interpolation for anything that supports .String()
. If the argument is "there is no interpolation syntax that won't break backwards compatibility with people's strings" that's arguing an edge case is worth preserving against a primary use case that would improve every go programmer's job on a daily (hourly? constant?) basis, which is absolutely insane. This kind of stuff is what drives off new and entry level programmers to Go and continues to hamper the growth of the language.
Which satisfies Stringer or is a string. Another example where typelists as discriminated unions could be useful. I have actually run into this issue several times but it's another topic.
I want to show my support on this proposal, and I want to give an example to illustrate comparing to `${variable}`
or m"{variable}"
, the %s/%d/...
way to generate strings is more prone to human error.
It's not obvious in trivial case, but when the use case scales, the disadvantage of prone to error surfaces. Below is just one example, the idea should not only apply to SQL queries, not only multiline string, but all similar examples where the number of variables to interpolate grows. It need not be as many as 50 variables like what @runeimp mentioned.
fmt.Sprintf(
`
SELECT * FROM table_name
WHERE
title=%s AND
(name=%s AND date=%s AND ratings=%d) OR
priority=%d AND
group=%s AND
date=%s OR
(genre=%s AND priority=%d AND ratings=%d)
`,
title,
name, date, ratings,
priority,
group,
date,
genre, priority, ratings
)
Writing the initial version might not be much of a problem, for example you could just do a checksum - 6 lines of %s/%d, 6 lines of variable arguments. You can even do a little code formatting so name, date, ratings
are on the same line to "sync" with the shape of the string containing %s/%d, although this is not always possible.
The maintainability problem surfaces when you try to edit it, e.g. change orderings, add some new text with new variables, remove text with some variables.
There're extra checks needed to make sure the ordering of the variable args are right, the number of the %s/%d matches exactly the number of arguments, besides the app logic. Sprintf
at some extent reminds me of malloc
and free
in C
, powerful, but more prone to human error.
Some suggest why not just use Printfln, so that variables have proximity to the content context? You can actually try it yourself - literally type and convert the fmt.Sprintf
version above to fmt.Sprintln
version below.
fmt.Sprintln(
"SELECT * FROM table_name ",
"WHERE ",
"title=", title, "AND ",
"(name=", name, " AND date=", date, " AND ratings=, ratings, ") OR ",
"priority=", priority, " AND ",
"group=", group, " AND ",
"date=", date, " OR",
"(genre=", genre, " AND priority=", priority, " AND ratings=", rating, ")"
)
Those extra spaces you need to be careful to insert, lots of commas and double quotes, not to say when you need to edit later on, you need to tip toe on the commas, quotes and space constantly, alongside the app logic.
Imagine with string interpolation like `title=${title}`
, it brings variables to immediate proximity to the content, I can focus more on the app logics, not the ordering of the variables arguments of fmt.Sprintf
and the ordering of "%s/%d", not whether the number of arguments matches the number of %s, %d, ... or not:
// pseudo code
fmt.Println(
f`
SELECT * FROM table_name
WHERE
title=${title} AND
(name=${name} AND date=${date} AND ratings=${ratings}) OR
priority=${priority} AND
group=${group} AND
date=${date} OR
(genre=${genre} AND priority=${priority} AND ratings=${ratings})
`
)
I've already bumped into errors several times because of a mismatch of the %s/%d.. with the following variable arguments as the code changes over time. I do think comparing to string interpolation, fmt.Sprintf()
is more prone to error. It has been the only way for C
, but for a modern language like Go, it should be also designed for less human error - in this case - developers. This is quite of an usability problem, and developer experience matters a lot. Checking ordering and matching while jumping back and forth are needed for experienced users as well, and would be a barrier for novel users.
I think the explicit argument index feature of package fmt is often forgotten. It doesn't achieve the same degree of maintainability as interpolated strings, but it does reduce the burden in cases like these.
This has been discussed quite a bit in this issue that has sadly been closed. I haven't taken the time (yet?) to re-write the proposal out of concern that I don't have the time to champion it right now. There is precedent for multi-line string support in other languages (my thoughts) and I don't know that we need to re-invent the wheel there.
I think fixing multi-line strings like the SQL query above is a separate change, though related to string interpolation. IMO ideally you could copy/paste a SQL query like in your example into a multi-line string and have it keep it's formatting, rather than having it be translated into an interpolated (and re-formatted) string.
The idea of string literals that evaluate to map[string]interface{} is interesting, especially given techniques like @HALtheWise's https://github.com/golang/go/issues/34174#issuecomment-620890833, but I worry about its performance. Creating a map is not free, and it would force every value used in it (as well as strings for the keys) to be allocated on the heap. Of course performance isn't everything, but it would be another case where performance-sensitive paths have to give up on the nice feature and write code that is comparatively ugly. I often write those performance-sensitive paths; I'm already used to feeling this sensation.
So, having re-encountered this proposal after a while, I would like to propose an alternative.
It seems most of @ianlancetaylor's arguments against adding string interpolation as originally proposed is about formatting non-string types. In particular, in https://github.com/golang/go/issues/34174#issuecomment-540828621, he mentions, "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." The discussion since has mostly regarded approaches that can handle those other types, but I would find it satisfactory to obviate the need by allowing only expressions that evaluate to values of type string or fmt.Stringer in formatting directives.
More formally, consider the following change to the spec. (I'd like to stress that I am proposing semantics, not syntax per se; f"{x}"
could be "\{x}"
or $"{x}"
or anything else, with a different construction.) Under the section "expressions," add a subsection along these lines:
Interpolated string literals represent string values constructed from the concatenation of constant character sequences interspersed with dynamically evaluated interpolation sequences. They are written as interpreted string literals prefixed with f
, with interpolation sequences surrounded by braces. Outside of interpolation sequences, it is an error for an interpolated string literal to contain different counts of "{" and "}" characters or a "}" character before a "{" character.
InterpolatedLit = `f"` { unicode_char | escaped_char } `"` .
InterpolationSequence = "{" Expression "}" .
Each interpolation sequence specifies an expression to evaluate at run-time. The expression must have a result type of string, or the result type must have a method String() string
, or it must be a constant representable as type string.
Interpolated string literals first have each interpolation sequence extracted. The remaining portion of the literal is interpreted as if it were an interpreted string literal. Then, the expression within each interpolation sequence is evaluated. If the result is a value assignable to type string or a constant representable as type string, then it replaces the interpolation sequence. Otherwise, the result of calling its String
method replaces the interpolation sequence.
[Examples ensue.]
In addition to that section, add to the section on assignability:
A value
x
is assignable to a variable of typeT
("x
is assignable toT
") if one of the following conditions applies:
- ...
x
is an (evaluated) interpolated string literal andT
is or has underlying typestring
.
In the section on constant expressions, add:
An interpolated string literal is a constant if each interpolation sequence in the literal contains a constant expression.
And in the section on order of evaluation, add the interpolation sequences of interpolated string literals to the list of places where expressions are evaluated in lexical left-to-right order.
I think the main disadvantage to this approach is that numbers can't be formatted, and that seems like a desirable feature. However, in terms of the spec, it would be just a few sentences to add formatting for integers, or it would be straightforward to do s := strconv.Itoa
or s := fmt.Sprint
to do f"{s(len(f))}"
.
@slycrel I don't see any discussion related to interpolated strings in that issue. I think this and more flexible raw string literals would solve different problems, although they might occur in similar places.
@slycrel I don't see any discussion related to interpolated strings in that issue. I think this and more flexible raw string literals would solve different problems, although they might occur in similar places.
I don't mean to be off-topic here. I was simply mentioning that there has been a lot of discussion around maintaining copy/pasting multi-line strings in that other issue, in particular using SQL queries as examples like @rivernews mentions. And that other languages have solved that somewhat independently of string interpolation. I definitely care more about string interpolation overall and think it would have a greater impact on a day to day basis as far as language usage and readability.
The point I was trying to make... String interpolation can be solved while saying large blocks of inline strings is maybe another (related) issue that could/should be addressed separately. IMO multi-line strings should be solved first because the next logical improvement to multi-line strings is how string interpolation works with them. Which is what led me to that issue in the first place while thinking about string interpolation in general.
I guess I see 3 things here. String interpolation in general, maintaining copy/pasted multi-line strings, and string interpolation in multi-line strings. I think they all have some overlap. And... these could be incrementally added separately rather than all dealt with together. Though I am not opposed to them all being fixed at once.
In case someone is interested, I just wrapped up a small string interpolation library: https://src.eruta.nl/beoran/ginterpol Note that string interpolation is simple, but not that simple. If the go compiler were to support string interpolation if would have to support string formatting at runtime directly, causing bloat.
I wanted to add support for this proposal as well. JavaScript's Tagged Template Literals was mentioned earlier and I felt the idea deserved to be elaborated on a little bit. I think by adding tag functions you could avoid adding fmt formatting language to the spec which would be cumbersome to include in my opinion. Tag functions could then be used to expand the functionality of literals. Say a tag function must match this type.
// Tag function definition.
type tag func (template []string, parts ...interface{}) string
The tag syntax would split the string on interpolations, providing them as an array, and all interpolated values in an array. Then implementing a tag function wrapper of Sprintf could look like this.
func Sprintf(template []string, parts ...interface{}) string {
fullTemplate := strings.Join(template, "")
return fmt.Sprintf(fullTemplate, parts)
}
// Usage:
Value := 20
Formatted := Sprintf"Value is %d{Value}, %T{Value}"
// Long form Usage:
LongFormatted := Sprintf([]string{"Value is %d", ", %T", ""}, Value, Value)
The tag syntax would basically be sugar on top of the normal long form usage.
There is still an issue with basic interpolation though. Either the spec needs basic formatting rules to allow simple interpolation without a tag function like Plain := "Value is {Value}"
or interpolation should only be allowed if you use a tag function. Personally I think the second option would be best even though it is more restrictive. This would keep formatting rules out of the spec and as an implementation detail of tag functions.
The downside of this approach is that this tag syntax can seem magical and doesn't look like a function call even though it is one. This might also be out of scope for a string interpolation proposal and would be better as a complimentary proposal. Personally though I think this could strike a good enough balance between simplicity and flexibility to be worth investigating.
As a note JavaScript has seen tag functions used for DSL's as well to help improve correctness which would be great to see brought to go. Getting intelisense for sql"SELECT * FROM {Table}"
would greatly improve DX.
Perhaps templates provide the functionality you are looking for?
For internal use (not sharable) I created something much like @jimmyfrasche's suggestion one evening:
- fmt.Printm functions that take a format string and a
map[string]interface{}
I made it support normal sprintf modifiers with the interpolated values, so we could do type specific formatting. It took an object or a map (much like text/template does). We didn't have to wait for it to end up in stdlib; I put it to use right away on a few projects, and shared within our internal go community. For a couple of my projects, it helped quite a bit.
The more interesting takeaway, I think, is that it has near zero adoption. I got a lot of people saying it looked cool; but at the end of the day people still use sprintf and text/template.
I don't know if this will add anything meaningful to the conversation or not, but this is my Golang journey, coming from a background of multiple programming languages (Swift, TypeScript, PHP, Python, C).
At first, I thought it was odd we didn't use string interpolation in tutorials. I figured we would get back to it later.
Then I struggled with the idea of not having classes, but I eventually overcame this roadblock and embraced structs and receivers.
Then I struggled with the community's frequent use of single character variables, but overcame this with my own best practices standard.
Finally, I had to do some more string operations and began really looking into string interpolation. And now I am here. I really like how Golang is simple and opinionated. I like that there is usually only one way to do things. In my experience, Golang would do better to trade fmt.Sprintf()
in for string interpolation. If there is only going to be one way to format data in strings, I would rather it be string interpolation.
@jfesler I think the adoption issue is the same as the problem for all solutions in Go as it doesn't support automatic access of local variables. If I have to create a data structure of any sort to collect all the variables already in flight to utilize string interpolation it is not string interpolation as utilized in all other languages such as Python, Ruby, JavaScript, etc. I might as well use text/template
, html/template
, os.Expand
, or whatever as I've already lost most of what makes using string interpolation so massively time saving. If I've just spend a few hour writing code with lots of variables in scope I want to just write in interpolated string and go. Not spend an additional, 30 minutes or more writing up a data structure and implementation code for a template system to access tens (possibly hundreds?) of variables.
I once saw a great talk by Rob Pike, where he noted the process that languages were borrowing features from one another, and how this process meant that over time languages become more similar to one another and more complicated. Go has, thankfully, resisted this process.
@amnonbc I saw that talk too, and it is a fascinating point.
However, I'm not sure it applies well to this scenario. Golang's fmt
library has borrowed sprintf()
from C. I would argue that fmt.Sprintf()
is more syntactically complicated to the developer than string interpolation. It's not so much that a feature was not borrowed, but rather that a different feature maybe should have been borrowed? Of course, practically, it is virtually impossible to change something as a fundamental as this in a well established language. Therefore, the only practical solution would be to borrow both features, but now we're back at Rob Pike's conundrum. 🤔
Here is how Rust implemented string interpolation this in new 1.58.0 release: Captured identifiers in format strings
let person = get_person();
println!("Hello, {person}!"); // captures the local `person`
println!("Hello, {}!", get_person()); // implicit position
println!("Hello, {0}!", get_person()); // explicit index
println!("Hello, {person}!", person = get_person()); // named
let (width, precision) = get_format();
for (name, score) in get_scores() {
println!("{name}: {score:width$.precision$}");
}
https://github.com/golang/go/issues/34174#issuecomment-568643567
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
}
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.
Unlike PHP, Go is a compiled language, so there is no support for anything similar to get_defined_vars
.
We could add a built in function definedVars() map[string]any that does roughly the same, though.
The PHP function returns all variables defined anywhere. That is infeasible in Go.
Yes, but just the variables visible at that point from the current function could be possible. Maybe functionVars() would be a better name then.
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!
I really like C#s approach for string interpolation.
$""
) so that they are interpolated -> Fully backwards compatible$"Hello {name}"
){datetime:HH:mm}
to show hours:minutes or {num:0.00}
to show two decimalsThere are a ton more features: csharp string interpolation formating
Introduction
For ones who don't know what it is:
Swift
Kotlin
C
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
Syntax proposed
$
or{}
would be more convenient in my opinion, but we can't use them for compatibility reasons\(…)
notation would be compatible but these\()
are a bit too stealthyI guess
{…}
and\(…)
can be combined into\{…}
So, the interpolation of variable
variable
into some string may look likeFormatting also has formatting options. It may look like
Examples of options
etc
Conversions
There should be conversions and formatting support for built in types and for types implementing
error
andfmt.Stringer
. Support for types implementingcan be introduced later to deal with interpolation options
Pros and cons over traditional formatting
Pros
Cons
%v
(?),%T
, etc)