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 : add support for conditional statements #25582

Open
mortdeus opened this issue May 26, 2018 · 9 comments

Comments

@mortdeus
Copy link

commented May 26, 2018

Preface:
(This proposal started as an alternative solution to Ian Taylor's proposal "simplify error handling with || err suffix")

Proposal:

My proposal is to implement the conditional operator ('?') as an entirely new Go feature.

Rationale:

Before I start trying to explain what my proposal is, I want to take a moment to explain what my proposal isn't. This is not a proposal to implement a C style '?' operator in Go.

'expression ? expression : expression' // not this

Rather this is a proposal to try and integrate the ? operator into the language in such a way that "makes sense" in terms of Go's design.

Now, to explain what the Go ? operator does. The operator adds an element of conditionality to a built in statement. In other words, statements in code are read more like "questions". Should we return this? Should this Go routine run? etc.

In order to demonstrate what we are ultimately trying to solve here I first need to refer everyone to the warty issues we have when it comes to the way we currently deal with error handling, (particularly the issue of having to read and write the following code over and over again.)

x, err := foo() if err != nil { return err }

This sequence of code is so unnecessarily prevalent in Go code; that Ian Taylor's proposal's discussion thread has grown to a staggering 400 comments (and still appears to be rising without any signs of slowing down.),

And out of all those comments, all but a very minute few are complaints regarding the never ending repetition of boilerplate error handling. And from those complaints many counter proposals have materialized. Proposals such as making "error" even more magical using a '||' operator to write a magic one liner.

For example, the os.Chdir function is currently

func Chdir(dir string) error {
if e := syscall.Chdir(dir); e != nil {
return &PathError{"chdir", dir, e}
}
return nil
}
Under this proposal, it could be written as

func Chdir(dir string) error {
syscall.Chdir(dir) || &PathError{"chdir", dir, err} //<<<*
return nil
}
/* *note that the 'err' term being pointed out in the code above just magically exists, and consider the confusion that causes and how defining our return argument as func Chdir(dir string) (err error) doesn't make life any better. */

Other proposals are suggestions that have been proposed before and tend to have an element of feature overlap to them like the proposal to add try-throw-catch exception handling when we already have panic and recover.

Having read what most people had to say, I decided to chime in and give my two cents on Ian Taylor's proposal. Which is copy and pasted below.

Yeah, I propose that we just leave things the way they are. I was under the impression that Go's error system was deliberately designed this way to discourage developers from punting errors up the call stack. That errors are meant to be resolved where they occur, and to know when its more appropriate to use panic when you cant.

I mean isn't try-catch-throw essentially the same behavior of panic() and recover() anyways?

Sigh, if we are really going to start trying to go down this road. Why can't we just do something like

_, ? := foo()
x?, err? := bar()

or perhaps even something like

_, err := foo(); return err?
x, err := bar(); return x? || err?
x, y, err := baz(); return (x? && y?) || err?

where the ? becomes a shorthand alias for if var != nil{ return var }.

We can even define another special builtin interface that is satisfied by the method

func ?() bool //looks funky but avoids breakage.

that we can use to essentially override the default behavior of the new and improved conditional operator.

And now that I have caught everyone up on where we are right now and how we ultimately got here. We now get to move on to the more interesting bits of my proposal.

Design:

This proposal suggests to add to the language

  • a new operator "?"
  • a new builtin interface type
  • a very special case overloaded switch statement OR a special method set that satisfies the above mentioned new builtin interface type.

Now, I realize this is very scary sounding and many of you are more than likely motioning your finger towards the backspace key right now. But I implore you all to hear me out.

First lets look at the syntax of the operator.

_, ? := foo()

This was my initial idea, that you can just assign to "?" as a way to say that if err != nil, you want to return the error to caller.

x?, err? := bar()

And this was taking the idea a step further. Here either x and/or err being != nil (should 0, "" == nil?) causes the function to assign new values to x, and err (in case were dealing with references) and then immediately return.

x, err? := bar()

And I just wanted to point out in this scenario, we only return when err != nil, when err == nil, x gets assigned to the new value and the program proceeds as normal.

Also I want to take a quick moment to point out that what I just described is not actually apart of the design im proposing. Rather its an entirely separate "mini" proposal, that can be used if we want to just get rid of the annoyingly repetitive "if err != nil {return err} code that is all over the place. And it also cleans up the syntax when you run into this wart

var err error if x, err = foo(); err != nil { return x, err }

with this proposal all that code gets condensed down to

x?, ? = foo();

Now I should point out that another key aspect of the ? operator in assignments is that it designates which variables are going to be sent back to the caller as return arguments.

For example x, ? = foo(); only sends back err if err != nil. x?, _ = foo(); only sends back x if x != nil (or 0, ""?), and last but not least x?, err? = foo() sends both back, in the event that either x or err != nil. It doesn't need to be both in that case.

Now, you might be asking, well what about when I have more or less return arguments than the function im returning from defines?

We can get around that a couple ways.

Assign identifiers to the return arguments and rely on a rule of a rightmost alignment when assigning to '?'.

For example

func foo() (msg Msg, len int, error){ defer func() int {? = msg.Len()} msg?, ? = fetchMsg() }

In this case every thing will line up properly because we are chaining the return args. However if you tried ?, ? = fetchMsg(), it should throw an invalid type error.

Or we can just use

Using ?, ??, ??? and ?n to manually indicate argument index from left to right. (or should it be right to left? and variadic assigns to same number?

func foo() (msg Msg, len int, error){ defer func() int {?? = msg.Len()} ?, ??? = fetchMsg()

And in the event that we have a ton of return args.

func foo() (msg Msg, len int, ts timestamp, error, error){ defer func() int {??,?5 = msg.Len()} ?, ???, ?4 = fetchMsg()

Anyways, as you can see, this "mini proposal" looses it's sex appeal completely when trying to use ? in many assignments and within defer.

Which is ultimately what led me to try to think of a completely different approach when thinking about this problem.

x, y, err := baz(); return (x? && y?) || err?

Here I show that instead of using ? in the assignment of variables, you use the ? to change a statement into what I essentially like to think of as the program asking permission.

_, err := foo(); return err?

The default behavior for the ? operator in this implementation is the same as before. We check to see if we aren't nil and if we aren't then we return. Otherwise we move on like normal.

x, err := bar(); return x? || err?
x, y, err := baz(); return (x? && y?) || err?

And you can use logical and/or (and the bitwise ops) to define the conditions that need to be satisfied for the return to execute.

x, y, err := baz(); return (x? && y?) || err?

Now when using a return statement like this, as before the ? designates which variables get sent back as return arguments and the alignment is left to right. It is assumed that everything after ? is regarded as a ',' until another ? is encountered and everything in between (including identifiers without a ?) is not relevant to an assignment.

I realize that this is a rather drastic change to the syntax of statements. Which is why I propose that we add another builtin interface.

The new interface which for now I just refer to as interface{?}, can be defined in two different ways.

The first way it can be defined is

type ? interface{ Go?() bool Return?() bool Select?() bool Switch?() bool For?() bool If?() bool Defer?() bool }

and then you can go about

type Foo int func (f Foo) Go?() bool { f.Foo() ok := Bar(f).Baz() return ok }

However, I don't think this is the best way, rather I would
create a magic switch case construct.

`

type ? interface{
?() bool
}

type Foo int
func (f Foo) ?() bool{
switch f? {
go?: // go specific code goes here
return?: // return specific code goes here
select?: // // select specific code goes here
switch?:// switch specific code goes here
for?: // for specific code goes here
if?: // if specific code goes here
defer?: // defer specific code goes here
}

foo()
bar(f).Baz()

// do more specific work after doing work that applies to all statements
switch f? {
go?: // go specific code goes here
return?: // return specific code goes here
select?: // // select specific code goes here
switch?:// switch specific code goes here
for?: // for specific code goes here
if?: // if specific code goes here
defer?: // defer specific code goes here
}
return true // or false
}`

And this way we can make super simple and easy to read calls like

`f, err := getTheFoo()

if f? && err? { // ? on builtins always defaults to nil/zero test.
return f, err
}else if err?{
return nil, err
}else{
return f?, nil
}`

which is a condensed form of

`f, err := getTheFoo()

if ifprep(f); f != nil && err != nil{
return f, err
} else if err != nil{
return nil, err
}else{
if f != nil {
returnprep(f)
return f, nil
}
}`

And that is basically what I propose.

This proposal is still very much a WIP and any feedback (positive or critical) would be greatly appreciated. Thanks!

@gopherbot gopherbot added this to the Proposal milestone May 26, 2018

@gopherbot gopherbot added the Proposal label May 26, 2018

@mortdeus mortdeus changed the title proposal: spec : add support for conditional statements proposal: Go 2 : add support for conditional statements May 26, 2018

@agnivade agnivade added the Go2 label May 26, 2018

@Azareal

This comment has been minimized.

Copy link

commented May 28, 2018

This might reduce the amount of boilerplate, for errors at-least, but it also creates a lot of confusion.
If you're doing that, then you might as-well just use a ternary operator, e.g. C's ? operator or similar. Not that I'm saying a ternary operator is a good idea.

@mortdeus

This comment has been minimized.

Copy link
Author

commented May 28, 2018

I admit that this has the potential of making things way too confusing, which might be the ultimate point I am trying to get to. That trying to fix this issue causes more harm than it solves.

However I should also point out that even things considered wonderful in Go like interfaces and structs embedding can be abused if people wander off from standard convention. Also I should also point out that this adds the ability to program generic behavior with regards to how statements interact with interfaces. especially in code that isn't our own. In other words it adds a generic function you can call on all interface{}s.

Considering that we are moving into the realm of dynamically linked packages, we shouldn't assume that we will always have the ability to influence the way our specific objects get handled. I just think that something like this might create a way to better communicate to the other side where reflection is the primary method to try and understand the passed in data at a deeper level at the cost of adding exponentially more complexity to a program.

In my view, this feature might be the right approach to solving issues that make people wish that Go still had generics, operator overloading, boilerplate reduction, and possibly other things that I haven't thought of yet. And as far as I know, no other language uses the ? operator specifically on statements like this. There are languages that has similar functionality (like side effects) but not quite like this.

@mortdeus

This comment has been minimized.

Copy link
Author

commented May 29, 2018

Also consider the fact that goroutines are discouraged to be used in library code.

The reason being primarily the fact that concurrency should be something that should be left to the user to determine what is appropriate. Right now we define GOMAXPROCS as a very general way of stating how many threads we want our program to use. Of course we can use the runtime.GOMAXPROCS function to edit this requirement at runtime, but what if we want to use more system threads in the concurrency we define, but not in the concurrency that is used in a library we import. What if we want an imported pkg to never use more than 2 threads, because we want our functions to have a higher priority?

The only way I can see how to do that is to encourage libraries to use a conditional Go statement. One that can essentially ask is it okay if I run this data concurrently? and if so on how many threads?

Or what about a situation like this, we have a library function that takes as an interface that defines a method called GetChan() chan pkg.SensitiveDataType. And then say that we define the same interface in the client code which uses a specific channel that we need to be very careful when dealing with concurrently.

Now say that we pass our object to the library function which takes a GetChanner. And then on their end they run `go func(ch chan pkg.SensitiveDataType){ modifyChan(ch)}(OurChanner).

Now we don't know if our channel has been corrupted, and in a proprietary dynamically linked pkg, we can't discover why, and the library author really has no way to ask if this is okay to do on their end, and modifying the interface to include an "isItOkayIfIRunThisConcurrently()?" breaks code if it isn't already implemented in a popular interface, and fuglifies interfaces in general.

The better way I see is to have a go func(ch chan pkg.SensitiveDataType){ modifyChan(ch)}(OurChanner?) that will either pass in nil or changes the underlying channel when the ? side effect is executed. On the library end they will either get a panic or error they can recover from or they will be able to proceed as normal modifying a channel the user determines is okay and on their end is able to resolve as they see fit.

I mean these are really bizarre "what if" corner cases, but I don't think it's best to just say "yeah, how about you just don't do that in library code."

@davecheney

This comment has been minimized.

Copy link
Contributor

commented May 29, 2018

Also consider the fact that goroutines are discouraged to be used in library code.

wut?

@mortdeus

This comment has been minimized.

Copy link
Author

commented May 30, 2018

Im not actually sure when or where I heard that it was discouraged to use goroutines in library code. (I think it was in one of the older talks which makes finding the exact reference at this moment difficult)

Looking at Go's code review policy I can see that what I am talking about has specifically been addressed to make sure that functions are designed to be sync and not async.

@bcmills

This comment has been minimized.

Copy link
Member

commented May 30, 2018

I'm having trouble following what it is that you're actually proposing.

Could you split up the text into smaller sections and more clearly separate the background discussion from the specific version you want to propose?

@likebike

This comment has been minimized.

Copy link

commented Jun 16, 2018

Your proposal is way too verbose. Just get to the point with some good code examples.

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

commented Sep 12, 2018

I am also unclear on the actual proposal. @mortdeus can you condense into a specific proposal without the discussion of how the proposal was developed? Thanks.

@mortdeus

This comment has been minimized.

Copy link
Author

commented Sep 26, 2018

I apologize for the confusion and I'll start working on a formal proposal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
8 participants
You can’t perform that action at this time.