As suggested in this answer, it is a matter of hardware support, though tradition in language design also plays a role.
when a function returns it leaves a pointer to the returning object in a specific register
Of the three first languages, Fortran, Lisp and COBOL, the first used a single return value as it was modeled on mathematics. The second returned an arbitrary number of parameters the same way it received them: as a list (it could also be argued that it only passed and returned a single parameter: the address of the list). The third return zero or one value.
These first languages influenced a lot on the design of the languages that followed them, though the only one which returned multiple values, Lisp, never gathered much popularity.
When C came, while influenced by the languages before it, it gave a great focus on efficient use of hardware resource, keeping a close association between what the C language did and the machine code that implemented it. Some of its oldest features, such as "auto" vs "register" variables, are a result of that design philosophy.
It must be also pointed out that assembly language was widely popular until the 80s, when it finally started to be phased out of mainstream development. People who wrote compilers and created languages were familiar with assembly, and, for the most part, kept to what worked best there.
Most of the languages that diverged from this norm never found much popularity, and, therefore, never played a strong role influencing the decisions of language designers (who, of course, were inspired by what they knew).
So let's go examine assembly language. Let's look first at the 6502, a 1975 microprocessor that was famously used by the Apple II and VIC-20 microcomputers. It was very weak compared to what was used in the mainframe and minicomputers of the time, though powerful compared to the first computers of 20, 30 years before, at the dawn of programming languages.
If you look at the technical description, it has 5 registers plus a few one-bit flags. The only "full" register was the Program Counter (PC) -- that register points to the next instruction to be executed. The other registers where the accumulator (A), two "index" registers (X and Y), and a stack pointer (SP).
Calling a subroutine puts the PC in the memory pointed to by the SP, and then decrements the SP. Returning from a subroutine works in reverse. One can push and pull other values on the stack, but it is difficult to refer to memory relative to the SP, so writing re-entrant subroutines was difficult. This thing we take for granted, calling a subroutine at any time we feel like, was not so common on this architecture. Often, a separate "stack" would be created so that parameters and subroutine return address would be kept separate.
If you look at the processor that inspired the 6502, the 6800, it had an additional register, the Index Register (IX), as wide as the the SP, which could receive the value from the SP.
On the machine, calling a re-entrant subroutine consisted of pushing the parameters on the stack, pushing PC, changing PC to the new address, and then the subroutine would push its local variables on the stack. Because the number of local variables and parameters is known, addressing them can be done relative to the stack. For example, a function receiving two parameters and having two local variables would look like this:
SP + 8: param 2
SP + 6: param 1
SP + 4: return address
SP + 2: local 2
SP + 0: local 1
It can be called any number of times because all the temporary space is on the stack.
The 8080, used on TRS-80 and a host of CP/M-based microcomputers could do something similar to the 6800, by pushing SP on the stack and then popping it on its indirect register, HL.
This is a very common way of implementing things, and it got even more support on more modern processors, with the Base Pointer that makes dumping all local variables before returning easy.
The problem, the, is how do you return anything? Processor registers weren't very numerous early on, and one often needed to use some of them even to find out which piece of memory to address. Returning things on the stack would be complicated: you'd have to pop everything, save the PC, push the returning parameters (which would be stored where meanwhile?), then push the PC again and return.
So what was usually done was reserving one register for the return value. The calling code knew the return value would be in a particular register, that would have to be preserved until it could be saved or used.
Let's look at a language that does allow multiple return values: Forth. What Forth does is keeping a separate return stack (RP) and data stack (SP), so that all a function had to do was pop all its parameters and leave the return values on the stack. Since the return stack was separate, it did not get in the way.
As someone who learned assembly language and Forth in the first six month of experience with computers, multiple return values look entirely normal to me. Operators such as Forth's /mod
, which return the integer division and the rest, seem obvious. On the other hand, I can easily see how someone whose early experience was C mind find that concept strange: it goes against their ingrained expectations of what a "function" is.
As for math... well, I was programming computers way before I ever got to functions in mathematics classes. There is a whole section of CS and programming languages which is influenced by mathematics, but, then again, there's a whole section which is not.
So we have a confluence of factors where math influenced early language design, where hardware constraints dictated what was easily implemented, and where the popular languages influenced how the hardware evolved (the Lisp machine and Forth machine processors were roadkills in this process).
def f(): return (1,2,3)
and then you can use tuple-unpacking to "split" the tuple:a,b,c = f() #a=1,b=2,c=3
. No need to create an array and manually extract elements, no need to define any new class.[a, b] = f()
vs.[a, b, c] = f()
) and obtained insidef
bynargout
. I'm not a big fan of Matlab but this actually comes in quite handy at times.