logoalt Hacker News

Joker_vD • today at 6:00 AM • 14 replies • view on HN

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.

Replies

gucci-on-fleek • today at 7:41 AM

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;
  }
➕ show 1 reply
layer8 • today at 9:47 PM

In C you can return a struct with two members. The reason the standard library doesn’t do this isn’t ergonomics, it’s performance, or maybe the traditional inability of compilers to map it to what would be the idiomatic register assignment, due to how ABIs are defined.

kaashif • today at 6:46 AM

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.

tester756 • today at 8:04 AM

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"); }

➕ show 1 reply
wongarsu • today at 8:24 AM

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

➕ show 3 replies
sdcfgy • today at 8:44 AM

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;
  }
➕ show 2 replies
Kevin_Flynn • today at 11:44 AM

Is returning 2 values from a function ever required ?

If your code is organized well, and your functions act on context structures, a pass/fail result, or an error code where 0 = success, and everything else is an error, or a pointer to a structure with multiple values, seems to be entirely sufficient.

I quite prefer C, over C++ and other perhaps "more expressive" languages, exactly because it is simpler. It does more with less.

"It can scarcely be denied that the supreme goal of all theory is to make the irreducible basic elements as simple and as few as possible without having to surrender the adequate representation of a single datum of experience." - Albert Einstein

➕ show 1 reply
muvlon • today at 7:49 AM

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.

➕ show 2 replies
dwattttt • today at 7:33 AM

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.

edwcross • today at 6:58 AM

errno is global, but also _Thread_local, to help mitigate issues in a multithreaded environment. A kludge over a kludge...

➕ show 1 reply
StilesCrisis • today at 11:17 AM

C allows struct returns, and C++ allows something quite close to what you've written verbatim. But in 1989, there was less of a focus on error handling; most code wasn't checking errno at all, so why spend time returning it to every caller?

jdthedisciple • today at 8:24 AM

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...

psychoslave • today at 8:27 AM

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 */ }
➕ show 1 reply
fragmede • today at 6:02 AM

Thank you for reminding me that, while Python is slow, there are still reasons to pick it over the alternatives.

➕ show 1 reply