Go's multiple return values and (Go) types

March 19, 2025

Recently I read Were multiple return values Go's biggest mistake? (via), which wishes that Go had full blown tuple types (to put my spin on it). One of the things that struck me about Go's situation when I read the article is exactly the inverse of what the article is complaining about, which is that because Go allows multiple values for function return types (and in a few other places), it doesn't have to have tuple types.

One problem with tuple types in a statically typed language is that they must exist as types, whether declared explicitly or implicitly. In a language like Go, where type definitions create new distinct types even if the structure is the same, it isn't particularly difficult to wind up with an ergonomics problem. Suppose that you want to return a tuple that is a net.Conn and an error, a common pair of return values in the net package today. If that tuple is given a named type, everyone must use that type in various places; merely returning or storing an implicitly declared type that's structurally the same is not acceptable under Go's current type rules. Conversely, if that tuple is not given a type name in the net package, everyone is forced to stick to an anonymous tuple type. In addition, this up front choice is now an API; it's not API compatible to give your previously anonymous tuple type a name or vice versa, even if the types are structurally compatible.

(Since returning something and error is so common an idiom in Go, we're also looking at either a lot of anonymous types or a lot more named types. Consider how many different combinations of multiple return values you find in the net package alone.)

One advantage of multiple return values (and the other forms of tuple assignment, and for range clauses) is that they don't require actual formal types. Functions have a 'result type', which doesn't exist as an actual type, but you also needed to handle the same sort of 'not an actual type' thing for their 'parameter type'. My guess is that this let Go's designers skip a certain amount of complexity in Go's type system, because they didn't have to define an actual tuple (meta-)type or alternately expand how structs worked to cover the tuple usage case,

(Looked at from the right angle, structs are tuples with named fields, although then you get into questions of nested structs act in tuple-like contexts.)

A dynamically typed language like Python doesn't have this problem because there are no explicit types, so there's no need to have different types for different combinations of (return) values. There's simply a general tuple container type that can be any shape you want or need, and can be created and destructured on demand.

(I assume that some statically typed languages have worked out how to handle tuples as a data type within their type system. Rust has tuples, for example; I haven't looked into how they work in Rust's type system, for reasons.)


Comments on this page:

By lilydjwg at 2025-03-20 05:22:48:

In Rust (and likely other languages), tuples (and slices) of the same type(s) are considered the same type. Unlike named structs, they are not defined by the programmer anyway.

One thing I find multiple return values annoying is that I can't assign the returned values to a variable and pass them on later.

By cks at 2025-03-20 12:09:50:

I think that if Go had tuples as a type and used them for multiple returns, there would have been more pressure toward a model of structural equality instead of name equality, or alternately the addition of type aliases earlier than Go did (so that you could give names to otherwise unnamed tuple types for your convenience, while still preserving assignability).

My guess is that the people would want names for tuple types when they were put into structs, passed around as argument types, and so on. Partly this is because there's only so many times you want to type 'tuple(net.Conn, error)' before you throw up your hands, and partly because some return tuple types would have been generic enough to make people want to name them instead of having to remember what 'tuple(string, int, int)' actually was.

By judson at 2025-03-20 15:13:49:

A dynamically typed language like Python doesn't have this problem

And, yet, Scheme does have multiple-value returns, and they're weird just like Go's are. The Kernel programming language, heavily inspired by Scheme, pushed back against that (see page 52). Its solution is basically Python-style tuple-unpacking, generalized to trees; plus the ability to write "#ignore" instead of a variable name, with the obvious effect.

One advantage of multiple return values … is that they don't require actual formal types.

But that's not really an advantage in and of itself. The advantage is that it frees you from any inconvenience associated with that formality; that inconvenience is not inherent, but is a result of specific language-design decisions. Ergonomics, as you say, and as the linked article implies. The Kernel report, which I linked above, mentions how various language differences can accumulate in a way that pushes designers toward "baroque" features.

I don't know Go, but "type definitions create new distinct types even if the structure is the same" stood out to me. And to the first commenter, and apparently to the article's author (although the "Results" section doesn't state it outright, I understand the proposed code as implying identical-anonymous-structure-equivalency).

By cks at 2025-03-20 16:12:46:

Broadly and briefly, the 'type definitions create new types' issue is nominal typing, as opposed to structural typing (although you can mix both in the same language). This is a long standing division in language design where there's no right or wrong answer that people have agreed on. Go comes down on the nominal typing side, although it's not completely strict about it.

By judson at 2025-03-20 17:32:52:

Hmm. Maybe "type definitions create new types" isn't even the real problem to fix. The need to re-define a type is also an artifact of language design, and having a way to refer to the existing type could eliminate that need. It could be a well-designed "auto" keyword (that preserves the benefit of formal typing), or some convenient way to say "a list made up of whatever function Foo() returns".

Written on 19 March 2025.
« How ZFS knows and tracks the space usage of datasets
Go's choice of multiple return values was the simpler option »

Page tools: View Source, View Normal.
Search:
Login: Password:

Last modified: Wed Mar 19 23:31:30 2025
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.