| ▲ | Joker_vD 4 hours ago |
| 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 two values from a function call has caused people to invent all kinds of insane APIs/contracts. [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
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. |
|
| ▲ | gucci-on-fleek 2 hours ago | parent | next [-] |
| C has supported struct returns since at least C89: /* 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;
}
Needing to typedef a unique struct for every possible function return type isn't very ergonomic, but you can work around that with C23: // 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;
}
|
|
| ▲ | wongarsu 2 hours ago | parent | prev | next [-] |
| 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 Of course C++ gives you the option to do any of the above, and still contains insane API contracts |
| |
| ▲ | qznc an hour ago | parent | next [-] | | I would not describe exceptions as return values. Speaking in a C context, exceptions are more like "longjmp" than "return". | |
| ▲ | cubefox an hour ago | parent | prev [-] | | 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. 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. 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. |
|
|
| ▲ | tester756 2 hours ago | parent | prev | next [-] |
| Wut? C# besides all other ways to return Tuples, Result<T>s, NULLable, etc, etc. Still had something as sane as: if (double.TryParse("5.x5", out var val))
{
Console.WriteLine(val);
}
else
{
Console.WriteLine("err");
} |
|
| ▲ | kaashif 3 hours ago | parent | prev | next [-] |
| 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. |
|
| ▲ | sdcfgy an hour ago | parent | prev | next [-] |
| 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. Example: #define E_DIVZERO -100
int divide(float a, float b, float *result) {
if (b == 0.0) return E_DIVZERO;
*result = a / b;
}
|
|
| ▲ | muvlon 2 hours ago | parent | prev | next [-] |
| 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. |
| |
| ▲ | cubefox 7 minutes ago | parent [-] | | Strictly speaking the unions in TS are untagged/set-theoretic, which is different from Rust or Zig. |
|
|
| ▲ | dwattttt 2 hours ago | parent | prev | next [-] |
| 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. 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. |
|
| ▲ | jdthedisciple 2 hours ago | parent | prev | next [-] |
| 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... |
|
| ▲ | edwcross 3 hours ago | parent | prev | next [-] |
| errno is global, but also _Thread_local, to help mitigate issues in a multithreaded environment. A kludge over a kludge... |
|
| ▲ | psychoslave 2 hours ago | parent | prev | next [-] |
| Doesn't even need two return values. One tagged union suffices: Result(double) x = strtod·or·error(s);
if (!x.ok) {
handle(x.error);
return;
}
use(x.value);
Which in real C, with no language support, is just: typedef struct {
double value;
int error; /* 0 = ok */
} maybe·double;
maybe·double x = strtod·or·error(s, NULL);
if (x.error) { /* handle */ }
|
|
| ▲ | fragmede 4 hours ago | parent | prev [-] |
| Thank you for reminding me that, while Python is slow, there are still reasons to pick it over the alternatives. |
| |
| ▲ | nvme0n1p1 4 hours ago | parent [-] | | Basically all commonly used languages support this nowadays. Even ones you might not expect like C++ and C# have added it. The only major exceptions that come to mind are C and Java. | | |
| ▲ | RossBencina 2 hours ago | parent [-] | | You have been able to return a struct by value in C since at least C89: typedef struct {
double d;
int err;
} Ret;
Ret foo(void) {
Ret r = { 0.0, 43};
return r;
}
int main() {
Ret r = foo();
}
You can't return a bare fixed-size array though. |
|
|