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: fmt: add flags to control printing of slices #52936

Closed
josharian opened this issue May 16, 2022 · 13 comments
Closed

proposal: fmt: add flags to control printing of slices #52936

josharian opened this issue May 16, 2022 · 13 comments

Comments

@josharian
Copy link
Contributor

josharian commented May 16, 2022

TL;DR

func ExampleCommaFlag() {
	fmt.Printf("%,v", []int{1, 2, 3})
	// Output:
	// 1, 2, 3
}

func ExampleNewlineFlag() {
	fmt.Printf("%\nq", []string{"a", "b", "c'})
	// Output:
	// "a"
	// "b"
	// "c"
}

func ExampleSemicolonFlag() {
	fmt.Printf("%;d", []int{1, 2, 3})
	// Output:
	// 1; 2; 3
}

Proposal

Pretty-printing a slice of things is a common task. Yet it takes a fair amount of code to convert []int{1, 2, 3, 4} into a pretty "1, 2, 3, 4". (Either print into a buffer, checking indices to avoid a trailing comma, or convert into strings and use strings.Join.)

This is the sort of common task that fmt could make easy, for a few common separators: comma, semicolon, and newline.

I propose that we add corresponding flags to control how package fmt prints slices.

Though this proposal only deals with slices, it was motivated by thinking about the question about how a multierr in the standard library would format (#52607 (comment), #47811 (comment)). This fmt flag would extend naturally to common formatting choices for multierrs. (And potentially to maps and structs, but that's less obvious to me.)

Q&A

  • Why these separators? In my experience, they're the most common.
  • What about adding an "and" before the final item? Out of scope, because it gets into internationalization/localization questions that we don't want to tackle in the standard library, and there isn't a good way to further parameterize printing with fmt.
  • Why only infix, instead of "after every element"? It is easy for the caller to add leading/trailing formatting in the format string itself; it is not easy to undo.
  • What happens if a slice-ish flag is present for a non-slice argument? Nothing, similar to how fmt already handles irrelevant flags. This is particularly nice behavior for printing errors that might or might not be multierrs.

Alternatives

The primary alternatives here are:

  • Just write the code, in a case by case basis.
  • Write a generic/reflect-y function to pretty-print slices.

The drawbacks to those are:

  • Work gets done eagerly. (Related: proposal: Go 2: lazy values #37739.)
  • No clear extension to multierr formatting.
  • Separate pretty-printing ends up allocating into a separate buffer, instead of being able to use package fmt's existing buffer.
  • This is a simple enough thing that it'd be nice to have as "batteries included".

Using fmt.Formatter would help with doing work lazily and with not allocating into a separate buffer. However, fmt.Formatter is not easy to hold. (Related: #51668.) And there are no generic methods, so there's no way to do this in a generic way. In particular, this requires extra hoops for the most common slice types, like []int and []string (and interface-based multierrs?), which is where I would anticipate this being used the most.

Downsides

Package fmt is already complicated. This further increases that complication.

Tools that parse and interpret fmt formatting strings might require updating.

cc @robpike @jba @neild @ianlancetaylor @maja42 @jimmyfrasche @rogpeppe

@ChrisHines
Copy link
Contributor

This feature could be the basis for more flexibility in the formatting capabilities of my github.com/go-stack/stack package.

It currently supports several useful formats for a call frame:

$ go doc github.com/go-stack/stack.Call.Format
package stack // import "github.com/go-stack/stack"

func (c Call) Format(s fmt.State, verb rune)
    Format implements fmt.Formatter with support for the following verbs.

        %s    source file
        %d    line number
        %n    function name
        %k    last segment of the package path
        %v    equivalent to %s:%d

    It accepts the '+' and '#' flags for most of the verbs as follows.

        %+s   path of source file relative to the compile time GOPATH
        %#s   full path of source file
        %+n   import path qualified function name
        %+k   full package path
        %+v   equivalent to %+s:%d
        %#v   equivalent to %#s:%d

But has only limited flexibility for slices of call frames:

$ go doc github.com/go-stack/stack.CallStack.Format
package stack // import "github.com/go-stack/stack"

func (cs CallStack) Format(s fmt.State, verb rune)
    Format implements fmt.Formatter by printing the CallStack as square brackets
    ([, ]) surrounding a space separated list of Calls each formatted with the
    supplied verb and options.

@jimmyfrasche
Copy link
Member

What about a fmt.ListFormatter[T] type? Used like:

fmt.Sprintf("%v", fmt.ListFormatter(list, opts))

opts TBD but allow for custom separators and the like. Each item in the list uses the specified formatting directive.

That probably wouldn't help with multierrors much but it would make it simpler to print prettier lists. It could be prototyped outside std using the regular formatter interface. Bringing it in to std would allow it to be lazy and reuse buffers.

@dsnet dsnet changed the title fmt: add flags to control printing of slices proposal: fmt: add flags to control printing of slices May 16, 2022
@gopherbot gopherbot added this to the Proposal milestone May 16, 2022
@ianlancetaylor ianlancetaylor added this to Incoming in Proposals (old) May 16, 2022
@rsc
Copy link
Contributor

rsc commented May 18, 2022

I don't doubt the need, but I am not sure we can redefine these characters to be flags instead of verbs. For example https://go.dev/play/p/nVDu4H-e5GX.

It's true that on string it doesn't matter, but if we did fmt.Printf("%,v", customThing) and customThing had a Format method, then today Format gets called with verb = ','. Would it get called with verb = 'v' tomorrow? Or would the set of flags be different for things with Format methods and things without?

@rsc rsc moved this from Incoming to Active in Proposals (old) May 18, 2022
@rsc
Copy link
Contributor

rsc commented May 18, 2022

This proposal has been added to the active column of the proposals project
and will now be reviewed at the weekly proposal review meetings.
— rsc for the proposal review group

@josharian
Copy link
Contributor Author

today Format gets called with verb = ','. Would it get called with verb = 'v'

That is indeed unfortunate, as it appears to freeze the set of flags.

It looks to me like we already (inadvertently?) made a breaking change to the set of verbs, though, with %w. From fmt/print.go:(*pp).handleMethods:

	if verb == 'w' {
		// It is invalid to use %w other than with Errorf, more than once,
		// or with a non-error arg.
		err, ok := p.arg.(error)
		if !ok || !p.wrapErrs || p.wrappedErr != nil {
			p.wrappedErr = nil
			p.wrapErrs = false
			p.badVerb(verb)
			return true
		}
		p.wrappedErr = err
		// If the arg is a Formatter, pass 'v' as the verb to it.
		verb = 'v'
	}

It looks like previous uses of %w with errors that implemented fmt.Formatter got altered to %v.

Maybe because it was so restricted in scope, but I don't recall seeing a single bit of fallout from that. fmt.Formatter is very sparsely used, and I would be very surprised to learn someone was using a non-letter verb with it, given the obvious pattern established by package fmt (non-letters for flags, letters for verbs).

All that said...

Or would the set of flags be different for things with Format methods and things without?

This seems like it would be the right choice (sadly). We would then document the exact set of flags compatible with fmt.Formatter.

@MatthewJamesBoyle
Copy link

This proposal looks sensible to me for basic types.

Perhaps a different proposal but how would it print slices of complex structs?

@jimmyfrasche
Copy link
Member

I threw together a quick proof of concept for using a generic fmt.Formatter to map the format string over a slice's elements https://go.dev/play/p/V-QsENTB4j3 (with special thanks to the CL for #51668). It doesn't have any configuration but that would be trivial to add.

@rsc
Copy link
Contributor

rsc commented May 25, 2022

I am still not sure about changing the flag semantics, nor about exactly where the line is for where we stop putting features into format strings. (Perhaps we've already crossed it.)

/cc @robpike

@robpike
Copy link
Contributor

robpike commented May 26, 2022

Fmt can't do everything. Other packages can, though.

@jimmyfrasche
Copy link
Member

I've added (perhaps too many) knobs to my proof of concept and published it for anyone who needs it: https://pkg.go.dev/github.com/jimmyfrasche/slicefmt

@rsc
Copy link
Contributor

rsc commented Jun 1, 2022

It sounds like maybe we've converged on not changing the standard library and instead focusing on other packages to provide this kind of thing. Do I have that right?

@rsc rsc moved this from Active to Likely Decline in Proposals (old) Jun 8, 2022
@rsc
Copy link
Contributor

rsc commented Jun 8, 2022

Based on the discussion above, this proposal seems like a likely decline.
— rsc for the proposal review group

@rsc rsc moved this from Likely Decline to Declined in Proposals (old) Jun 15, 2022
@rsc
Copy link
Contributor

rsc commented Jun 15, 2022

No change in consensus, so declined.
— rsc for the proposal review group

@rsc rsc closed this as completed Jun 15, 2022
@golang golang locked and limited conversation to collaborators Jun 15, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
No open projects
Development

No branches or pull requests

7 participants