Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: Go 2: add nameof() built-in function #36924

Closed
beoran opened this issue Jan 31, 2020 · 11 comments
Closed

proposal: Go 2: add nameof() built-in function #36924

beoran opened this issue Jan 31, 2020 · 11 comments
Labels
FrozenDueToAge LanguageChange Suggested changes to the Go language Proposal v2 An incompatible library change
Milestone

Comments

@beoran
Copy link

beoran commented Jan 31, 2020

  • Would you consider yourself a novice, intermediate, or experienced Go programmer?
    I am experienced, I have been using Go for 10 years since before v1.0.
  • What other languages do you have experience with?
    Batch files, Basic, C, C++, D, Java, Javascript, Lua, Objective-C, Oberon, Pascal, PHP, Python, R, Ruby, Shell scripts, Visual Basic, ...
  • Would this change make Go easier or harder to learn, and why?
    It would not make Go easier to learn, but also not substantially harder. If the new built-in function is well documented, everyone who doesn't know it yet can read its documentation to find out how it works.
  • Has this idea, or one like it, been proposed before?
    This idea is based on the proposal proposal: Go 2: obtain struct field name in type-safe way #36332.
  • If so, how does this proposal differ?
    In stead of special syntax I proposed a built-in function, which is more Go-like and easier to learn.
  • Who does this proposal help, and why?
    This proposal would help people writing ORM's, interpreters, or with debugging aid, and other people who need to use the names of Go language artifacts, such as struct field names, or type names have to be used as strings.
  • Is this change backward compatible?
    Yes, adding a new built in function is normally completely backwards compatible, and does not break the Go1 compatibility guarantee.
  • Show example code before and after the change.

Example: Orm-ish use.

package main

import (
	"fmt"
	"strings"
	"reflect"
)

type CartItem struct {
    Name string
    Stock int
    Price uint    
}

// simulate database/sql
type DB struct {
}

func (db * DB) Exec(query string, parameters ...interface{}) error {
	fmt.Printf("Would execute query %s with values %v\n", query, parameters)
	return nil
}

func (ci CartItem) Insert(db *DB) error {
	// Not very future proof. If the struct changes, I could easily forget to change field name here as well.
	query := "INSERT INTO CartItem (Name, Stock, Price) VALUES (?, ?, ?)"
	err := db.Exec(query, ci.Name, ci.Stock, ci.Price)
	return err
}

// Dummy implementation that simulates at run time run time what I would like nameof to do 
// at compile in the case of a struct value.
// However, a built-in function could also be applied to a constant to get the name of the constant,
// a type to get the name of the type, etc, and in these cases the return value should perhaps be 
// a string, and not a slice of strings. This cannot be achieved with reflection alone. 
func nameof(any interface {}) []string {
	names := []string{}
	typ := reflect.TypeOf(any) 
	switch typ.Kind() { 
		case reflect.Ptr:
			typPtr := typ.Elem()
				switch typPtr.Kind() {
					case reflect.Struct:						
						for i := 0 ; i < typPtr.NumField(); i ++ { 
							field := typPtr.Field(i)
							names = append(names, field.Name)
						}								
					// more useful cases could be added here
					default:
						parts := strings.Split(typPtr.String(), ".")						
						return parts[len(parts)-1:len(parts)]
				}
		case reflect.Struct:
			for i := 0 ; i < typ.NumField(); i ++ { 
				field := typ.Field(i)
				names = append(names, field.Name)
			}
		// more useful cases could be added here
		default:
			parts := strings.Split(typ.String(), ".")
			return parts[len(parts)-1:1]
	}
	return names
}

func (ci *CartItem) InsertNameof(db *DB) error {
	// Get the field names at compile time. Less chance for error.
	query := "INSERT INTO CartItem (" + strings.Join(nameof(&ci), ", ") +  ") VALUES (?, ?, ?)"
	err := db.Exec(query, ci.Name, ci.Stock, ci.Price)
	return err
}


