-
Notifications
You must be signed in to change notification settings - Fork 17.6k
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: new builtin to pick from one of two values #36303
Comments
I worry that this would hurt readability. If one sees The only way I can see this being obvious to the reader is with a better name (which, if longer, probably defeats the purpose of the addition), or with added syntax, which I believe has been rejected in the past. I personally don't think this is common and painful enough to warrant adding complexity to the language or builtin package. This is just my opinion, though.
I strongly disagree with this part. Function calls always evaluate all of their arguments before the call takes place, and that's as specified by the language. Edit: to quote the spec:
The special case is when one does |
This is a similar idea to the Personally, I'm strongly in favor of it as I'm fed up of writing five or six lines of code when one will do. I even prefer it to the ternary operator found in many C family languages (C, C++, Java, C# etc) both on basic readability grounds and because I think it makes it less likely that it will be heavily nested as one often finds in the latter languages. However, having said that and despite the good examples you've dug out, I think the chance of it being adopted is virtually nil. The Go team have consistently opposed any form of ternary operator and the last time this was seriously discussed (in #33171) there were more people against it than for it. You can, of course, do something like this already using If and when we get generics, it'll be possible to write something like this yourself. There'll still be no short-circuit evaluation but it'll be fine where the arguments are cheap to evaluate which would account for the majority of use cases. |
@mdevan thanks for taking efforts and gathering these examples! I suspect it is still hopeless, but it would be good to keep trying :) However I'd prefer conditional operator to look like operator. It is similar to what @mvdan says - proposed So perhaps some other syntax could be used? E.g.
though this looks a bit ugly especially without highlighting even for me. anyway it is not as ugly as dedicated if-else with variable |
Seems pretty similar to #31659 (comment). |
About lazy evaluation It's just what you're used to. Do you find anything "wrong" in this code? if len(a) > 0 && a[0] == 42 {
// ..
} Chances are you understood the intention of the author perfectly well, and so did the compiler. However, it was only because you've been relying on and using short-circuit evaluation for god knows how long now. By the same logic as in https://golang.org/doc/faq#Does_Go_have_a_ternary_form, it can be argued that this form: if len(a) > 0 {
if a[0] == 42 {
//..
}
} although longer, is unquestionably clearer. |
This is precisely what I mean. All Go developers are used to function calls evaluating all arguments. If you suddenly break that rule for special cases that look exactly the same, my guess would be that this will cause pain while people readjust and learn the new rules. I'm not saying it's not possible. My point is that I don't think it's worth it, and proposals like this one should generally be clear net wins solving an important problem. I also agree with @alanfo and people in previous threads. Given that generics are a real possibility, why not just wait for that? You can re-open this proposal if generics end up getting rejected. Edit: also remember that changing the language spec to alter function argument evaluation rules would break a lot of tools out there, such as linters, in subtle ways. It seems to me like the bar for such subtle changes to the spec should be high. |
This proposal isn't technically possible with generics. This is because only one of the LHS/RHS is evaluated, however with generics, both sides will be evaluated since it would be a normal function call. This also brings in the issue of the confusion of something like The "generic function" solution is inconsistent with the original |
As several people said above, in Go the function call syntax suggests that all arguments are evaluated before the function call is made. It would be unusual to change that for a single function, even one that is built into the language. Also, there is no strong support for this proposal based on the emoji voting. For these reasons, this seems like a likely decline. Leaving open for four weeks for final comments. |
The use case that I feel makes me miss this the most is when defining multiple fields in a struct. For example, when configuring a database connection: type DbConfig struct {
CACertificate *tls.Certificate
ClientCertificate *tls.Certificate
user string
password string
} How it could be configured with func getConfig(mtlsEnabled, externalCa bool) DbConfig {
return DbConfig{
user: os.Getenv("DB_USER"),
password: os.Getenv("DB_PASSWORD"),
CACertificate: pick(
externalCa,
external.GetCACertificate(),
local.GetCACertificate(),
),
ClientCertificate: pick(mtlsEnabled, credentials.GetClientCertificate(), nil),
}
} How it could be configured with the ternary operator: func getConfig(mtlsEnabled, externalCa bool) DbConfig {
return DbConfig{
user: os.Getenv("DB_USER"),
password: os.Getenv("DB_PASSWORD"),
CACertificate: externalCa ?
external.GetCACertificate():
local.GetCACertificate(),
ClientCertificate: mtlsEnabled ? credentials.GetClientCertificate(): nil,
}
} One way to do it today: func getConfig(mtlsEnabled, externalCa bool) DbConfig {
config := DbConfig{
user: os.Getenv("DB_USER"),
password: os.Getenv("DB_PASSWORD"),
}
if externalCa {
config.CACertificate = external.GetCACertificate()
} else {
config.CACertificate = local.GetCACertificate()
}
if mtlsEnabled {
config.ClientCertificate = credentials.GetClientCertificate()
}
return config
} To me this option is the worst one and the hardest to maintain. It can grow to become a huge function with lots of For a perspective, if you have a redis cluster with Another way to do it today: func getConfig(mtlsEnabled, externalCa bool) DbConfig {
return DbConfig{
user: os.Getenv("DB_USER"),
password: os.Getenv("DB_PASSWORD"),
CACertificate: getCACertificate(externalCa),
ClientCertificate: getClientCertificate(mtlsEnabled),
}
}
func getCACertificate(externalCa bool) *tls.Certificate {
if externalCa {
return external.GetCACertificate()
}
return local.GetCACertificate()
}
func getClientCertificate(mtlsEnabled bool) *tls.Certificate {
if mtlsEnabled {
return credentials.GetClientCertificate()
}
return nil
} This option is better then the previous one in terms of readability, but the logic and rules to return one single configuration struct is scattered in multiple functions.
Now, regarding whether it should be an operator or a special function with lazy evaluation, I agree with @RodionGork that an operator would be better. All the logical operators have short-circuit and behave similar to this case of lazy evaluation, we use logical operators to combine conditions, it sounds reasonable to me that it should be a conditional operator not a function. Edit: Fixed the examples |
I think the operator If Go gets generics, you could trivially write As for the FAQ, it says
Ternary is notoriously unclear, but
There is already another form of control flow in If there's interest, I can break this into a separate issue. I looked to see if anyone else had proposed |
I don't know what |
Sorry, I meant “zero value” rather than “blank”.
|
The main objections to the proposal so far are:
I further propose a modification, backed by a grammar change. It still is a ternary operator in disguise but hopefully more readable and telegraphs short-circuit evaluation too. Best illustrated by examples: // before
r := rune(c)
if c > utf8.MaxRune {
r = utf8.RuneError
}
// after
r := pick utf8.RuneError if c > utf8.MaxRune else rune(c)
// or
r := pick rune(c) if c <= utf8.MaxRune else utf8.RuneError // before
digits = prec
// If no precision is set explicitly use a precision of 6.
if digits == -1 {
digits = 6
}
// after
digits = pick 6 if digits == -1 else prec // example from a comment above
func getConfig(mtlsEnabled, externalCa bool) DbConfig {
return DbConfig{
user: os.Getenv("DB_USER"),
password: os.Getenv("DB_PASSWORD"),
CACertificate: (pick external if externalCa else local).GetCACertificate(),
ClientCertificate: pick credentials.GetClientCertificate() if mtlsEnabled else nil,
}
} Formally, the proposal is to add a new grammar rule:
where Semantically:
|
No change in consensus, so closing. The |
Consider a builtin called
pick
that can pick from one of two given values of the same type, depending on a boolean expression:Yes, it is yet another ternary operator proposal :-), but this time suggested as a builtin similar to
append
ordelete
.The signature of pick would look like:
where
Type
is any(?) type and the compiler would enforce thatlhs
,rhs
and the return value are of the same type - similar to how the 2nd and later arguments ofappend
and the return value are of the same type.Only one of the arguments would be evaluated depending on the result of the boolean expression, similar to an if-else statement.
As for evidence that this can simplify code, here are some examples, all taken from the single Go source code file fmt/format.go:
lines 76-80:
lines 124-132:
lines 370-374:
lines 419-423:
lines 454-458, also lines 481-485:
lines 464-467, also lines 476-479:
lines 526-531:
The text was updated successfully, but these errors were encountered: