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: spec: define _ on rhs as zero value #19642

Closed
dantoye opened this issue Mar 21, 2017 · 30 comments
Closed

proposal: spec: define _ on rhs as zero value #19642

dantoye opened this issue Mar 21, 2017 · 30 comments

Comments

@dantoye
Copy link

@dantoye dantoye commented Mar 21, 2017

Currently, _ only has behaviour on the LHS.

I would like to propose giving it meaning on the RHS as meaning "zero value". This has two main areas of effect.

First, when returning from a function with multiple return values, you can omit the zero-value allocation. For example,

func GetString() (string, error) { return _, errors.New("nostring") }

Understandably you can use named returns as well, but that is less than ideal if you use the named return value as a "scratch space" before deciding to return zero anyway. For example, when building some struct, such as a Request, you might use a named return to build it and, later in the function, come across an error which invalidates your scratch space, in which case you would want to return a zero-value instead of a half-initialized struct with some garbage inside.

Second, when you want to reset a struct, such as when using a pool, you could use _ to restore it to a zero-value.

func Pool() func() (User, func()) {
	var pool User
	// in reality, use some pooling mechanics
	return func() (u User, reset func()) {
		return pool, func() {
			// zero out and return this copy to the pool
			pool = _
		}
	}
}

nil would remain the correct way to create the zero-value for reference types.

@bradfitz
Copy link
Contributor

@bradfitz bradfitz commented Mar 21, 2017

This has been proposed and rejected in the past. I'll try to dig up references.

@gopherbot gopherbot added this to the Proposal milestone Mar 21, 2017
@gopherbot gopherbot added the Proposal label Mar 21, 2017
@dsnet
Copy link
Member

@dsnet dsnet commented Mar 21, 2017

As a consideration, a different language proprosal (#12854) for stronger type elliding may solve what this proposal is trying to do.

Returning the zero value is only painful for structs. The proposal in #12854 would allow the following:

func MyFunc() (otherpkg.ReallyLongStructName, error) {
	return {}, errors.New("not implemented")
}
@dantoye
Copy link
Author

@dantoye dantoye commented Mar 21, 2017

dsnet, the {} syntax would more clearly differentiate it's use from nil, plus it builds on existing syntax. I love it, a definitely improvement over _.

@randall77
Copy link
Contributor

@randall77 randall77 commented Mar 21, 2017

Returning the zero value is only painful for structs, slices, and maps

nil is a fine zero value for slices and maps. "" is fine for strings. 0 is fine for numbers. I think structs are the only types that would benefit from a more succinct zero.
Doesn't seem worth it to me. (But if #12854 solved this as a side effect, that would be ok.)

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Mar 21, 2017

It's interesting to note that we can get an expression for the zero value for any type by using reflect.Zero, but there is no corresponding language construct. The closest we can come in the language is to declare a variable of that type.

@robpike
Copy link
Contributor

@robpike robpike commented Mar 21, 2017

That is interesting, but changing _ to address that disparity feels wrong to me. Not a very scientific comment, I admit.

@dantoye
Copy link
Author

@dantoye dantoye commented Mar 21, 2017

I agree, the {} syntax blows overloading _ out of the water in hindsight.

@rogpeppe
Copy link
Contributor

@rogpeppe rogpeppe commented Mar 22, 2017

Personally, I'm an advocate for a new "zero" identifier, similar to nil
apart from that it can be used for values of any type, not just pointer types.

@robpike
Copy link
Contributor

@robpike robpike commented Mar 22, 2017

Then propose it, @rogpeppe.

@nigeltao
Copy link
Contributor

@nigeltao nigeltao commented Mar 22, 2017

FWIW, I'm still in favor of the original proposal: a new "zero" identifier, just spelled "_".

Just recently, in https://go-review.googlesource.com/c/38280/2/font/sfnt/sfnt.go#867 I changed e.g.
return nil, ErrNotFound
to
return nil, 0, 0, ErrNotFound
and having to distinguish "nil" versus "0" here seems like unnecessary, and unimportant, detail. I'd rather write:
return _, _, _, ErrNotFound

Off-topic, but while I think of it, alternative sugar for this particular line could be
return ... ErrNotFound
but I'm not sure if even I like that idea.

@rogpeppe
Copy link
Contributor

@rogpeppe rogpeppe commented Mar 23, 2017

Then propose it, @rogpeppe.

I'm not sure another proposal would add much value. Perhaps the title of this one could be changed to something like "Proposal: define an identifier that means the zero value for all types", because there are a number of possibilities for spelling, but the gist is the same regardless.

@nigeltao I agree that can be a pain, particularly when there are many return statements and even more so when returning struct types by value. I've come to like this idiom:

func (f *Font) viewGlyphData(b *Buffer, x GlyphIndex) (buf []byte, offset, length uint32, err error) {
	fail := func(err error) ([]byte, int, int, error) {
		return nil, 0, 0, err
	}
	xx := int(x)
	if f.NumGlyphs() <= xx {
		return fail(ErrNotFound)
	}
	i := f.cached.locations[xx+0]
	j := f.cached.locations[xx+1]
	if j < i {
		return fail(errInvalidGlyphDataLength)
	}
	if j-i > maxGlyphDataLength {
		return fail(errUnsupportedGlyphDataLength)
	}
	buf, err = b.view(&f.src, int(i), int(j-i))
	return buf, i, j - i, err
}

That idiom also makes it easier to add and remove return values.
Currently the compiler's not clever enough to make it cost-free,
(well, it wasn't the last time I checked) but I don't see why it shouldn't.

@bcmills
Copy link
Member

@bcmills bcmills commented Mar 23, 2017

@ianlancetaylor

It's interesting to note that we can get an expression for the zero value for any type by using reflect.Zero, but there is no corresponding language construct.

Is there any type T for which

var x = *new(T)

doesn't work?

@bcmills
Copy link
Member

@bcmills bcmills commented Mar 23, 2017

@rogpeppe Another option might be to make nil a valid identifier for the zero-value of all types, since it already represents the zero-value of many types.

I don't actually like that option — nil is confusing enough as it is, and the potential for "I thought that was a pointer but it's not" bugs is high — but it's the zero-value name that would add the least new surface area to the language.

@rsc rsc added the Go2 label Mar 27, 2017
@rsc rsc changed the title Proposal: Give _ behaviour when used on the RHS, specifically for returns and for zeroing variables proposal: spec: define _ on rhs as zero value Mar 27, 2017
@go101
Copy link

@go101 go101 commented Apr 14, 2017

I like the zero predeclared identifier idea.
And I still like the "use _ to mute identifiers" idea: #17389
:)

@tandr
Copy link

@tandr tandr commented Jul 26, 2017

@rogpeppe

I'm not sure if it needs to be a new keyword here. There is iota, and (if to push it) we can reuse default

With iota

type S struct { }
var v1 S = iota // equivalent v1 := S{}
var v2 *S = iota // var v2 *S = nil
var v3 int32 = iota // 0

func f() (S, int, error) {
    return iota, iota, iota // equivalent to return S{}, 0, nil
}

With default

type S struct { }
var v1 S = default // equivalent v1 := S{}
var v2 *S = default // var v2 *S = nil
var v3 int32 = default // 0

func f() (S, int, error) {
    return default, default, default // equivalent to return S{}, 0, nil
}

Personally I like default more :)

@mish15
Copy link

@mish15 mish15 commented Aug 18, 2017

I was going to file an experience report, but found this issue first. I'm definitely in favour of allowing _ as a zero return. Having to return values makes the code less readable and inconsistent with interface/pointer nil returns (i've seen this cause a bunch of bugs). It may also allow the compiler/runtime to detect when a zero value is used when it shouldn't be. e.g.

func getFoo() (foo, error) {
	if noFoo {
		return _, errors.New("no foo")	
	} 
	return foo{}, nil
}
f, err := getFoo()
if err != nil {
	// oops forgot to handle
}
Save(f) // This could panic
@davecheney
Copy link
Contributor

@davecheney davecheney commented Aug 18, 2017

I agree with @nigeltao, let's allow _ to be a standin for a constant or literal zero value in a return statement.

@jimmyfrasche
Copy link
Member

@jimmyfrasche jimmyfrasche commented Aug 24, 2017

I was leery of this proposal because I could only see use for it in return values and for composite literals of non-pointer structs.

But I thought of another good use case: generated code.

Currently, if you need a zero value in generated code where you are generating code using an arbitrary type, you need to either

  • look up the type and build the zero value with a look up table and some rules
  • use var v T

The first is complicated for little benefit and the second can make the code feel awkward.

With a universal zero value, you can just use that.

If Go2 gets generics, the same argument applies except that you would have to use var v T.

@neild
Copy link
Contributor

@neild neild commented Sep 5, 2017

Another possibility might be to extend the composite literal syntax to cover non-composite types as well.

x := int32{0}         // equivalent to x := int32(0)
x := int32{}          // omit the initializer for the zero value
x := int32{1, 2}      // error
x := &int32{}         // equivalent to x := new(int32)
x := &int32{1}        // obviates proto.Int32
var x int32 = {}      // lightweight zero-values with https://github.com/golang/go/issues/12854
var ch = (chan int){} // equivalent to make(chan int), perhaps?

type T struct {
  ch chan struct{
    s string
    err error
  }
}

t := T{
  ch: {}, // No need to repeat the channel type (assumes #12854 or #21496).
}

I don't think this pulls its weight by itself, but might be of more use with hypothetical Go 2 generics. It could also serve as a replacement for both make and new, which I find intriguing.

@bogatuadrian
Copy link

@bogatuadrian bogatuadrian commented Nov 30, 2017

I saw @davecheney was in favour of this in his comment so I'm wondering if this is still being discussed someplace else? If so, a link would be useful.

TL;DR: A representation of zero-values would be useful regardless of the syntax used.

My take: I have experienced what @dantoye was "complaining" about multiple times, and without even knowing about this thread, I imagined that a good token that could solve this problem would be _. I don't really care about what token would be used instead of _ (maybe a community-wide poll would help with this, as long as we won't get to _yMc_face), but it would be very helpful to have this "shorthand".

Also I don't see a problem with it given that it doesn't break compatibility with Go 1 (given that the previous token usage was LHS, although this might cause a little confusion). The only downside is the possible confusion when you have code like:

_, err := foo(42)
if err != nil {
        return _, err
}

But I think go developers are smart enough to understand this, and won't make any pythonish assumptions.

In addition, things like:

if duration == _ {
        doThat()
}

could be useful (again, not necessarily suggesting the _ construct, but the concept behind, i.e. a RHS representation of a zero value)

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Jan 30, 2018

Per the comment above about an expression that produces the zero value of a type (which can be done using *new(T)), if we ever support some form of generics we are going to want some way to zero out a variable of a generic type. That is, given

var v T

for some generic type T, it will sometimes be desirable to write

v = 0

but of course T might actually be string, in which case that won't compile, so one would have to write

v = *new(T)

but that seems absurd. So one advantage of this proposal, if we get generics, is the ability to write

v = _

(or any other notation for a general zero value).

@mewmew
Copy link
Contributor

@mewmew mewmew commented Feb 25, 2018

A few different alternatives.

// analogous to tagged struct literals.
func f() (X: int, Y: float64, E: error) {
	return X: 42
	return X: 12, Y: 3.14
	return E: errors.New("bar")
}

// _ as zero value of type (i.e. reflect.Zero).
func g() (int, float64, error) {
	return 42, _, _
	return 12, 3.14, _
	return _, _, errors.New("bar")
}

// `zero` predeclared identifier as zero value of type (i.e. reflect.Zero).
func h() (int, float64, error) {
	return 42, zero, zero
	return 12, 3.14, zero
	return zero, zero, errors.New("bar")
}

// i special case error return value to use zero value for excluded return
// arguments.
//
// Note, functions with multiple error values have to be handled specifically in
// the specification; either use only last error, force all error values to be
// returned.
//
//    func j() (error, int, error) {
//       // only last error
//       return errors.New("bar")
//       // force all error values
//       return errors.New("foo"), errors.New("bar")
//    }
//
// Note, this example is not one that we would endorce, however it was included
// as it was part of our discussion and helped us iterate upon different
// alternatives.
func i() (int, float64, error) {
	return 42, 0, nil
	return 12, 3.14, nil
	return errors.New("bar")
}

As a follow up to the example analogous to tagged struct literals, it would be interesting to extend this concept to function parameters as well.

func j() {
	k(A: 13)
	k(B: 3.14)
	k(B: 3.14, A: 42)
	k(42, 3.14)
}

// analogous to tagged struct literals.
func k(A: int B: float64) (X: int, Y: float64, E: error) {
	return X: 42
	return X: 12, Y: 3.14
	return E: errors.New("bar")
}
@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Jun 19, 2018

Would _ be permitted only in an assignment or return statement, or would it be permitted in other cases? Would it be OK to write

if v == _ { fmt.Println("zero") }

If we permit this kind of operation, then _ has some of the same confusions as nil.

Another comment. If we use _, then we can see code like

_ = x
return _

That seems potentially confusing.

@dantoye
Copy link
Author

@dantoye dantoye commented Jun 20, 2018

@ianlancetaylor Most simply, _ only has behavior on the LHS right now.

This proposal simply defines RHS behavior as an untyped const roughly translating to "zero value". Identical to how an untyped const 123 has different meaning to different numeric Kinds, _ would have different meaning to all Kinds, and always mean the 0 value of that type.

There is no ambiguity or conflict. if v == _ is using _ as a RHS. _ = x is being used as LHS. return _ is being used as a RHS.

@bcmills
Copy link
Member

@bcmills bcmills commented Jun 20, 2018

I tend to think about Go expressions (including variables) in terms of the sets of values they can take. (In mathematical terms, Go expressions are a join-semilattice.) At the moment, the expression _ can take any value at all: it is the top of the value lattice. In contrast, zero-values are very near the bottom: for any given T, there is only one possible value for *new(T).

This proposal would add a second meaning for _: as an lvalue it means “the top of the expression lattice”, and as an rvalue it means “the top of the zero-value lattice”. Those are very different meanings.

With the meaning of “top of the expression lattice”, then @ianlancetaylor's example of v == _ should mean “v is equal to any value”: that is, it should mean true. On the other hand, with the meaning of “top of the zero lattice”, then it should mean “v is equal to the meet of the type of v and the top of the zero-value lattice”: that is, it should mean v == 0 or v == nil or similar.

So, under this proposal, in order to determine what _ means, you would have to first determine whether it is an lvalue or an rvalue. I am aware of only one other place in the language where the meaning of an expression changes depending upon whether it is an lvalue or an rvalue (#20660 (comment)), and I think it's confusing there too.

If we're going to choose some identifier to mean “any zero-value”, I don't think we should use one that already means “any value at all”.

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Jun 20, 2018

@dantoye I did not mean to suggest that there is any ambiguity or conflict. I meant to say that if we permit if v == _ { } while also permitting v = _ then we are creating another instance of https://golang.org/doc/faq#nil_error, with _ taking the place of nil.

@griesemer
Copy link
Contributor

@griesemer griesemer commented Feb 5, 2019

The only place where this will really be used is in a return statement of a function with multiple results. In that particular case, it may be more convenient for the writer of the code to be able to write just '_'`s, but readability is definitively not improved. It gets worse if there are many return values. If there are few return values, it is often better to write out the result values.

Finally, there is still the option to use a naked return which is actually better if there are many return values because of the documentation of the return values in the signature.

@griesemer griesemer closed this Feb 5, 2019
@bogatuadrian
Copy link

@bogatuadrian bogatuadrian commented Mar 30, 2019

@griesemer

If there are few return values, it is often better to write out the result values.

If you use a return type from a different package it might not be worth to investigate that certain type

For example:

func foo() somepackage.Type

In this case, the developers of the current package should not be concerned with what that type represents. It might be a type definition like type Type string with some exported consts; as a developer you would have to check that type (or an arbitrary number of type definitions that lead you to the true type) and determine that final type's zero-value to finally write your code.

Finally, there is still the option to use a naked return which is actually better if there are many return values because of the documentation of the return values in the signature.

What happens if you actually assign a value to a return variable?

func foo() (i int) {
	i, err := bar()
	if err != nil {
		return
	}
	return
}

Maybe some 3rd party library bar (incorrectly) sets i on error. As a developer I would be inclined to write return 0, err on error, but that leads us to my first point.

(opionion) Regarding redability, if this kind of a feature would be accepted and people would know about it, I think it wouldn't be very hard for us to understand when we see return _ (or any other marker instead of _).

Nevertheless, I see no significant difference of readability between say return 0, nil, false and return _, _, _. If you have a path where you know you have to return zero-values for certain variables/positional-returns, does it matter that much what type they were?

P.S. Why was this issue closed?

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Mar 31, 2019

@bogatuadrian The issue was closed because we decided against adopting this proposal.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
You can’t perform that action at this time.