func main() {
	db := &DB{}
	ci := &CartItem{ "Mouse", 42, 12 }	
	ci.Insert(db)
	ci.InsertNameof(db)
	
	
	// For a struct pointer nameof() returns the names of the fields
	fmt.Printf("nameof(ci): %s\n", nameof(ci))
	
	// For struct nameof() also returns the names of the fields
	fmt.Printf("nameof(*ci): %s\n", nameof(*ci))
	
	// Use a double struct pointer to get the name of the struct itself.
	fmt.Printf("nameof(&ci): %s\n", nameof(&ci))
	
	// Not possible, but nameof should return "CartItem"	
	// fmt.Printf("nameof(&ci): %s\n", nameof(CartItem))
	
	// Not possible, but nameof should return "Price"
	// fmt.Printf("nameof(&ci): %s\n", nameof(CartItem.Price))
	
	// Not correct nameof should return "Price", not "uint"
	fmt.Printf("nameof(&ci): %s\n", nameof(ci.Price))
}

https://play.golang.org/p/ZhE2LgD9vLs

  • What is the cost of this proposal? (Every language change has a cost).
    The function has to be implemented, documented and tested.
    • How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?
      None, unless if we need some vet checks for abuse of nameof().
    • What is the compile time cost?
      Low, because only if the function is used, the compiler has at do a few type lookups, but it should already have that data available.
    • What is the run time cost?
      None, the whole point is to have this functionality at compile time.
  • Can you describe a possible implementation?
    See above for a partial run time simulation. More details need to be worked out for non-struct uses.
  • How would the language spec change?
    The Built-in_functions section would need to get a sub chapter on "Obtaining Names" or such.
  • Orthogonality: how does this change interact or overlap with existing features?
    It somewhat overlaps with reflect, but nameof() could also fetch names of non-expressions, which is not possible using reflect.
  • Is the goal of this change a performance improvement?
    Not directly, but it could be expected, since it wold lessen the need for using reflection.
    • If so, what quantifiable improvement should we expect?
      Some minor perfomance increase for the use cases of nameof().
    • How would we measure it?
      Comparing a simulated nameof() based on reflection whith a compiler based built-in function
  • Does this affect error handling?
    No.
  • Is this about generics?
    No.

As for how to deal with expressions that really have no names themselves, like nameof(0), nameof(f()) ofr f() Foo, in these cases, I propose nameof() would return the names of the types of the expression, so this is "int" and "Foo", respectively. In fact every for every non-constant expression the name is that of the type or fields of the type. For const and types, the name of the const or the type is returned.

@gopherbot gopherbot added this to the Proposal milestone Jan 31, 2020
@ianlancetaylor ianlancetaylor changed the title proposal: add nameof() built-in function proposal: Go 2: add nameof() built-in function Jan 31, 2020
@ianlancetaylor ianlancetaylor added v2 An incompatible library change LanguageChange Suggested changes to the Go language labels Jan 31, 2020
@ianlancetaylor
Copy link
Contributor

As I understand this, nameof returns information about the type of its argument. nameof(&v) returns the name of the type of v, I guess as a string. If v has a struct type, then nameof(v) returns the names of the fields of v, I guess as a []string. Or maybe the first case returns a []string with a single element. I'm not clear on what nameof(v) returns if v does not have a struct type.

This is not quite what I was expecting, and it's interesting because, as you show, we can pretty much implement it using the reflect package. I think that even an individual field name could be returned if we provide two arguments: nameof(&ci, &ci.Price).

If that really does the job then it seems to me that we should start by writing this as a function, or set of functions, in the reflect package. Then if efficiency becomes a concern we can consider hoisting that function to become a compiler intrinsic, much as we've done with the sync/atomic and math/bits functions.

@ucirello
Copy link
Contributor

@beoran - maybe you could elaborate why %T in fmt.*rintf calls isn't enough?

https://play.golang.org/p/NZ0smcD2taK

@pierrec
Copy link

pierrec commented Jan 31, 2020

I use reflect for this kind of use case and cache/pre-compute the field names at init() time to avoid recalculations. Or use code generation if I really feel like it.

@creker
Copy link

creker commented Jan 31, 2020

This is very confusing. Opening this proposal I expected to see something akin to C# nameof

var someVariable = "foo";
var name = nameof(someVariable); //name equals to "someVariable"

And that would indeed be extremely useful. In C# I constantly use that feature for things like property binding, error messages, logging. In Go this would be useful to write ORM-like code where you often have to pass column names explicitly. That's very error prone.

@beoran
Copy link
Author

beoran commented Jan 31, 2020

@ianlancetaylor, yes, the idea of nameof is that it returns type name information and field name about the arguments as a string or an array for strings. Maybe nameof is not the good name of this idea, but nameoftype or similar is a bit long. The name can be bike shedded later. I agree a second argument could be added that changes how nameof works.

For struct values it is as you surmise, a nameof(&value) returns the name of the type of the value as a single string or an array of strings, I don't care much either way. And nameof(value) returns the names of the exported fields of the struct. As for your example nameof(&ci, &ci.Price) would then be "uint", while nameof(ci, ci.Price) would be "Price". If nameof really becomes built-in then nameof(&ci.Price) and nameof(ci.Price) could be also possible.

As for non-struct values, I would propose to continue the scheme we are developing here: nameof(&something) returns the name of the type of something as a string, nameof(something) returns the name of either something itself, or for interfaces, the names of the interface's methods. So, type Bar int; const Foo = Bar(7)0 ; nameof(Foo) evaluates to "Foo" , and nameof(&Foo) evaluates to "Bar". And a := int64(7) ; nameof(a) evaluates to "a", and nameof(&a) evaluates to "int64".

It's true that a part of the functionality of nameof could be implemented using reflection, and that could be a first step, basically as a utility function in "reflect". But, as a built-in function, it could also be used in situations where no value is produced normally, and, the result of nameof could be a compile time constant, probably if we allow arrays (not slices) to be constants as well.

@ucirello, for ORM's the name of the type of a struct is most conveniently used when not prefixed by the package name, while %T prefixes the package name. Aslo %T doesn't allow getting the names of the exported fields of a struct value or type.

@pierrec, I tend to use code generation most of the time, but that goes to show that it's somewhat of a missing feature in Go.

@creker, I can see what you are getting at, perhaps the functionality I am thinking of is too confusing. I agree nameof() could also be more like the C# #nameof if most people agree that this would be better then my current idea. I do wonder then how we could get the names of all fields in a struct, as AFAIK, C# doesn't support that, and that was something that was asked for in the original issue. But anyway I would like to hear more ideas on how nameof should work, and how it could be useful to others, so we can improve this proposal together.

@creker
Copy link

creker commented Jan 31, 2020

@beoran doesn't reflect already cover the "get the names of all fields in a struct"? From my experience, field names are not enough. You want to get both names and pointers to fields so that you can modify them. That's how some ORM-like libraries work.

@beoran
Copy link
Author

beoran commented Feb 2, 2020

@crecker, yes, considering it, my proposal is mxing up two things, although these came from the original proposal, namely getting the names of a struct's fields, which could probably be something like reflect.FieldNameOf(), and nameof() which should be closer to C#'s #nameof.

I could change this proposal and start with C#'s definition, adapted from here: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/nameof

nameof built-in function

The nameof built-in function obtains the name of a variable, type, or member
as a string constant:

fmt.Println(nameof(shopping.CartItem))  // output: CartItem
fmt.Println(nameof(int))  // output: int
fmt.Println(nameof(shopping.CartItem.Price))  // output: Price

var numbers = []int{ 1, 2, 3 };
fmt.Println(nameof(numbers));  // output: numbers
fmt.Println(nameof(len(numbers)))  // output: len
fmt.Println(nameof())  // output: (the empty string)

As the preceding example shows, in the case of a type and a package, the
produced name is not fully qualified.

The nameof built-in function is evaluated at compile time and has no effect
at run time.

You can use the nameof built-in function or ORM, for debugging, or to make
error messages more maintainable:

type Named {
    Name *string
}

func (n *Named) SetName(name *string) error {
    if name == nil {
        return fmt.Errorf(nameof(name) + " cannot be nil" )
    }
    n.Name = name
}

@jimmyfrasche
Copy link
Member

Wouldn't SetName(aNilStrPtrVar) return the error string "name cannot be nil"?

@beoran
Copy link
Author

beoran commented Feb 4, 2020

@jimmyfrasche , If we follow the design of the C# nameof, then yes, since the compiler doesn't know at compile time how the function will be called and what the name of the parameter will be.

@ianlancetaylor
Copy link
Contributor

Should we close this proposal and open a new one that is more like C# nameof?

As noted above, it seems to me that this proposal can already be implemented using the reflect package.

@beoran
Copy link
Author

beoran commented Feb 5, 2020

You're right, therefore I withdraw this proposal and open another for the C#-like nameof in the linked issue.

@beoran beoran closed this as completed Feb 5, 2020
@golang golang locked and limited conversation to collaborators Feb 4, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge LanguageChange Suggested changes to the Go language Proposal v2 An incompatible library change
Projects
None yet
Development

No branches or pull requests

7 participants