1. 13
  1.  

  2. 6

    Sometimes I wonder why recent languages don’t support multiple [return] values. We are even using destructuring assignment as a poor man’s multiple values. It solves the problem described in the article w/o cluttering the functions code with conditionals

    1. 2

      Most recent languages honestly do (Python, Elixir, Go, Rust, Kotlin, and Swift (I think) to name a few), and they’re getting backported in some others (C# 7 and C++17, for example) by improving tuples.

      I think the bigger issue with most of these languages is they don’t have anything like ML/Rust/Erlang-style fail-if-the-return-looks-like-this mode that lets you effectively use them as if they were exceptions when you want—e.g., the frequent pattern in Erlang/Elixir of Foo, ok = some_fun(), or Rust’s let v = bar()?;. Without that, you either do what Ruby’s doing here (throw an exception if exception: true is passed), or you have to explicitly check yourself (where Go is the extreme example of if foo, err := bar(); err != nil { ... } being a constant refrain).

      1. 1

        I don’t now about Elixir, Go or Kotlin, but neither Python or Go support multiple-values. In Go you have to accept to assign all the values the function returns, which kinda defeats the whole purpose of having multiple return values in the first place! (I checked in a REPL just to be sure). I haven’t kept up with recent developments in Python3 but AFAIK Pythonistas use lists or dictionaries to mimic multiple-values. Similarly in ES6 people use dictionaries + destructuring assignment in ES6. For Example

        const foo = () => ({a: 1, b: 2})
        
        const {a} = foo()
        
        console.log(a)
        

        Prints 1. The ES6 approach is better than Golangs and Python solution but it still forces the caller to reuse the name the callee decided on[0] plus all the return values are allocated on the stack.

        AFAICT Rust again doesn’t support multiple value but uses pattern matching (which destructuring assignment is one example of) to mimic them.

        Multiple values are useful for more than exception handling btw, one example is the division operator

        [0]: Yeah I know one can rebind the name, but the syntax is cumbersome to use and one still have to remember the name of the value as opposed to its purpose, which is easier to remember).

        1. 2

          Well, now I’m confused. On the one hand, you say,

          …neither Python [n]or Go support multiple-values

          …and then immediately link to a REPL that starts with “Go supports multiple return values,” and even shows an example of ignoring some of the returned values. Likewise, while I agree with you that Python uses sequences for multiple returns behind-the-scenes, the use in practice looks like

          def foo(a, b): return b, a
          c, d = foo(1, 2)
          

          which is indistinguishable in practice from what I do in an ML. (And in Python 3.5 and later, you can even do things like a, b, *rest = foo().) Python even has multiple-return division operations (see math.fmod from as far back as Python 2.7).

          Could you give an example of a language that does what you want, and how it differs from the previous? I feel as if we’re using the same words, but for radically different things.

          1. 2

            I’m sorry I wasn’t more clear in my previous reply.

            Could you give an example of a language that does what you want, and how it differs from the previous?

            Common Lisp.

            and how it differs from the previous?

            • The caller doesn’t have to be aware that the callee supports multiple values unless they want to use it.
            • The compiler can determine at the call site how many values are going to be used and allocate appropriately.

            which is indistinguishable in practice from what I do in an ML.

            Yes, and to the best of my knowledge MLs don’t support multiple values. Pattern matching enables one to mimic multiple values, which is what most recent language are doing and it provides at least 90% of the value of having multiple values.

            The Go REPL example shows that the caller must be aware of the amount of values the callee is returning, which leads to clumsy UX. I’m guessing you wanted to link to modf but I’m not sure what you were getting at. The CL example of / shows how we can re use the same function instead of having mod and div. Similarly be taken advantage retrieving the value in a map where nil indicates absence without affecting the UX of the ‘happy path’ (ie. nil is not possible a value in the map).

            –––––––

            It is kinda ironic that my comment was motivated out of the idea of less is more in language design, in light of all the feature creep I’m seeing recently in JS and Ruby, but it appears that the approach of recent languages is better example of less is more ¯_(ツ)_/¯

            1. 1

              to the best of my knowledge MLs don’t support multiple values

              Returning multiple values (like in Go) is just a special case of tuples, which most MLs support. So you don’t need an extra language feature for this. But I wouldn’t use this for errors like Go does - it makes more sense to use a variant/sum type for this.

    2. 4

      I recommend reading the discussion in Redmine. It seems the rational is that Integer(string) rescue default_value:

      1. is slower than Integer(string, exception: false) || default_value.
      2. generates a lot of noise in debug mode.

      I have mixed feelings about that.

      1. 6

        Noise in debug mode can be a significant usability issue. Performance as well. Even though Ruby isn’t traditionally used for ultra high performance code, doesn’t mean someone somewhere won’t benefit from parsing integers faster.

        I’m more interested in exception: true for calls like system. Getting the exact error for an execution is super helpful, like the ENOENT example.

        1. 1

          Noise in debug mode can be a significant usability issue. Performance as well. Even though Ruby isn’t traditionally used for ultra high performance code, doesn’t mean someone somewhere won’t benefit from parsing integers faster.

          “Someone somewhere” isn’t a good enough criterion to add a feature to the language. It can easily lead to a ton of hacks being piled onto the language.

          My biggest concern is the conflation of a language and a runtime. Performance and debugging are first and foremost runtime issues. I realize not all language constructs can be implemented effectively but in this case it seems Ruby is modified to address purely runtime-related issues.

          Regarding exception: true for system it seems you’re conflating two things - returning error information and raising an exception. Errors can easily be returned as objects instead of being raised an exception.

          My feeling is this addition makes the language less elegant although I understand the underlying motivation.

          1. 1

            “Someone somewhere” was meant to be generous, but I am quite certain this will be useful for many people.

            I think other languages frequently do this worse than this. At the very worst, some have different functions with synonymous names that do one or the other thing. Either way, Ruby can’t just stop throwing ArgumentError in Integer by default, since that would break most code that actually error handles that conversion. This is a pretty good solution given the constraints, and fixing it in the runtime would be hot garbage.

            How would that even work? Right now there is a runtime flag, # frozen_string_literal: true, but that doesn’t really reduce the readability or workability of code. Having something like # kernel_throws_exceptions: true/false would make code outrageously difficult to read. If you alter the behavior of Kernel module functions you’d have to check the header of every file to figure out what the behavior was. If you made it a vm flag, developers would have to know which setting is used when writing code, and users would have to know which setting they have to use when running the code. It just doesn’t work.

            Even if they wanted to break back compat, which they don’t, using things like option types or multiple returns aren’t really idiomatic in ruby. It would be a lot sillier to blatantly go against the grain of the language in order to avoid a slightly awkward optional keyword flag.

            Performance and debugging are first and foremost runtime issues.

            I haven’t disagreed with anything this much in a long long long time. But I’d also prefer not to get into it.

            I realize not all language constructs can be implemented effectively but in this case it seems Ruby is modified to address purely runtime-related issues.

            Not really. Raising exceptions will be slower than returns, always. Unless you deliberately slow down returns to match exceptions, or your language has such weird semantics that returns are necessarily also slow. But both of those situations, aside from not applying to ruby, are ridiculous.

            1. 1

              Just as if (false) ... is dead code and can be eliminated, so too my_method rescue my_value can disable exceptions in my_method. This will be difficult to implement in general (you’d need to enable/disable exceptions at various stack levels depending on how the call site looks like) but should be totally doable for functions implemented in C.

              Option types or multiple returns aren’t idiomatic Ruby and I think this might be my problem here.

      2. 1

        TBH, I feel that’s a pretty ugly workaround to avoid fixing the real problems of a slow exception mechanism and an inconsistent library interface.

        1. 1

          If you read the issue tracker, the performance bit is really secondary to avoiding debug output by jumping through hoops. I think the author took the wrong part to highlight in their write up.

          Exception performance is not the problem to be solved here - exceptions are intended to be, well, exceptional and using them for flow control rather than error handling is a bit of a smell.

          As far as the inconsistent interface, conversion functions have consistency and aren’t intended to mirror implicit casting methods. It’s been a while since I’ve read Confident Ruby, but as I recall it covers the topic really well. Let’s also say that hypothetically you were to resolve that inconsistency - that would be a breaking change which is in all likelihood not worth the cost. This change is backwards compatible, solves the problem of jumping through hoops to avoid debug output, and is consistent in the conversion function interface.

          Now, whether this is something a lot of Rubyists will need to know or care about… probably not. I’d relegate this to the realm of Ruby trivia for a very select set of problems. It’s a good change, though.