Last active
May 24, 2019 06:00
-
-
Save kfsone/9a2f81f10a914871cc04d8e3d9bfa219 to your computer and use it in GitHub Desktop.
A proposal for formalizing and inlining error handling in the Go language
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Go's informal error handling syntax introduces a lot of boiler-plate and ambiguity: | |
// boiler-plate: you have to capture and test the value, and you frequently want to forward it, etc | |
// there is also the potential for people to violate convention/norms. | |
func sqrt(value float64) (float64, error, error) { | |
// what does the second error mean? | |
} | |
func v(value float64) (error) { | |
// just, what ? | |
} | |
// So lets look at an actual quick example of a thin client helper function | |
func sqrt(value float64) (float64, error) { | |
if value < 0 { | |
return 0, errors.New("can't sqrt a negative value") | |
} | |
return math.sqrt(value) | |
} | |
// thin-client: | |
func f(v1 float64, v2 float64) (float64, error) { | |
v3, err := sqrt(v1) | |
if err != nil { | |
return 0, errors.New("v1 was invalid") | |
} | |
v4, err := sqrt(v2) | |
if err != nil { | |
v4 = sqrt(v1 + v2) | |
if err != nil { | |
return 0, err | |
} | |
} | |
return v3 + v4 | |
} | |
// In another language, far far away, this might have simply been: | |
float sqrt(float value) { | |
if (value >= 0) | |
return math.sqrt(value); | |
throw invalid_argument("can't sqrt negative") | |
} | |
float f(float v1, float v2) { | |
float v3 = sqrt(v1); | |
try { | |
float v4 = sqrt(v2); | |
} catch (invalid_argument) { | |
v4 = sqrt(v1 + v2); | |
} | |
return v3 + v4; | |
} | |
// We had a LOT less room for error here. But ... ugh, exceptions. | |
// | |
// My proposal has two potential iterations. Syntax totally open for debate, | |
// and I've deliberately chosen evocative elements so that what seems like the | |
// logical default choice is an equal amongst candidates. | |
// The first iteration would simply be to reduce the clutter of the normal | |
// error handling by allowing | |
// if <expression>; error(...) | |
// to be syntactic sugar for | |
// if <expression> { return <args0..n-1>, <error clause> } | |
// | |
// This is specifically to address the question of "what do the other return parameters mean when there is an error?" | |
// | |
// The goal is to explicitly remove those fields from the programmers mind. | |
// | |
// Obviously there are times when it is legitimate for an error to accompany partial data, and the old | |
// syntax still allows for this. | |
v, err := call() | |
if err != nil; error(err) | |
if err == nil; error(errors.New("you can't win")) | |
if err != nil { | |
fmt.Println("This is impossible!") | |
return 0, err | |
} | |
// You saw some extra information in the above that you didn't notice. | |
// | |
// In the first two error cases, the author explicitly indicated intent NOT to return anything but an error. | |
// | |
// Future code analysis tools could tag/inspect this and be able to warn users when they do something like: | |
// | |
// v, err := above_func() | |
// if err != nil { ... non-terminal ... } | |
// fmt.Print(v) // warning: above_func does not populate v in an error condition | |
// The second iteration is "failable funcs". These are annotated, in this proof | |
// by suffixing a question mark. | |
// Failable functions *must* have one and only one error return and it must be | |
// the trailing return. | |
// | |
// In this proof, failable funcs have two potential syntaxes. | |
v1, ... vn, err := failfn?() | |
if err != nil; error(...) | |
// the error value *must* be tested, or else a warning is produced. | |
// | |
// The second syntax introduces an allowed else after a failable function | |
// which can be followed by either a singular error expression or a compound | |
// statement that ends with an error expression: | |
func failable?() (int, err error) { | |
// "else error(err)" here is effectively | |
// if err != nil { | |
// return default, v1_default, ... vN_default, err | |
// } | |
v1, ... vn, err := failfn?() else error(err) | |
v2, ... vn, err := failfn?() else { | |
log.Print("v2 was invalid") | |
error(errors.New("invalid argument")) | |
} | |
return 0, nil | |
} | |
// Addenda: | |
// 1. Precedence: What if "else" had precedence over "="? | |
// This should allow the following: | |
v1, err = failfn?(v1) else { | |
log.Print("v1 is bad:", v1) | |
error(err) | |
} | |
// to be interpreted as the contents of the else clause happening *before* v1 is assigned, and since it results in an | |
// error, v1 is actually never overwritten. Thus: | |
self.Member = self.GetConnection?() else { | |
// I don't have to use an intermediate value! | |
// if GetConnection? fails, self.Member won't be assigned to | |
error(err) | |
} | |
// I also wanted to consider eliding the 'err' where possible to make it less onerous, but I'm torn on making it inconsistent. | |
// part a: auto capture failfn?.argument(-1) -> named error variable | |
func failable?() (int, err error) { // if the error is named, we can auto-capture it so the user doesn't have to specify it | |
v1 := failfn?() else error(err) | |
return v1, nil | |
} | |
// I would also have liked to elide the ', nil' in returns, but this would come back as developer pain | |
// any time the number of arguments changes. For this to be viable, I'd want a consistency change in | |
// failable funcs so that you must *never* specify the error value in return in failable funcs: | |
func failable?() (int, err error) { | |
v1, err := failfn?() | |
if err != nil { | |
log.Print("failfn failed") | |
// return 0, err // compiler error, expecting 1 argument | |
return 0 // returning 2 args would be invalid, 'err' is returned implicity | |
} | |
return v1 | |
} | |
// There's a C++-grade sharp edge here: the user might expect that final statement to return err = nil, | |
// but it would just return err. | |
// | |
// I think this can be addressed: In a callable function, return *always* returns err = nil. The only way | |
// to override this is with an error() expression. | |
return error(err) // all leading return values zeroed | |
// return ; error(err) // just, no. | |
return 0, 1, 2, 3; error(err) // familiar ';' syntax per for etc | |
return 0, 1, 2, 3 // } return 0, 1, 2, 3, nil | |
return 0, 1, 2, 3; // } return 0, 1, 2, 3, nil | |
// lastly: I like the explicit appearance of this, but it might not be viable: | |
return 0, 1, 2, 3; error(nil) | |
func NewConnection?(host string, port int, non_blocking bool) (socket Socket, err error) { | |
address := Resolve?(host) else { | |
log.Print("Could not resolve", address) | |
return error(err) | |
} | |
if port < 0 || port > 65535 { | |
return error(errors.New("Port out of range")) | |
} | |
socket := NewTCPSocket() { | |
return error(errors.New("Could not bind port")) | |
} | |
if non_blocking { | |
socket.SetNonBlocking() | |
} | |
// Look ma, no assignment required | |
socket.Connect?(address, port) else { | |
// a non-blocking connection may return an EINPROGRESS error | |
// if the connection wasnt instantaneous | |
if !isEINPROGRESS(err) { | |
error(err) | |
} | |
// Demote from an error | |
log.Print("Connection establishing", host, port) | |
// continue back to outer scope | |
} | |
return socket | |
// or | |
// return socket; error(nil) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment