go icon indicating copy to clipboard operation
go copied to clipboard

proposal: spec: always branch for conditionals

Open jkassis opened this issue 1 year ago • 16 comments

Go Programming Experience

Experienced

Other Languages Experience

So many

Related Idea

  • [ ] Has this idea, or one like it, been proposed before?
  • [ ] Does this affect error handling?
  • [ ] Is this about generics?
  • [X] Is this change backward compatible? Breaking the Go 1 compatibility guarantee is a large cost and requires a large benefit

Has this idea, or one like it, been proposed before?

I don't think so.

Does this affect error handling?

No.

Is this about generics?

No.

Proposal

create an "always" branch for conditionals. the scope of always includes the scope of if-else so that variables declared and assigned in the scope of if-else appear in the scope of "always". in some cases, this removes the need to nest if-else logic inside of else blocks.

this...

if a, err := f(b); err != nil {
  ...
} else {
  if b, ok := bar(a); ok {
    ...
  } 
  a.foo()
}

becomes...

if a, err := f(b); err != nil {
  ...
} else if b, ok := bar(a); ok {
  ...
} always {
  a.foo()
}

Language Spec Changes

addition of an "always" block to if-else sequences.

Informal Change

always blocks allow you to perform operations on variables declared and assigned in if-else statements.

Is this change backward compatible?

yes.

Orthogonality: How does this change interact or overlap with existing features?

no.

Would this change make Go easier or harder to learn, and why?

easier. cleaner syntax. less nesting is always good.

Cost Description

trival.

Changes to Go ToolChain

No response

Performance Costs

No response

Prototype

No response

jkassis avatar Aug 20 '24 11:08 jkassis

How does "always" differ from "else true"?

randall77 avatar Aug 20 '24 12:08 randall77

I don't understand the example. It boils down to

    if c1 {
        ...
    } else {
        if c2 {
            ...
        }
        f()
    }

This converts to

    if c1 {
        ...
    } else if c2 {
        ...
    } always {
        f()
    }

For the converted code to be the same as the original code, it must be the case that the always block is only executed if c1 is false, and is executed whether c2 is true or false. I don't understand what always means.

ianlancetaylor avatar Aug 20 '24 17:08 ianlancetaylor

I believe that the intent is to add a new keyword always. That will not be backwards compatible. Preexisting variables/functions can be named always and those would no longer be permitted. If the intent is not a new keyword, can you explain how always would work.

timothy-king avatar Aug 20 '24 17:08 timothy-king

I'm also finding the OP proposal confusing. It breaks the identation logic as can be seen in the example. It is unexpected in that it looks much like a Python finally but seems to apply only to its immediate preceding block...?

thediveo avatar Aug 21 '24 08:08 thediveo

the proposal is for a new keyword, but one that is only valid in the context of an if / else block and not as a "condition" of "if" or "else if"... thus it doesn't conflict with variables named "always".

@ianlancetaylor the example does not reduce to that simplified conditional because conditions in go can be used to declare and assign variables within the scope of the if/else block.

if a := fn1() ; a {
  fn2(a)
}

creates variable "a" that is valid for the scope of the conditional block.

in this...

if a := fn1() ; a {
  fn2(a)
}
fn3(a)

fn3(a) is invalid because the scope of a has already disappeared.

the ability to declare variables for only the scope of a sequence of if/else conditionals leads to clean, sequential, maintainable, readable code until the programmer needs to perform some work on or with a.

the proposal is not for a "finally" but for an "always" such that

if a, err := f(b); err != nil {
  ...
} else if b, ok := bar(a); ok {
  ...
} always {
  a.foo()
} else if c, err := a.bar(); err !=nil {
  ...
} else {
  return c
}

would also be valid.

this allows us to "do some work" with "a" (which entered the scope through a conditional assignment) before proceeding with the subsequent branches of the conditional and without introducing a nested conditional scope in an "else" block.

jkassis avatar Aug 22 '24 15:08 jkassis

fwiw, i'm not married to "always". "next" or "do". "and" makes sense, but conflates with the boolean operator. perhaps "with", which is pythonic i suppose.

jkassis avatar Aug 22 '24 15:08 jkassis

@jkassis Thanks, but that doesn't answer my question.

My question is: when does the always clause execute?

Your original post says that this

if a, err := f(b); err != nil {
  ...
} else {
  if b, ok := bar(a); ok {
    ...
  } 
  a.foo()
}

is equivalent to this:

if a, err := f(b); err != nil {
  ...
} else if b, ok := bar(a); ok {
  ...
} always {
  a.foo()
}

The first code sequence calls a.foo() if f(b) returns a nil error. It calls a.foo whatever bar(a) returns.

If the second code sequence is equivalent, then the always clause is not executed if the first condition (err != nil) is true, and the always clause is executed if the second condition (ok) is true.

So when precisely is the always clause executed?

ianlancetaylor avatar Aug 22 '24 16:08 ianlancetaylor

@ianlancetaylor ... oh my goodness. i see your point and i did make a mistake in my example. please forgive. this example is to the point...

if a, err := f(b); err != nil {
  ...
} else {
  a.foo()
  if b, ok := bar(a); ok {
    ...
  } 
}

would be...

if a, err := f(b); err != nil {
  ...
} with {
  a.foo()
} else if b, ok := bar(a); ok {
    ...
}

jkassis avatar Aug 23 '24 13:08 jkassis

noting that i changed the "always" keyword to "with" as per the evolving conversation.

jkassis avatar Aug 23 '24 13:08 jkassis

Can there be multiple "with" keywords? Or does it depend on where it is located?

zwell avatar Aug 26 '24 07:08 zwell

definitely, though a with after a with doesn't make much sense.

jkassis avatar Aug 26 '24 19:08 jkassis

here's a practical example...

	// start the client pool
	var clientPool *ClientPool
	if poolID, err := GetPodIdx(s.Name); err != nil {
		s.ErrorCheck(http.StatusInternalServerError, fmt.Errorf("could not get poolID from podName: %v", err))
		return
	} else if clientPool = ClientPoolMake(s.Ctx, netConf, 1024, poolID); false {
	} else if err := clientPool.Start(); err != nil {
		s.ErrorCheck(http.StatusInternalServerError, fmt.Errorf("could not start client pool: %v", err))
		return
	}

becomes...

	// start the client pool
	var clientPool *ClientPool
	if poolID, err := GetPodIdx(s.Name); err != nil {
		s.ErrorCheck(http.StatusInternalServerError, fmt.Errorf("could not get poolID from podName: %v", err))
		return
	} with {
	        clientPool = ClientPoolMake(s.Ctx, netConf, 1024, poolID)
	} else if err := clientPool.Start(); err != nil {
		s.ErrorCheck(http.StatusInternalServerError, fmt.Errorf("could not start client pool: %v", err))
		return
	} 

jfkfrb avatar Aug 26 '24 19:08 jfkfrb

obviously, you can also put more than one statement in the with clause, which is an extra advantage.

jkassis avatar Aug 27 '24 03:08 jkassis

You can put more than one statement in an else if clause too, sort of...

	if a, b := 1, 3; a == b {
		panic("error")
	} else if func() bool {
		a++
		b--
		return false
	}() {
	} else if a == b {
		print("ok")
	}

magical avatar Aug 27 '24 04:08 magical

that's true, but less readable imo.

jkassis avatar Aug 27 '24 05:08 jkassis

This is syntactic sugar for a nested else clause. The emoji voting is not in favor. Therefore, this is a likely decline. Leaving open for four weeks for final comments.

ianlancetaylor avatar Sep 04 '24 21:09 ianlancetaylor

How much emoji is required?

with{...} vs else if func () bool {... ; return false }(){} gives me butterfly-mojis!

As a former Google Borg-man, I'm sure others would feel the same.

jkassis avatar Sep 07 '24 19:09 jkassis

@rebeccajae can you give me some :heart:

jkassis avatar Sep 07 '24 19:09 jkassis

@adamrogoyski can you give me some :heart: too?

jkassis avatar Sep 07 '24 19:09 jkassis

How much emoji is required?

There is no specific requirement, but this proposal is all thumbs-down and no thumbs-up.

By way of comparison, in Go 1.21 we had a fairly minor change to the language: adding a clear builtin function. That proposal, #56351, had 122 thumbs up and 4 thumbs down.

ianlancetaylor avatar Sep 08 '24 03:09 ianlancetaylor

wow. so many thumbs down. i suspect because the original example was in error. i updated the proposal title and content, but suspect i won't get the votes. can you please point me to the relevant package. i'll provide the PR.

jkassis avatar Sep 08 '24 06:09 jkassis

was looking through the "clear" proposal... how would i locate the PR for code associated with that?

jkassis avatar Sep 08 '24 15:09 jkassis

@jkassis This is getting off-topic, but you can search for the gopherbot comments saying "Change https://go.dev/cl/... mentions this issue". In this case the main change is: https://go-review.googlesource.com/c/go/+/453395

gophun avatar Sep 08 '24 15:09 gophun

This proposal is syntactic sugar, so several places need changing. The compiler parser in cmd/compile/internal/syntax. The internal IR may need changing in cmd/compile/internal/ir, I'm not sure. Something will need to translate the new syntax into existing constructs somewhere. Then there is separate code that needs changing in go/parser, go/ast, go/print, and probably go/types.

Note that writing a pull request for this proposal is not going to make us any more likely to accept it. We are currently path to declining this proposal.

ianlancetaylor avatar Sep 09 '24 01:09 ianlancetaylor

No change in consensus.

ianlancetaylor avatar Oct 02 '24 21:10 ianlancetaylor