It's quite astonishing how the historical inability of commonly used programming languages (from FORTRAN all the way to, say, Java/C#) to ergonomically return <i>two</i> values from a function call has caused people to invent all kinds of insane APIs/contracts.<p><pre><code> [double x, errno_t errno] = strtod(s, nullptr);
if (errno != 0) {
// ...
}
[x, _] = strtod(s, nullptr); // When you're certain the error is impossible or just don't care
</code></pre>
Instead, we're stuck with "check if the return value is -1 and if it is, check the global variable errno" at best, and with "the function doesn't really reliably reports its errors so don't feed it malformed data, idc" at worst.
C has supported struct returns since at least C89:<p><pre><code> /* Compile with "gcc -std=c89 -pedantic -Wall -Wextra -Werror". */
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct {
double value;
enum { NO_ERROR = 0, PARSE_ERROR, TOO_BIG } error;
} result_t;
static result_t parse_double(char const *str) {
char *endptr;
double parsed;
result_t result;
errno = 0;
parsed = strtod(str, &endptr);
if (endptr == str) {
result.value = 0.0;
result.error = PARSE_ERROR;
} else if (errno == ERANGE) {
result.value = 0.0;
result.error = TOO_BIG;
} else {
result.value = parsed;
result.error = NO_ERROR;
}
return result;
}
int main(void) {
result_t r;
r = parse_double("1.23");
printf("value: %f, error: %d\n", r.value, r.error);
r = parse_double("abc");
printf("value: %f, error: %d\n", r.value, r.error);
r = parse_double("1e5000");
printf("value: %f, error: %d\n", r.value, r.error);
return 0;
}
</code></pre>
Needing to typedef a unique struct for every possible function return type isn't very ergonomic, but you can work around that with C23:<p><pre><code> // Compile with "gcc -std=c23 -pedantic -Wall -Wextra -Werror".
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
typedef enum {
NO_ERROR = false,
PARSE_ERROR,
TOO_BIG,
} errors;
static struct { double value; errors error; } parse_double(char const *str) {
errno = 0;
char *endptr = nullptr;
double parsed = strtod(str, &endptr);
typeof(parse_double(str)) result;
if (endptr == str) {
result.value = 0.0;
result.error = PARSE_ERROR;
} else if (errno == ERANGE) {
result.value = 0.0;
result.error = TOO_BIG;
} else {
result.value = parsed;
result.error = NO_ERROR;
}
return result;
}
int main() {
auto r = parse_double("1.23");
printf("value: %f, error: %d\n", r.value, r.error);
r = parse_double("abc");
printf("value: %f, error: %d\n", r.value, r.error);
r = parse_double("1e5000");
printf("value: %f, error: %d\n", r.value, r.error);
return 0;
}</code></pre>
Java and C# famously solve this with a hidden implicit return value for errors: the exception. The current trend in language design is to take the same idea and make it explicit instead, using sum types. Like Rust's `Result`, where the result contains either the normal value or an error, but never both<p>Of course C++ gives you the option to do any of the above, and still contains insane API contracts
I would not describe exceptions as return values. Speaking in a C context, exceptions are more like "longjmp" than "return".
Arguably the better solution compared to these "algebraic sum types" (they go by countless different names) are set-theoretic union types, used by a few languages such as TypeScript.<p>They allow for types like "String or Integer". A function which returns a value of type "A" can automatically also be assigned to a variable of type "A or B", and a function which accepts a parameter of type "A or B" can automatically aldo be passed a variable of type "A". Which is not the case for algebraic sum types, which require somewhat inconvenient wrapping or unwrapping.<p>Unlike algebraic sum types, set-theoretic union types are also not necessarily mutually exclusive. E.g. a number of type "Integer or Float" could be both. Moreover, set-theoretic types can be generalized to arbitrary Boolean combinations of types, e.g. "A or (B and not C)", possibly involving disjunctions, conjunctions, and negations (since {and, or, not} are functionally complete). That's not possible for algebraic sum/product data types. Though there is no mainstream language yet with a set-theoretic type system that supports negation types.
Wut? C# besides all other ways to return Tuples, Result<T>s, NULLable, etc, etc.<p>Still had something as sane as:<p>if (double.TryParse("5.x5", out var val))
{
Console.WriteLine(val);
}
else
{
Console.WriteLine("err");
}
C++/C#/Python absolutely do have ergonomic ways to do this (and have for decades by this point...) but programmers continue to use sentinel values. It is a bit puzzling.
When I write C I tend to always return any error code on the stack and pass the pointer to where I want the result to go. Makes managing memory easier. The owner gets to allocate.<p>Example:<p><pre><code> #define E_DIVZERO -100
int divide(float a, float b, float *result) {
if (b == 0.0) return E_DIVZERO;
*result = a / b;
}</code></pre>
Heck, even using multiple returns for this feels like a kludge to me at this point, when more advanced languages like TypeScript, Rust or Zig offer sum types/tagged unions.
I'd argue it's as much a mentality as language barrier; high assurance code would typically always return an error code, and use out parameters rather than return values. This has been possible for as long as C has existed (I imagine), it's just not common practice.<p>I have a pet theory that new languages benefit at least as much from an opportunity to reset "common practice" / defaults as they do syntax/semantic improvements.
errno is global, but also _Thread_local, to help mitigate issues in a multithreaded environment. A kludge over a kludge...
Tbf from what I coarsely remember having > 1 return value from a function was taught as bad practice and to be avoided in my days...
Doesn't even need two return values. One tagged union suffices:<p><pre><code> Result(double) x = strtod·or·error(s);
if (!x.ok) {
handle(x.error);
return;
}
use(x.value);
</code></pre>
Which in real C, with no language support, is just:<p><pre><code> typedef struct {
double value;
int error; /* 0 = ok */
} maybe·double;
maybe·double x = strtod·or·error(s, NULL);
if (x.error) { /* handle */ }</code></pre>
Thank you for reminding me that, while Python is slow, there are still reasons to pick it over the alternatives.