-
Notifications
You must be signed in to change notification settings - Fork 17.8k
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: spec: conditional-assign (?=) for limited ternary operator functionality #31659
Comments
I'd honestly prefer for the if and switch statements to be turned into expressions instead. There are quite a few mainstream languages that either started with these things as expressions or migrated to them, and the syntax will not be confusing for a lot of people. And something like this would suffice for ternary:
And of course, if you have a lot of conditions:
|
I like this proposal which is simple, clear, generic, backwards-compatible and would cut down on verbosity whilst avoiding the excesses of the C-style ternary operator. It should also be possible to implement it efficiently in the compiler. In the absence of the ternary operator, I often write a function instead but, lacking generics as well, this isn't a great solution. I wondered at first whether there might be a case for having a TBH I'm not a fan of |
What about the Python way? x := "yes" if true else "no" Some people don't like the |
This is not about the If the syntax is available as part of an expression, then it can be misused. |
if/switch expressions do not help eliminate the drawbacks. By definition, an expression can be nested, so the drawbacks are there. It is just a different syntax for the ternary operator, with all the drawbacks it has when used as part of an expression. The reason this proposal can work, is that the operator cannot be part of an expression, as it is an assignment operator, like |
This is necessary to avoid ambiguity with the syntax you chose, but it is a bit unfortunate. Consider (using the python syntax for clarity only):
The restriction that does the work “to avoid abuse” is that of being a statement instead of an expression, not of having a single variable on the LHS. Some other questions and observations. Can you declare variables this way, using Other syntax in Go like |
Sorry, I didn't mean you were talking about including the ternary operator, I just was saying that writing: var s ?= boolean_expr, ifTrueVal, ifFalseVal Could be easier to read if you write: var s ?= ifTrueVal if boolean_expr else ifFalseVal If you see |
Right. I tried to avoid using expression vs statement because many folks get confused. So I wanted to use lay terms ie RHS LHS that anyone understands. I see Remember: there can only be one variable on the LHS. The syntax is really only applicable for:
Given that |
Key message in the proposal is that this is an operator that doesn't fit into an expression at all, because once it does, it can be misused. See example of misuse: var s ?= trueVal1 if boolean_expr_1 else trueVal2 if boolean_expr_2 else trueVal3 if boolean_expr_3 else falseVal Looking at above, I don't even know if the syntax is correct. The compiler might let me know. And this is why folks do not like the ternary operator when nesting is allowed. By using commas, there cannot be any nesting. The operator is strictly attached to assignment to exactly one variable. It's like a function call that does: func ternary(b bool, s1 string, s2 string) string {
if b {
return s1
} else {
return s2
}
}
var s = ternary(boolean_expr, trueVal, falseVal) This is the limited scope that this proposal allows. |
I'm still undecided whether or not I like the proposal, but as for @urandom's proposed syntax:
This doesn't necessitate having if/switch expressions. Instead, this could be a peculiar form of assignment: an assign-if or assign-switch. Essentially, For example, this would allow Note that the true/false arms of an assign-if would have to be expressions, not the statement lists you can have in an if statement's arms. The Avoiding the commas of the original proposal also lets us allow multiple assignment:
Saying Separately, gofmt could possibly allow (but not mandate) the 1-liner form:
This would be similar to allowing (but not mandating) 1-liner functions. Both of these are acceptible to gofmt:
Having said all that, though, an assign-if concept would be quite unusual. While convenient, it doesn't feel orthogonal to the other language features. |
Another, slightly more general way to have something similar would be to introduce the if modifier at the end of a statement, much like in Ruby. An if at the end of a statement is executed only when the if condition is true. This would be backwards compatible as well. So: func FooOrBar(isBar bool) {
var a string
a = "bar" if a = "foo" ; isBar
return a
} would then be syntactic sugar for this: func FooOrBar(isBar bool) {
var a string
if a = "foo" ; isBar {
a = "bar"
}
return a
} Well, just a wild idea. |
Well there may be different ways of looking at this but the fact remains that, as soon as you try to use if/else and switch in this fashion, various difficulties present themselves and it defeats the basic purpose of this proposal (to express simple conditionals as one liners in a Go-like way). Frankly I'd rather things were left as they are than reuse the conditional keywords in this way. For me the beauty of this proposal is that it addresses what is clearly a pain-point for many people (you only have to look at discussions on golang-nuts to see that this is so) and does so in a simple way which avoids the main objection to having a 'standard' ternary operator in the first place. It's not as if this is a rare scenario - it comes up all the time. Personally, I don't see it as a drawback that you can't assign values to more than one variable at a time with this proposal as, apart from simple swaps, I regard assignments where the variables refer to each other on the RHS as potentially confusing any way. |
@nigeltao wrote:
I actually do like this, and I will be fine if this is an accepted substitute. I like how it can allow for multiple assignments. However, when I write them side by side, the ?= looks simpler. See: a = if valueIsOk { "abc" } else { "def" }
a ?= valueIsOK, "abc", "def" There is also more possible confusion with a = if valueIs1 { "a1" } else if valueIs2 { "b2" } else { "b3" }
a = if valueIs1 { "a1" } else { if valueIs2 { "b2" } else { "b3" } }
func doSomething(s string) { ... }
doSomething(if valueIs1 { "a1"} else { "b2" })
Finally, I also love that go is about orthogonality. I agree that the However, as an operator, it is still kinda orthogonal, as it is just fulfilling a missing feature available in other languages, like var valueIsOk boolean
var s ?= valueIsOk ? "s1" : "s2"
// is just syntactic sugar for
var s string
if valueIsOk {
s = "s1"
} else {
s = "s2"
} |
@nigeltao I like your syntax; I share your uncertainty about whether I like the proposal. FWIW, here are a few interesting cases. Comma, ok variant: var m map[string]int
x, ok := if b { m["abc"] } else { m["def"] }
x := if c := f(); c != 0 { ... } else { ... } Using the results of a function call returning multiple values: func f() (int, int)
x, y := if b { 0, 0 } else { f() } It seems like all of these are things we would probably want to permit, and the semantics for them seem reasonably clear. |
This proposal doesn't mention anything about short-circuiting (ala
Are This has a large impact on the syntax; the syntax should be consistent with the answer. When I read
Implies that only one of |
Another interesting case: mixed type inference. x := if b { 0 } else { byte(1) } Presumably, EDIT: Even more interesting is x := if b { 0 } else { 1.5 } |
Regarding @randall77's point, I think the proposed operator should definitely short-circuit. This is the more efficient option and is what people will expect. I don't see why there is a consistency problem with:
as it's not a multi-assignment - there's only one variable on the LHS. Turning now to @josharian 's examples, I think that type inference should take place from left to right and so neither example should compile. In both cases Of course, if x had already been declared to be a |
Yes, but on the RHS there's no visual distinction between the three values. They are just three comma-separated expressions, just like the RHS of a multiassignment or the args of a function.
So in |
An alternative, which fits a little better with Go's preference for explicitness, is to typecheck both halves independently, and fail if they are different. That will also make the job of refactoring (automated or not) easier. Among other things, it means that it is sound to invert the condition and flip the slots. |
OK, I understand the point you're making and, if a succinct way can be found to achieve that, I'd have no problem with it. The way you've suggested should work and another possibility (borrowing some syntax from the ternary operator itself) would be:
|
I agree that it would be preferable to look at the 'true' and 'false' values independently as there would then be no distinction in the programmer's mind between assigning to a new implicitly typed variable, to a new explicitly typed variable or to an existing variable. However, the problem is that when untyped numeric literals are used (which would be commonplace if this proposal, or something like it, were accepted) the 'true' and 'false' values aren't necessarily independent and the compiler might therefore need to examine the matter from both perspectives when performing type inference to see whether a 'matching type' existed. So, in the examples you cited earlier, there would be no match from the 'true' value's perspective but there would be a match from the 'false' value's perspective. So I think there would be more work for the compiler compared to what I suggested earlier, though it should still be doable. |
Typechecking is cheap. |
Whilst that's true when you only have a single expression, as you said yourself in an earlier post this would be the first time that dual type inference were needed. However, if generic functions are eventually introduced on something like the lines of the draft design paper, then you'd have exactly the same problem there - except worse as you could have any number of parameters to deal with. For example, if one introduced a generic function to replace the ternary operator, like so: func iff(type T) (cond bool, trueValue, falseValue T) T {
if cond {
return trueValue
}
return falseValue
} then it would be disappointing if one couldn't write this: x := iff(b, 0, 1.5) where Incidentally, I'm not advocating the use of such a function instead of this proposal as the former would suffer from the dual problems of permitting nesting of 'iff's and (unless it was a built-in) there would be no short-circuiting - all parameters would need to be evaluated. However, it's something I would consider doing for my own code rather than having to write multiple lines for simple conditionals. |
I would also like to see a ternary operator in Go. I am writing some code where the structure I am processing may or may not have been initialised e.g. tmp_Tsimple_14 := &Simples{ If the fields within params.Body have not been set I obviously get runtime errors. I would like to be able to generate code such that I can use a ternary condition to ensure I can pass the field e.g. tmp_Tsimple_14 := &Simples{ |
That seems like more of an issue for |
Data point; I use: Sadly that always evaluates and copies (I ban go fmt in my projects for this and other reasons.) @robpike is on record that (paraphrased) "Go does not allow control flow in expressions." But in fact, it does, as |
One important question for all language change proposals is: What can you do significantly more easily if the proposal is adopted? There is one case in which this syntax accomplishes something more than saving a line or two of code: initializing global variables. Consider this at the top level: var v = if b { x } else { y } Currently this initialization requires an |
I'm not really in favor of this proposal, but I don't see type inferencing as a problem. It's not fundamentally different from cases like a := 1 + 2
b := int32(1) + 2
c := 1 + int64(2) In these cases we need to infer the type of at least one of the values on the right hand side, and we know how to do that. For x := if b { 0 } else { byte(1) } the type of x := 0 + byte(1) For x := if b { 0 } else { 1.5 } the type of x := 0 + 1.5 |
Once again, I haven't decided on whether I like this (or any suggestion above), but for the record, here's a comment that a colleague made, off-list... If you dislike ternaries because of nesting, you could at least require parentheses to help accentuate the nesting.
Alternatively, you could require that the
|
@nigeltao wrote ...
This is ... not bad at all. Maybe @robpike etc can comment on whether they find this acceptable. It definitely seems to resolve the common complaint with ternary operators. |
This is really nice, because by limiting this
is equivalent to
OR
This gives ternary operators usable in multiple contexts (not just assignments) but clearly legible and with none of the drawbacks that the go designers were concerned about when they left it out. I would love to hear what the go team thinks about this. Thanks for highlighting this idea @nigeltao . I really like it, way more than my original proposal. |
Yet another syntax idea (and not necessarily a good idea) for the pot... If you wanted to discriminate between an if-statement and an if-expression, the latter could require parentheses:
|
What designates a "simple" expression? Nested statements are not the only things that make ternary operators unreadable:
|
That actually looks quite readable (the ternary), definitely on the level of the actual if block |
I personally don't find it very readable, but I guess readability is subjective and the expression isn't too complex. |
Yea, I find it readable too. Once your eyes get used to scanning for the ? and :, it became easy for me to read, or at least just as readable as the long form. |
In JS I add a line break for convoluted ternaries
|
One way to possibly address the syntactic and readability issues would be to use a builtin function. For example,
Because it is a builtin function, it could have the lazy evaluation of the true and false values that we want (only one of the true/false values is evaluated, depending on the condition's value). On the other hand, it would be the only builtin function with lazy evaluation (which is also why we could not implement this function using generics, if we had generics). |
For me that would be a perfect solution. We'd have the brevity and efficiency of the ternary operator but in a more readable form. I think people would be less likely to pass nested I'm not sure whether So, yeah, bring it on and soon :) |
One of the biggest frustrations with Go is the verbosity and how much boilerplate you have to write over and over again. Error checking (which is being addressed in some way) and generics (also eventually being addressed) will go a long way to helping this. Arrow functions and ternary operator would be equally as helpful. I understand the drawbacks of ternary operators. I have worked in PHP, Ruby, Python and Javascript a lot and have seen some atrocious ternary statements. But at some point we need to let people decide how they want to write their code. Individual projects should be able to set their own standards for how to write code. Go already has features which, in my opinion, when used make the code more difficult to read. Named return parameters and naked return statements being the biggest culprits. Defer statements a close second. If Go allowed configuration of configuration of golint and gofmt, it could allow individual projects to decide if they wanted to allow ternary operators (or named return params, naked returns, etc). Personally I prefer the Python ternary
over the more common ternary
|
The concern with the function call is that it affords nesting, which is a major concern that the go team identified with the ternary operator. |
Having a function |
Although the risk would still be there, I think it would be much lower in the case of the
It would be surprising if it were a normal function but it will be a built-in and these often have features (for example function overloading) which are not found in the normal language. There would, of course, be three arguments passed to the function including the bool condition so at least two would be evaluated which is the same as the ternary operator itself. |
On further thought, I like the |
A function like cond would be nice, but hygienic macros to implement such
functions ourselves would be even better. See my issue #32620.
|
Thanks for the suggestion. This proposal has some minor issues mentioned above. Paraphrasing @nigeltao (#31659 (comment)), the new functionality is not particularly orthogonal. It is a new kind of declaration, but although it provides general functionality doesn't extend past declarations. Paraphrasing @randall77 (#31659 (comment)), the statement is presumably intended to short-circuit in that the unselected branch is not evaluated, but the syntax does not indicate that in any clear way. The syntax is also unusual: currently Go only has comma separated expressions in function calls and composite literals, in which each element of the list becomes part of some value; here the comma separated list has a different purpose. Setting those minor issues aside, the benefit of this proposal is not high. You can already write this kind of code in just a few lines. Even initializing a top level variable can be done using an The lack of a conditional ternary operator in Go comes up from time to time but it is not a common complaint. And the people who do make the complaint most likely want the general operator, not a specific initialization syntax. Writing for @golang/proposal-review . |
Introduce a
?=
operator, to support the limited use-case of a conditional assignmentFor example:
By using commas in the RHS, it limits this syntax to only be usable on explicit assignment to a declared variable and consequently limits the LHS to only have one declared variable.
Also, because
go
does not permit variable assignment to declare variables on the RHS, this CANNOT be abused.This is not a ternary operator proposal, or contain the drawbacks of it.
I appreciate the resistance to do a ternary operator (see https://golang.org/doc/faq#Does_Go_have_a_ternary_form ), and I have also looked at some proposals and conversations to add it back in some form: (see https://golang.org/issue/20774 )
This is NOT such a proposal.
We all can clearly see the benefit of the ternary operator as seen in (scenario 1) where it translates:
to a 1-liner
The overriding drawback is that it permits the creation of incredibly complex nested expressions as seen in (scenario 2)
VERSUS
Clearly, it's incredibly hard to understand the one-liner in scenario 2. Its longer version, while many lines, is clearly more readable. I have looked at the one-liner many times, and I don't even know if it is even correct, or if the syntax is correct. Clearly, the one-liner is unreadable, and it was great that it was disallowed in the language.
The main drawback occurs only when there is more than 1 boolean expression checked within the RHS. The way to limit it is to make the syntax be tied to the
assignment
operator, so it can only occur during explicit variable assignment.We know the common use of ternary operators by pragmatic programmers is scenario 1. What if we can allow that use-case, and just that alone? It could dramatically reduce the number of lines of code. This is what this proposal should permit.
Thoughts? Please let us discuss ideas about how this could give us the middle ground we all crave i.e. pragmatically limit the use of ternary operator to the one-line, one expression cases that we all agree improves readability.
^^ cc'ing @agl @rsc @bradfitz @dominikh @robpike @ianlancetaylor
Alternative syntax from comments below that I really like:
@nigeltao #31659 (comment)
The text was updated successfully, but these errors were encountered: