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: spec: conditional-assign (?=) for limited ternary operator functionality #31659

Closed
ugorji opened this issue Apr 24, 2019 · 50 comments
Labels
FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Milestone

Comments

@ugorji
Copy link
Contributor

ugorji commented Apr 24, 2019

Introduce a ?= operator, to support the limited use-case of a conditional assignment

var s ?= boolean_expr, ifTrueVal, ifFalseVal

For example:

var v, s string
// ...
 s ?= len(v) == 0, "blank", "not blank"

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:

var n T
if expr {
    n = trueVal
} else {
    n = falseVal
}

to a 1-liner

var n = expr ? trueVal : falseVal

The overriding drawback is that it permits the creation of incredibly complex nested expressions as seen in (scenario 2)

var b1, b2, b3 bool
var s = b1 || b2 ? b2 && b3 ? b1 && b3 : "s1" : "s2" : "s3": "s4" // impossible to read

VERSUS

var s string
if b1 || b2 {
    if b2 && b3 {
        if b1 && b3 {
            s = "s4"
        } else {
            s = "s3"
       }
    } else {
        s = "s2"
    }
} else {
    s = "s1"
}

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)

// boolean expressions and assignment values are simple statements which do not nest
var s string = b1 ? "s1" : b2 ? "s2" : b3 ? "s3" : "s4000"
// equivalent to
if b1 { s = "s1" } else if b2 { s = "s2" } else if b3 { s = "s3" } else { s = "s4000" }
@bradfitz bradfitz changed the title [proposal] Go 2: spec: conditional-assign (?=) for limited ternary operator functionality proposal: Go2: spec: conditional-assign (?=) for limited ternary operator functionality Apr 24, 2019
@bradfitz bradfitz added the v2 A language change or incompatible library change label Apr 24, 2019
@gopherbot gopherbot added this to the Proposal milestone Apr 24, 2019
@ianlancetaylor ianlancetaylor changed the title proposal: Go2: spec: conditional-assign (?=) for limited ternary operator functionality proposal: Go 2: spec: conditional-assign (?=) for limited ternary operator functionality Apr 24, 2019
@urandom
Copy link

urandom commented Apr 25, 2019

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:

a := if condition {
     trueVal
} else {
     falseVal
}

And of course, if you have a lot of conditions:

a := switch condition {
case A: aVal
case B: bVal
....
case Z: zVal
....
}

@alanfo
Copy link

alanfo commented Apr 25, 2019

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 ?:= operator as well so that the variable could be defined on the same line without the need for var. But I think on balance it's best to stick with a single ?= operator and to require the use of var in this scenario as type inference can, of course, still be used.

TBH I'm not a fan of if and switch doubling up as expressions having used them in this guise in Kotlin (which uses when instead of switch). They tend to produce rather untidy code as well as blurring the distinction between statements and expressions which I personally dislike. In fact, this is a very heated topic in the Kotlin community half of which would like to see the ternary operator (or some form of it) introduced whereas the other half prefer the status quo.

@ntrrg
Copy link

ntrrg commented Apr 25, 2019

What about the Python way?

x := "yes" if true else "no"

Some people don't like the ? : sintax

@ugorji
Copy link
Contributor Author

ugorji commented Apr 25, 2019

What about the Python way?

x := "yes" if true else "no"

Some people don't like the ? : sintax

This is not about the ? : syntax at all. This is NOT a proposal to introduce a ternary operator. Please re-read the whole proposal. This is a proposal to allow a conditional assignment to a declared variable, which will accommodate most of the pragmatic use-cases of ternary operator, while bypassing the drawbacks.

If the syntax is available as part of an expression, then it can be misused.

@ugorji
Copy link
Contributor Author

ugorji commented Apr 25, 2019

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:

a := if condition {
     trueVal
} else {
     falseVal
}

And of course, if you have a lot of conditions:

a := switch condition {
case A: aVal
case B: bVal
....
case Z: zVal
....
}

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 = or :=, which MUST have exactly one declared variable on the LHS.

@josharian
Copy link
Contributor

MUST have exactly one declared variable on the LHS.

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

a, b ?= b, a if swap else a, b

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 ?= modifies the LHS (+=, <<=, etc.). That makes this a bit of an anomaly.

@ntrrg
Copy link

ntrrg commented Apr 25, 2019

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 var s ?= boolean_expr, ifTrueVal, ifFalseVal is like writing a boolean_expr ? ifTrueVal : ifFalseVal

@ugorji
Copy link
Contributor Author

ugorji commented Apr 25, 2019

MUST have exactly one declared variable on the LHS.

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

a, b ?= b, a if swap else a, b

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 ?= modifies the LHS (+=, <<=, etc.). That makes this a bit of an anomaly.

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 ?= just like I see :=. Within go, the spec makes clear that op= works when op is an arithmetic operator. ?= and := both work in this context, because neither ? nor : is an arithmetic operator, so there's no ambiguity/confusion. It becomes a singular multi-byte operator, just like || or << or >> or :=. And there's no ambiguity, and there's precedence in this specific limited case of "richer assignment" like := does.

Remember: there can only be one variable on the LHS. The syntax is really only applicable for:

(var)? varName ?= boolean_expr, trueValAssignableToType, falseValAssignableToType

Given that go already prevents if/switch expressions, the example you suggested is not possible in go in any syntax, and so doesn't apply.

@ugorji
Copy link
Contributor Author

ugorji commented Apr 25, 2019

ifTrueVal if boolean_expr else ifFalseVal

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 var s ?= boolean_expr, ifTrueVal, ifFalseVal is like writing a boolean_expr ? ifTrueVal : ifFalseVal

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.

@nigeltao
Copy link
Contributor

I'm still undecided whether or not I like the proposal, but as for @urandom's proposed syntax:

a := if condition {
     trueVal
} else {
     falseVal
}

This doesn't necessitate having if/switch expressions. Instead, this could be a peculiar form of assignment: an assign-if or assign-switch. Essentially, = if or := if is the atomic syntactic concept, and the curlies and else afterwards are basically ceremony that makes the whole thing look familiar, similar to (but different than) the existing if statement.

For example, this would allow a = if etc (and we could possibly also allow a += if etc) but this would still prohibit a = b + if etc. It would definitely prohibit the abuse of nested ternaries.

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 else { falseVal } could be optional, implying that falseVal is the zero value of typeof(trueVal). OTOH that could be confusing if people read a = if condition { trueVal } to mean a no-op if condition is false.


Avoiding the commas of the original proposal also lets us allow multiple assignment:

a, b = if condition {
     ta, tb
} else {
     fa, fb
}

Saying if instead of ? as per the original proposal also lets us allow assign-switch, as @urandom already noted.


Separately, gofmt could possibly allow (but not mandate) the 1-liner form:

a := if condition { trueVal } else { falseVal }

This would be similar to allowing (but not mandating) 1-liner functions. Both of these are acceptible to gofmt:

func Foo() bool { return foo }

func Bar() bool {
    return bar
}

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.

@beoran
Copy link

beoran commented Apr 26, 2019

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.

@alanfo
Copy link

alanfo commented Apr 26, 2019

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.

@ugorji
Copy link
Contributor Author

ugorji commented Apr 26, 2019

@nigeltao wrote:

a := if condition {
     trueVal
} else {
     falseVal
}

This doesn't necessitate having if/switch expressions. Instead, this could be a peculiar form of assignment: an assign-if or assign-switch. Essentially, = if or := if is the atomic syntactic concept, and the curlies and else afterwards are basically ceremony that makes the whole thing look familiar, similar to (but different than) the existing if statement.

a := if condition { trueVal } else { falseVal }

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 = if like: does go now support if expressions, why can't I use it in if expressions fully, why can't i use the construct outside of assignment, etc. E.g. why not?

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

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.

Finally, I also love that go is about orthogonality. I agree that the = if would not be orthogonal, as we will be overloading the if keyword and statement and restricting where it's full power can be used. We typically mostly do this for built-in functions e.g. make, new, etc.

However, as an operator, it is still kinda orthogonal, as it is just fulfilling a missing feature available in other languages, like :=, alias, etc and acting as syntactic sugar. i.e.

var valueIsOk boolean
var s ?= valueIsOk ? "s1" : "s2"
// is just syntactic sugar for
var s string
if valueIsOk {
  s = "s1"
} else {
  s = "s2"
}

@josharian
Copy link
Contributor

@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"] }

if containing a statement:

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.

@randall77
Copy link
Contributor

This proposal doesn't mention anything about short-circuiting (ala || and &&).
in

   x ?= a, b, c

Are b and c evaluated unconditionally, or are they only evaluated based on the result of evaluating a? In the C and Java ternary operators, at least, it is the latter.

This has a large impact on the syntax; the syntax should be consistent with the answer. When I read x ?= a, b, c, it looks like a, b, and c are all unconditionally evaluated (as they are in a multiassignment). On the other side,

   x := if a {b} else {c}

Implies that only one of b or c is evaluated.

@josharian
Copy link
Contributor

josharian commented Apr 26, 2019

Another interesting case: mixed type inference.

x := if b { 0 } else { byte(1) }

Presumably, x has type byte, even though the default type for the constant 0 is int. I don't think there are any cases in which we currently do dual/paired type inference. (And if we allow switches, n-ary type inference.)

EDIT: Even more interesting is

x := if b { 0 } else { 1.5 }

@alanfo
Copy link

alanfo commented Apr 26, 2019

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:

x ?= a, b, c

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 x is inferred to be an int and so can't be assigned a byte or float64 value.

Of course, if x had already been declared to be a byte or float64 variable, then there would be no problem as assigning 0 to it would then be allowed.

@randall77
Copy link
Contributor

 x ?= a, b, c

as it's not a multi-assignment - there's only one variable on the LHS.

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.
If we're short circuiting, I would want some visual cue that a is always evaluated and b and c are not. Say (not that I am proposing this):

 x ?= a, [b, c]

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 x is inferred to be an int and so can't be assigned a byte or float64 value.

So in x ?:= a, b, c, we infer the type of x from the type of b, and then check that c is assignable to that type? That would work. It's a little bit unsatisfying that it isn't symmetrical in b and c. It might cause people to invert the condition unnecessarily to get the type they want inferred into the b slot. But that's a relatively small objection.

@josharian
Copy link
Contributor

I think that type inference should take place from left to right and so neither example should compile.

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.

@alanfo
Copy link

alanfo commented Apr 27, 2019

@randall77,

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:

x ?= a, b : c  

@alanfo
Copy link

alanfo commented Apr 27, 2019

@josharian,

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.

@josharian
Copy link
Contributor

there would be more work for the compiler

Typechecking is cheap.

@alanfo
Copy link

alanfo commented Apr 28, 2019

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 x is inferred to be a float64 variable.

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.

@stevef1uk
Copy link

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{
int(params.Body.Tsimple.ID),
params.Body.Tsimple.Dummy,
parseTime(params.Body.Tsimple.Mediate),
Simple3{
int(params.Body.Tsimple.Embedded.ID),
float32(params.Body.Tsimple.Embedded.Floter),
},
}

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{
int(params.Body.Tsimple != nil ? params.Body.Tsimple.ID : 0),
...

@ugorji ugorji closed this as completed Apr 29, 2019
@ugorji ugorji reopened this Apr 29, 2019
@bcmills
Copy link
Contributor

bcmills commented Apr 29, 2019

But nesting on a single command line is bad, especially wrt ternary operator i.e. e.g. this is bad (and this is what the ternary operator allows concisely (making it even worse)

That seems like more of an issue for gofmt than for the language spec itself: gofmt is free to add separating newlines as appropriate.

@networkimprov
Copy link

Data point; I use: v := a; if t { v = b }

Sadly that always evaluates and copies a. I would love to see an efficient way to do this.

(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 && and || may not evaluate the second operand.

@josharian
Copy link
Contributor

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 init function. In addition to the reduced verbosity, it would be easier for the compiler to convert this to static data in cases in which b can be evaluated to a constant. (We currently make no effort to understand the contents of an init function, and I don't see that changing.)

@ianlancetaylor
Copy link
Contributor

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 is byte, just as it is for

x := 0 + byte(1)

For

x := if b { 0 } else { 1.5 }

the type of x is float64, just as it is for

x := 0 + 1.5

@nigeltao
Copy link
Contributor

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.

// This is a syntax error.
var a = b0 ? b1 ? x : y : z

// This is fine.
var a = (b0 ? (b1 ? x : y) : z)

Alternatively, you could require that the ifTrueVal to be a 'simple' expression, for some definition of 'simple'. Even if you have nesting, it'd always read left-to-right, following the false arms:

// This is a syntax error.
var a = b0 ? b1 ? x : y : z

// This is fine.
var a = !b0 ? z : b1 ? x : y

@ugorji
Copy link
Contributor Author

ugorji commented Apr 30, 2019

@nigeltao wrote ...

Alternatively, you could require that the ifTrueVal to be a 'simple' expression, for some definition of 'simple'. Even if you have nesting, it'd always read left-to-right, following the false arms:

// This is a syntax error.
var a = b0 ? b1 ? x : y : z

// This is fine.
var a = !b0 ? z : b1 ? x : y

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.

@ugorji
Copy link
Contributor Author

ugorji commented Apr 30, 2019

@nigeltao wrote ...

Alternatively, you could require that the ifTrueVal to be a 'simple' expression, for some definition of 'simple'. Even if you have nesting, it'd always read left-to-right, following the false arms:

// This is a syntax error.
var a = b0 ? b1 ? x : y : z

// This is fine.
var a = !b0 ? z : b1 ? x : y

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 ifTrueVal to a simple expression i.e. one without a nested conditional, then this reads just as simply as a if/(else-if/)*else block (with no nesting). Really nice!
i.e.

var a = b0 ? w : b1 ? x : b2 ? y : z

is equivalent to

var a
if b0 {
  a = w
} else if b1 {
  a = x
} else if b2 {
  a = y
} else {
  a = z
}

OR

var a
switch {
case b0: a = w
case b1: a = x
case b2: a = y
default: a = z
}

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.

@nigeltao
Copy link
Contributor

nigeltao commented May 1, 2019

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:

// This is a statement.
if b {
  x()
} else {
  y
}

// This is an expression.
(if b {
  x()
} else {
  y
})

@deanveloper
Copy link

Alternatively, you could require that the ifTrueVal to be a 'simple' expression, for some definition of 'simple'. Even if you have nesting, it'd always read left-to-right, following the false arms:

What designates a "simple" expression? Nested statements are not the only things that make ternary operators unreadable:

z := x && y || b1 | b2 > 0 ? b1 | b2 : b3

// vs

var z int
if x && y || b1 | b2 > 0 {
    z = b1 | b2
} else {
    z = b3
}

@urandom
Copy link

urandom commented May 7, 2019

That actually looks quite readable (the ternary), definitely on the level of the actual if block

@deanveloper
Copy link

I personally don't find it very readable, but I guess readability is subjective and the expression isn't too complex.

@ugorji
Copy link
Contributor Author

ugorji commented May 7, 2019

That actually looks quite readable (the ternary), definitely on the level of the actual if block

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.

@networkimprov
Copy link

In JS I add a line break for convoluted ternaries

z := x && y || b1 | b2 > 0 ? b1 | b2
                           : b3

z := x && y || b1 | b2 > 0 ?
     b1 | b2 : b3

@ianlancetaylor
Copy link
Contributor

One way to possibly address the syntactic and readability issues would be to use a builtin function. For example,

s = cond(len(v) == 0, "blank", "not blank")

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

@alanfo
Copy link

alanfo commented Jun 5, 2019

One way to possibly address the syntactic and readability issues would be to use a builtin function.

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 cond expressions to a function than to the ternary operator itself but, even if they did, it should still be readable because the nested cond's would stand out in an editor or IDE using syntax highlighting.

I'm not sure whether cond would be the best name for it as it's quite commonly used as a bool variable or parameter in my experience. Other possibilities would be iff (or iif) which is common in BASIC-like languages or perhaps tern as an easy reminder of what it does.

So, yeah, bring it on and soon :)

@MrTravisB
Copy link

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

x = 1 if True else 2

over the more common ternary

x = true ? 1 : 2

@ugorji
Copy link
Contributor Author

ugorji commented Jun 6, 2019

One way to possibly address the syntactic and readability issues would be to use a builtin function. For example,

s = cond(len(v) == 0, "blank", "not blank")

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

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.

@networkimprov
Copy link

networkimprov commented Jun 6, 2019

Having a function cond() which doesn't always evaluate both all arguments would be surprising.

@alanfo
Copy link

alanfo commented Jun 6, 2019

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.

Although the risk would still be there, I think it would be much lower in the case of the cond function because you don't often see very complicated expressions passed as arguments to functions - they tend to be assigned to intermediate variables first to make the code more readable.

Having a function cond() which doesn't always evaluate both arguments would be surprising.

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.

@ugorji
Copy link
Contributor Author

ugorji commented Jun 14, 2019

On further thought, I like the cond(...) function with the caveat that it cannot embed other cond.

@beoran
Copy link

beoran commented Jun 14, 2019 via email

@ianlancetaylor
Copy link
Contributor

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 init function. While the cases where this syntax would help clearly do exist, it's not obvious that they are common enough to justify new syntax.

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 .

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests