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: Safe navigation operator (?.) #42847

Closed
kevburnsjr opened this issue Nov 26, 2020 · 23 comments
Closed

proposal: Go 2: Safe navigation operator (?.) #42847

kevburnsjr opened this issue Nov 26, 2020 · 23 comments

Comments

@kevburnsjr
Copy link

@kevburnsjr kevburnsjr commented Nov 26, 2020

Proposal

Add a new operator (?.) to support safe navigation.

Example

package main

type a struct {
	b *b
}

type b struct {
	c int
}

Current Behavior

Navigation across pointer of nil value causes runtime panic.

func main() {
	x := a{&b{1}}
	y := a{}
	println(x.b.c)
	println(y.b.c)
}

x.b.c evaluates to 1
y.b.c panics

1
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x45db4c]

goroutine 1 [running]:
main.main()
	/tmp/sandbox869110152/prog.go:16 +0x6c

Proposed Behavior

Safe navigation across pointer of nil value evaluates to nil value of same type as target property.

func main() {
	x := a{&b{1}}
	y := a{}
	println(x.b?.c)
	println(y.b?.c)
}

x.b?.c evaluates to 1
y.b?.c evaluates to 0 (nil value of type similar to c)

1
0

Reasoning

Null property traversal is a common cause of runtime panic. Frequently developers must disrupt the flow of their program by adding nil checks in order to reproduce the behavior of a safe navigation operator. These nil checks add to the maintenance cost of the program by making the code less readable and introduce new opportunities for error.

Current idiom

func (a *a) getC() int {
	if a.b == nil {
		return 0
	}
	return a.b.c
}

Proposed idiom

func (a *a) getC() int {
	return a.b?.c
}

Similar features in other languages

See https://en.wikipedia.org/wiki/Safe_navigation_operator

Also known as

  • Optional chaining operator
  • Safe call operator
  • Null-conditional operator

Go 2 language change template

  • Would you consider yourself a novice, intermediate, or experienced Go programmer?
    Experienced

  • What other languages do you have experience with?
    PHP, Javascript, Java, TCL, Bash, Actionscript, Erlang, Python, C++

  • Would this change make Go easier or harder to learn, and why?
    Adds one more operator to learn which should feel familiar to people having experience with analogous operators in other languages popular among Gophers (C#, Ruby, Python, PHP, Typescript, Rust, Scala).

  • Has this idea, or one like it, been proposed before?
    Not found in github issues. One slightly similar golang-nuts post from 2013.

  • If so, how does this proposal differ?
    Previous discussion appears to relate specifically to null pointer receiver methods rather than nested struct traversal or command chaining.

  • Who does this proposal help, and why?
    Developers moving to Go from languages that already support safe navigation.

  • What is the proposed change?
    Add operator ?. (see above).

  • Please describe as precisely as possible the change to the language.
    Add token QUES_PERIOD = "?.". Modify selector expression or add new safe selector expression to achieve behavior described above. Not valid on left hand side.

  • What would change in the language spec?
    Expansion of selector expression definition and operator list

  • Please also describe the change informally, as in a class teaching Go.
    The safe navigation operator?. can be used to short circuit property traversal when a nil pointer is encountered, avoiding panic.

  • Is this change backward compatible?
    Yes.

  • What is the cost of this proposal? (Every language change has a cost).
    Higher maintenance costs due to increase in number of operators and more complex selector expression code.

  • How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?
    Possibly several. Depending on implementation, maybe none.

  • What is the compile time cost?
    Presumably low

  • What is the run time cost?
    Presumably low

  • Can you describe a possible implementation?
    Add new token QUES_PERIOD = "?.". Then either modify ast.SelectorExpr or add new ast.SafeSelectorExpr

  • Do you have a prototype? (This is not required.)
    In development

  • How would the language spec change?
    Expansion of selector expression definition and operator list

  • Orthogonality: how does this change interact or overlap with existing features?
    Syntactically identical with . operator in selector expressions except where the value of the expression is nil (where the . operator would panic)

  • Is the goal of this change a performance improvement?
    No.

  • Does this affect error handling?
    No.

  • Is this about generics?
    No.

@gopherbot gopherbot added this to the Proposal milestone Nov 26, 2020
@gopherbot gopherbot added the Proposal label Nov 26, 2020
@mvdan
Copy link
Member

@mvdan mvdan commented Nov 26, 2020

Please note that you should fill https://github.com/golang/proposal/blob/master/go2-language-changes.md when proposing a language change.

@ianlancetaylor ianlancetaylor changed the title proposal: Go2: Safe navigation operator (?.) proposal: Go 2: Safe navigation operator (?.) Nov 26, 2020
@mdempsky
Copy link
Member

@mdempsky mdempsky commented Nov 26, 2020

A few points that I see in need of consideration for this proposal:

  1. Because of struct embedding, a x.y field reference can actually dereference multiple pointers. I assume the semantics of x?.y would provide the zero value if any of the pointers are nil.

  2. What if x.y involves no pointer dereferences? Is it still valid to write x?.y?

  3. What are the semantics of x?.y.z? Does it mean x ? x.y.z : 0 or (x ? x.y : 0).z? (I think in JS it means the former, but that in some other languages it means the latter.)

  4. It looks like some languages also provide ?[ for safe indexing and ?( for safe function/method calls. I'm struggling a little to think how these would actually be used in idiomatic Go code, but seems like a possible future direction to keep in mind.

  5. I think this also relates to previous proposals like #37165. I seem to also remember a proposal for extending && and || to non-boolean types, but I'm not able to find it at the moment.

--

Personal opinion: I'd find this occasionally handy (e.g., I wrote this code yesterday, and I could have replaced 5 lines with just w.string(n.Sym()?.Name)). It's prevalence in other languages suggests it's something worthwhile to consider. On the other hand, Go has eschewed the ternary ?: operator (which is also common in other languages) and instead generally favored more explicit if statements. (For my part, I also occasionally wish Go had ?:.)

@kevburnsjr
Copy link
Author

@kevburnsjr kevburnsjr commented Nov 27, 2020

A few points that I see in need of consideration for this proposal:

  1. I see.
package main

type a struct{ *b }
type b struct{ *c }
type c struct{ d int }
func main() {
	println(a{&b{&c{1}}}.d) // 1
	println(a{}.d)          // panic
}

The goal is to replace nil pointer dereference panics with nil values of the expected type. I don't think there's any ambiguity in these scenarios about the type to which the expression should evaluate. Still seems feasible?

  1. I think yes. It would emulate . semantics, but should probably only be allowed on the right hand side since safe navigation doesn't make sense for assignment.

  2. So it's either x.y resolves ? x.y.z : 0 or (x.y resolves ? x.y : <nil value of type y>).z The latter is probably simpler to implement. Javascript short circuits the whole expression to untyped null (iirc) which is not feasible in a strongly typed language like Go.

  3. Yes, that might be a natural progression. I chose to omit those operators to constrain the scope of the proposal, focusing on whether safe navigation should be deemed worthy in general.

  4. #37739 lazy values

@gopherbot please remove label WaitingForInfo.

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Nov 27, 2020

So it's either x.y resolves ? x.y.z : 0 or (x.y resolves ? x.y : ).z The latter is probably simpler to implement. Javascript short circuits the whole expression to untyped null (iirc) which is not feasible in a strongly typed language like Go.

The question is not what is simpler to implement, but what people reading code will naturally expect. It's hard for me to see that x?.y.z means anything other than (x ? y : <zero value of y's type>).z, but if y is a pointer type then nobody would want to write that.

@kevburnsjr
Copy link
Author

@kevburnsjr kevburnsjr commented Nov 27, 2020

True. x?.y?.z might be more common where y is a pointer type.

@mdempsky
Copy link
Member

@mdempsky mdempsky commented Nov 27, 2020

It seems I was mistaken about JavaScript. It looks like ECMA standardized that ({})?.x.y fails with a "Cannot read property y of undefined" error.

But at least according to these TypeScript docs, foo?.bar.baz() means (roughly) foo ? foo.bar.baz() : undefined: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html

I agree with Ian that the JS behavior seems more natural (at least from a language implementor's perspective). But the TypeScript semantics do seem more useful, for the same reason Ian points out. (I assume you could write (foo?.bar).baz() if you really wanted the JS behavior.)

@hznuksco
Copy link

@hznuksco hznuksco commented Nov 28, 2020

I think If the ?. proposal is passed, it would be good to realize the ternary ?: operator as well. hah

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Dec 1, 2020

It's worth noting that if we adopted this it would be the first use of ? in Go.

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Dec 1, 2020

Should we also add x ?/ y which does not panic if y is 0 for integer types?

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Dec 1, 2020

a?[i] would not panic if i were out of bounds for a slice or array a.

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Dec 1, 2020

Using the current generic design draft, we could use a generic function for this, although the syntax is more awkward.

func Deref[T any](p *T) T {
    if p == nil {
        var zero T
        return zero
    }
    return *p
}

This would be used as Deref(a.b).c, which would be equivalent to this proposal's a.b?.c.

@mdempsky
Copy link
Member

@mdempsky mdempsky commented Dec 1, 2020

Is there a generics syntax for a?.b.c with its TypeScript semantics (i.e., a ? a.b.c : 0)? I can't immediately think of any.

@DeedleFake
Copy link

@DeedleFake DeedleFake commented Dec 2, 2020

a?[i] would not panic if i were out of bounds for a slice or array a.

I'm not sure how serious you are, but I think it would be more consistent to do this with a double assignment, like with maps:

v, _ := a[i]

It's a bit more awkward to stick into the middle of a call where the zero value'll work fine, though. It requires a second line of code.

Edit: You could also do this with a generic function, though:

func At[T any](a []T, i int) T {
  if (i < 0) || (i >= len(a)) {
    var z T
    return z
  }
  return a[i]
}

This also has the benefit of automatically handling nil slices, thanks to len([]T(nil)) being 0.

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Dec 2, 2020

@mdempsky If I understand you correctly, that is Deref(a).b.c. Am I missing something?

@mdempsky
Copy link
Member

@mdempsky mdempsky commented Dec 2, 2020

If b is a pointer type, that will panic due to nil pointer dereference. The TypeScript semantics are that the selection chain gets short-circuited if the LHS of the ?. operator is nil. That is, a?.b.c means a ? a.b.c : 0, not (a ? a.b : 0).c.

@kevburnsjr
Copy link
Author

@kevburnsjr kevburnsjr commented Dec 2, 2020

This would be used as Deref(a.b).c, which would be equivalent to this proposal's a.b?.c.

@ianlancetaylor True. Deref appears a concise and complete expression of the expected behavior of the proposed operator.

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Dec 2, 2020

@mdempsky Ah, OK, sorry for misunderstanding. But I don't think those semantics make sense for Go.

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Dec 15, 2020

Based on the discussion above, this is a likely decline. If generics are not added to the language, we can revisit. Leaving open for four weeks for final comments.

@alexbuicescu
Copy link

@alexbuicescu alexbuicescu commented Dec 23, 2020

a?[i] would not panic if i were out of bounds for a slice or array a.

I find a?.[i] a bit more natural and I think this would also be easier to implement. a?[i] could be harder to understand by the compiler as it might interpret it as a ternary operator and it could throw an error that you are missing the : operator

eg of what compiler wants you to write: a?[i]:[i+1]

@alexbuicescu
Copy link

@alexbuicescu alexbuicescu commented Dec 23, 2020

To make the safe navigation operator even more useful would be the addition of "nullable" types. I love how in typescript I can specify that a type can be nullable and then I'm forced to do a null check or use the safe navigation operator when using the object.

If nullable types are implemented then my view of the safe navigation operator would be to return nil (in case of a nil pointer along the way) or the wanted value (when all pointers are declared), eg:

type A struct {
  x int
}

var obj *A | nil
println(obj?.x) // nil
nullableX := obj?.x // nullableX's type would be "int | nil"

The takeaway would be that nullable types would directly impact the implementation of the safe navigation operator because it changes its return type

The closest issue I could find regarding this would be #19412

@vetcher
Copy link

@vetcher vetcher commented Dec 25, 2020

I also want to mention, that generic Deref does not resolves an issue with navigation in my projects. Commonly we write code that looks like this:

		return PharmacyNetworkRequest{
			Title:           order.GetPharmacyNetwork().GetTitle(),
			Phone:           strings.Join(order.GetPharmacyNetwork().GetPhones(), ", "),
			Image:           imageLinker(order.GetPharmacyNetwork().GetImages()),
			DeliveredAtText: orderDeliveryAtFormat(order.Params.DeliveryAt),
		}

where methods Get*() looks like protoc-gen-go generates:

func (o *Order) GetPharmacyNetwork() *PharmacyNetwork {
	if o == nil {
		return nil
	}
	return o.PharmacyNetwork
}

func (pn *PharmacyNetwork) GetTitle() string {
	if pn == nil {
		return ""
	}
	return pn.Title
}

func (pn *PharmacyNetwork) GetImages() []string {
	if pn == nil {
		return nil
	}
	return pn.Images
}

If the navigation operator is accepted, we will be able to remove this strange code and replace it with ?.

		return PharmacyNetworkRequest{
			Title:           order?.PharmacyNetwork?.Title,
			Phone:           strings.Join(order?.PharmacyNetwork?.Phones, ", "),
			Image:           imageLinker(order?.PharmacyNetwork?.Images),
			DeliveredAtText: orderDeliveryAtFormat(order?.Params?.DeliveryAt),
		}

With Deref it looks ugly:

		return PharmacyNetworkRequest{
			Title:           Deref(Deref(order).PharmacyNetwork).Title,
			Phone:           strings.Join(Deref(Deref(order).PharmacyNetwork).Phones, ", "),
			Image:           imageLinker(Deref(Deref(order).PharmacyNetwork).Images),
			DeliveredAtText: orderDeliveryAtFormat(Deref(Deref(order).Params).DeliveryAt),
		}
@deanveloper
Copy link

@deanveloper deanveloper commented Jan 5, 2021

As some final input of mine:

I think that my favorite feature of Kotlin, by far, is the ?. operator and all of the null-safe operators/features that Kotlin has. However, the ?. operator isn't very useful in my opinion without the rest of the operators/features that Kotlin has around null safety. The ?. feature really isn't useful to me without a null-coalescing operator to specify what I'd want instead of null, and it isn't clear when it is needed to be used without nullable types. In Kotlin, it's the entire family of null-safety features that make it so powerful, rather than just the ?. operator on its own.

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Jan 12, 2021

There were additional comments, but no change in consensus.

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

Successfully merging a pull request may close this issue.

None yet
10 participants