Writing

Feed Software, technology, sysadmin war stories, and more.

Monday, February 21, 2022

Results without all of the brackets

Yesterday, I put forth a bunch of API possibilities for the problem of needing to return a value but also indicating the lack of a value and being able to comment on why. In it, the notion of a "wrapper" around a string was presented, such that it might look like "Result<string>", and those brackets started looking like a pain. There was also the possibility of having something like "ResultString", but didn't that open the door to having a bunch of code duplication?

That's about where I was with my own projects about a year ago. I wanted the Result behavior, but didn't want to deal with templates and those brackets everywhere, or the duplication of code behind the scenes. Still, after some messing around, I came up with a proof of concept that only worked for strings, and started converting bits of my code to it. As things would get some attention for maintenance purposes, they'd get converted from one of the other ways to this style.

After this had existed for a while, the general happy feeling of having this stuff in place became infectious. I wanted it to cover other things with it, too. I wanted it to handle something a bit more complicated than a mere string, and so decided to try my hand at the template-wrangling metaprogramming cruft that would be necessary to make it go.

What ended up happening is that now there's a "result_template.h" which defines a "ResultT" that can be wrapped around another type, so you get "ResultT<string>" or "ResultT<vector<string>>" or whatever else. But, I wanted to insulate the rest of the code from all of those damn brackets, so any time a type like this has been created, it gets its own little header file.

Now, resultstr.h includes result_template.h (and <string>, naturally) and then just does "typedef ResultT<std::string> ResultStr;". That's it.

ResultU16 wraps a uint16_t. ResultU64 wraps a uint64_t. ResultVecStr wraps std::vector<std::string>. All of them have the same key behavior: you have to initialize them to either a value or an error, and then you have to check them before attempting to use that value.

It looks like this in practice:

ResultStr feedback = post.GetVar("feedback");
 
if (!feedback()) {
  // call the thing that spits out a HTTP 400 error
  return false;
}
 
if (feedback.value().length() > kMaxStrLen) {
  // spit out a HTTP/400 here too
  return false;
}

// use feedback.value() in prepared statement for INSERT

Without that call to feedback(), the call to value() later on would abort the program. This means it's pretty difficult to ship code that does the wrong thing, since it will never actually work. You'd have to ship code that merely compiled and didn't run. I mean, sure, people do that kind of crap, but there's only so much I'm aiming to fix with this.

There are some other bits like that in here, too: calling error() is only permitted after doing that check... *and* having it indicate a problem. If there's no problem and you call .error(), it'll abort the program. Likewise, if there IS an error, and you call .value(), it'll also abort the program.

So, to reiterate:

Someone initializes it to a value or an error and returns it to you.

As the caller, you have to check for success or you can't call anything else on it. If you're not going to check, all you can do is throw it away.

If you get back a successful result, then you can call .value(). But, if you call .error(), it will kill your program.

If you get back an error result, then you can call .error(). If you call .value() on that same object, it will kill your program.

And yes, it's possible for an evil person to write something that throws away the value of the check and just goes on to use .value() directly, like "if they don't want to deal with it", or "it makes the code messy". I imagine this would become common with some of the people who starred in one of my previous stories if they encountered this kind of Result scheme in their career.

Personally, I think I'd treat "deliberately throwing away the safety" along the same lines as how I'd treat an employee who deliberately blocked a fire door for their own gains. I don't think they'd stay an employee for very long, in other words.

Anyway, this is the sort of "spring cleaning" I've been doing of late. It's a neat concept I picked up from reading about other programming languages, and I encourage other people to give it a spin. You might like what it does to your code.