Remix.run Logo
▲ 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.