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

Open
dantoye opened this Issue Mar 21, 2017 · 27 comments

Comments

Projects
None yet
@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

This comment has been minimized.

Show comment
Hide comment
@bradfitz

bradfitz Mar 21, 2017

Member

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

Member

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

This comment has been minimized.

Show comment
Hide comment
@dsnet

dsnet Mar 21, 2017

Member

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

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

This comment has been minimized.

Show comment
Hide comment
@dantoye

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

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

This comment has been minimized.

Show comment
Hide comment
@randall77

randall77 Mar 21, 2017

Contributor

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

Contributor

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

This comment has been minimized.

Show comment
Hide comment
@ianlancetaylor

ianlancetaylor Mar 21, 2017

Contributor

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.

Contributor

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

This comment has been minimized.

Show comment
Hide comment
@robpike

robpike Mar 21, 2017

Contributor

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

Contributor

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

This comment has been minimized.

Show comment
Hide comment
@dantoye

dantoye Mar 21, 2017

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

dantoye commented Mar 21, 2017

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

@rogpeppe

This comment has been minimized.

Show comment
Hide comment
@rogpeppe

rogpeppe Mar 22, 2017

Contributor

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.

Contributor

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

This comment has been minimized.

Show comment
Hide comment
@robpike

robpike Mar 22, 2017

Contributor

Then propose it, @rogpeppe.

Contributor

robpike commented Mar 22, 2017

Then propose it, @rogpeppe.

@nigeltao

This comment has been minimized.

Show comment
Hide comment
@nigeltao

nigeltao Mar 22, 2017

Contributor

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.

Contributor

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

This comment has been minimized.

Show comment
Hide comment
@rogpeppe

rogpeppe Mar 23, 2017

Contributor

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.

Contributor

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

This comment has been minimized.

Show comment
Hide comment
@bcmills

bcmills Mar 23, 2017

Member

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

Member

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

This comment has been minimized.

Show comment
Hide comment
@bcmills

bcmills Mar 23, 2017

Member

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

Member

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 from Proposal: Give _ behaviour when used on the RHS, specifically for returns and for zeroing variables to proposal: spec: define _ on rhs as zero value Mar 27, 2017

@go101

This comment has been minimized.

Show comment
Hide comment
@go101

go101 Apr 14, 2017

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

go101 commented Apr 14, 2017

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

@tandr

This comment has been minimized.

Show comment
Hide comment
@tandr

tandr 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 :)

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

This comment has been minimized.

Show comment
Hide comment
@mish15

mish15 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

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

This comment has been minimized.

Show comment
Hide comment
@davecheney

davecheney Aug 18, 2017

Contributor

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

Contributor

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

This comment has been minimized.

Show comment
Hide comment
@jimmyfrasche

jimmyfrasche Aug 24, 2017

Member

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.

Member

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

This comment has been minimized.

Show comment
Hide comment
@neild

neild Sep 5, 2017

Contributor

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.

Contributor

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

This comment has been minimized.

Show comment
Hide comment
@bogatuadrian

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

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

This comment has been minimized.

Show comment
Hide comment
@ianlancetaylor

ianlancetaylor Jan 30, 2018

Contributor

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

Contributor

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

This comment has been minimized.

Show comment
Hide comment
@mewmew

mewmew Feb 25, 2018

Contributor

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

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

This comment has been minimized.

Show comment
Hide comment
@ianlancetaylor

ianlancetaylor Jun 19, 2018

Contributor

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.

Contributor

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

This comment has been minimized.

Show comment
Hide comment
@dantoye

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

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

This comment has been minimized.

Show comment
Hide comment
@bcmills

bcmills Jun 20, 2018

Member

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

Member

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

This comment has been minimized.

Show comment
Hide comment
@ianlancetaylor

ianlancetaylor Jun 20, 2018

Contributor

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

Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment