7

Returning values and errors

 2 years ago
source link: https://rachelbythebay.com/w/2022/02/20/return/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Returning values and errors

If you agree with the notion that you need to be able to tell the difference between the absence of a value and a value itself, then this has some impacts on the code you write. If you want to keep 'em separated (so they can come out and play), then it gets you looking at your API designs in certain ways.

These are my reactions to some of the ways a value can be returned (or not).

Imagine we're talking about a helper function that is intended to be run inside a program that's handling the access to a web page. It's probably a CGI program, but it could also be buried inside some much bigger thing, too. This is all about C++ styles, but you can probably see slices of other languages in here, too.

string* UserIP() -- Oh great. It's a pointer to a string, so that means it got allocated somewhere in there, and we have to worry about the lifetime of that buffer. Guess it's being created from scratch, and then we have to free (delete) it? But wait, what if the allocation fails? I guess it can return a NULL or nullptr or something. Also, what happens if it can't find the IP address? I guess it could return a NULL/nullptr/whatever for that, too. I sure hope everyone who uses this checks for it...

string UserIP() -- No more lifetime worries here, but oops, uh, well, what if we don't know what it is and need to provide an error? If it's a CGI situation, maybe REMOTE_ADDRESS or whatever wasn't defined for some reason. What then? Do we throw? Yuck! Do we return some magic value? Double yuck! Don't say return the string "unknown", since what if there's a host on the network named that, and you end up using that as a real value somehow? You just know someone would do it. Just ask the guy who has "NULL" as his license plate.

string UserIP(string* errmsg) -- Oh geez, I'm going to be sick. Sure, now they can "set an error message", but now how do you know when to look at errmsg vs. the return value?

bool GetUserIP(string* ip) -- Well, this is a little better, right? It's kind of wonky because now you're wrangling outvars and the user could do something stupid like handing you a bad pointer, plus that means THEY are having to think in terms of addresses and pointers and that's probably not great, either. But hey, at least you can point out that something failed. Too bad you can't say what.

bool GetUserIP(string* ip, string* errmsg) -- Okay, so now we can include a message if we want to provide some color commentary on what happened, but geez, now it's TWO outvars. Imagine what the call sites are going to look like.

Result<string> UserIP() -- Now this is getting interesting. You can do some fun stuff, like having a flag that only gets set once the Result is checked for correctness. Only then is .value() allowed to be used. If you call .value() first, YOU DIE. This should catch you much earlier in development, since even "happy path" cases will blow up if it's not being used correctly. You also have an .error() to return some nice message in the other case. It's a pity about the whole <> thing though. Kind of nasty-looking... and annoying to keep typing over and over and over.

ResultString UserIP() -- Same idea as before with fewer brackets, but oh dear, did someone code this up as a one-off? Now what happens when we want to do this with other types? Do we copy and paste the magic "you must check for correctness before calling value()" gunk into ResultInt, ResultFloat, ResultDouble, and so on? How do we keep them from diverging in the event of a bug fix that only touches one? Don't we really need the ability to do generic programming and templates?

string UserIP() -- Wait, didn't we do this already? Not quite. This time, we're in a slightly different language with different assumptions. The native "string" type now includes all of the correctness checking of ResultString (aka Result<string>), so now you have to check ALL OF THEM. Now it looks simple enough here, but you have to check everywhere. EVERYWHERE. Only then can you use .value() from it. How much extra CPU and memory goes into this for the cases where it'll never be "missing" and will always have some usable value? In fact, how often do we have the one case instead of the other, anyway? What percentage of our strings are "yep, always gonna be something" versus "might have failed completely"?

It's complicated, right? To win a little in one place, you have to spend a little more in another place.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK