golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
122.7k stars 17.49k forks source link

proposal: Go 2: string interpolation #34174

Closed sirkon closed 1 year ago

sirkon commented 5 years ago

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

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

Cons

runeimp commented 4 years 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. 👼

alvaroloes commented 4 years ago

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.

ianlancetaylor commented 4 years ago

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.

sbourlon commented 4 years ago

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)

beoran commented 4 years ago

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.

ianlancetaylor commented 4 years ago

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.

jimmyfrasche commented 4 years ago

This probably won't make everyone happy but I think the below would be the most general. It's broken up into three parts

  1. fmt.Printm functions that take a format string and a map[string]interface{}
  2. accept #12854 so you can drop the map[string]interface{} when calling it
  3. allow unkeyed names in map literals as shorthand for "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,
}
beoran commented 4 years ago

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.

HALtheWise commented 4 years ago

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.

ianlancetaylor commented 4 years ago

@HALtheWise Thanks, that is pretty neat.

slycrel commented 4 years ago

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. =)

rodcorsi commented 4 years ago

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}`
ianlancetaylor commented 4 years ago

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

HALtheWise commented 4 years ago

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.

HALtheWise commented 4 years ago

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.

Frietziek commented 4 years ago

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

Bessonov commented 3 years ago

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)
}
minyor commented 3 years ago

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

atdiar commented 3 years ago

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.

sirkon commented 3 years ago

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

sirkon commented 3 years ago

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 commented 3 years ago

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

kazzkiq commented 3 years ago

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.

atdiar commented 3 years ago

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())
}
shakefu commented 3 years ago

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.

atdiar commented 3 years ago

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.

rivernews commented 3 years ago

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.

zephyrtronium commented 3 years ago

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.

slycrel commented 3 years ago

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.

zephyrtronium commented 3 years ago

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

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 type T ("x is assignable to T") if one of the following conditions applies:

  • ...
  • x is an (evaluated) interpolated string literal and T is or has underlying type string.

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))}".

zephyrtronium commented 3 years ago

@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 commented 3 years ago

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

beoran commented 3 years ago

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.

Devcon4 commented 3 years ago

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.

amnonbc commented 2 years ago

Perhaps templates provide the functionality you are looking for?

jfesler commented 2 years ago

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.

halfnibble commented 2 years ago

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

  1. At first, I thought it was odd we didn't use string interpolation in tutorials. I figured we would get back to it later.

  2. Then I struggled with the idea of not having classes, but I eventually overcame this roadblock and embraced structs and receivers.

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

runeimp commented 2 years ago

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

amnonbc commented 2 years ago

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.

halfnibble commented 2 years ago

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

ianlancetaylor commented 2 years ago

50554 by @Cookie04DE suggests a different approach to this.

Dentrax commented 2 years ago

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$}");
}
Curid commented 2 years ago

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
}
eaglebush commented 2 years ago

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 commented 2 years ago

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

beoran commented 2 years ago

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

ianlancetaylor commented 2 years ago

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

beoran commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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