Como se sugiere en esta respuesta , es una cuestión de soporte de hardware, aunque la tradición en el diseño del lenguaje también juega un papel importante.
cuando una función regresa deja un puntero al objeto que regresa en un registro específico
De los tres primeros idiomas, Fortran, Lisp y COBOL, el primero utilizó un único valor de retorno, ya que fue modelado en matemáticas. El segundo devolvió un número arbitrario de parámetros de la misma manera en que los recibió: como una lista (también se podría argumentar que solo pasó y devolvió un solo parámetro: la dirección de la lista). El tercer retorno cero o un valor.
Estos primeros idiomas influyeron mucho en el diseño de los idiomas que los siguieron, aunque el único que devolvió múltiples valores, Lisp, nunca ganó mucha popularidad.
Cuando llegó C, aunque estaba influenciado por los lenguajes anteriores, se enfocó en el uso eficiente de los recursos de hardware, manteniendo una estrecha asociación entre lo que hizo el lenguaje C y el código de máquina que lo implementó. Algunas de sus características más antiguas, como las variables "auto" frente a "registro", son el resultado de esa filosofía de diseño.
También debe señalarse que el lenguaje ensamblador fue muy popular hasta los años 80, cuando finalmente comenzó a eliminarse progresivamente del desarrollo convencional. Las personas que escribieron compiladores y crearon lenguajes estaban familiarizados con el ensamblaje y, en su mayor parte, se apegaron a lo que funcionaba mejor allí.
La mayoría de los idiomas que divergieron de esta norma nunca encontraron mucha popularidad y, por lo tanto, nunca jugaron un papel importante en las decisiones de los diseñadores de idiomas (quienes, por supuesto, se inspiraron en lo que sabían).
Así que vamos a examinar el lenguaje ensamblador. Veamos primero el 6502 , un microprocesador de 1975 que fue utilizado por las microcomputadoras Apple II y VIC-20. Era muy débil en comparación con lo que se usaba en el mainframe y las minicomputadoras de la época, aunque potente en comparación con las primeras computadoras de 20, 30 años antes, en los albores de los lenguajes de programación.
Si nos fijamos en la descripción técnica, tiene 5 registros más algunos indicadores de un bit. El único registro "completo" fue el Contador de programas (PC), que registra los puntos a la siguiente instrucción que se ejecutará. Los otros registros donde el acumulador (A), dos registros de "índice" (X e Y), y un puntero de pila (SP).
Llamar a una subrutina coloca la PC en la memoria señalada por el SP, y luego disminuye el SP. Volver de una subrutina funciona a la inversa. Uno puede empujar y extraer otros valores en la pila, pero es difícil referirse a la memoria en relación con el SP, por lo que escribir subrutinas reentrantes fue difícil. Lo que damos por sentado, llamar a una subrutina en cualquier momento que nos parezca, no era tan común en esta arquitectura. A menudo, se crearía una "pila" separada para que los parámetros y la dirección de retorno de la subrutina se mantuvieran separados.
Si observa el procesador que inspiró el 6502, el 6800 , tenía un registro adicional, el Registro de índice (IX), tan ancho como el SP, que podría recibir el valor del SP.
En la máquina, llamar a una subrutina reentrante consistía en insertar los parámetros en la pila, presionar la PC, cambiar la PC a la nueva dirección, y luego la subrutina empujaría sus variables locales en la pila . Debido a que se conoce el número de variables y parámetros locales, el direccionamiento se puede hacer en relación con la pila. Por ejemplo, una función que recibe dos parámetros y tiene dos variables locales se vería así:
SP + 8: param 2
SP + 6: param 1
SP + 4: return address
SP + 2: local 2
SP + 0: local 1
Se puede llamar cualquier número de veces porque todo el espacio temporal está en la pila.
El 8080 , utilizado en TRS-80 y una gran cantidad de microcomputadoras basadas en CP / M podría hacer algo similar al 6800, presionando SP en la pila y luego apareciendo en su registro indirecto, HL.
Esta es una forma muy común de implementar cosas, y obtuvo aún más soporte en procesadores más modernos, con el puntero base que hace que el volcado de todas las variables locales antes de regresar sea fácil.
El problema es que, ¿cómo devuelves algo ? Los registros del procesador no eran muy numerosos desde el principio, y a menudo era necesario usar algunos de ellos incluso para averiguar qué memoria se debe abordar. Devolver cosas en la pila sería complicado: tendrías que hacer estallar todo, guardar la PC, presionar los parámetros de devolución (¿qué se almacenaría en ese momento?), Luego presionar la PC nuevamente y regresar.
Entonces, lo que generalmente se hizo fue reservar un registro para el valor de retorno. El código de llamada sabía que el valor de retorno estaría en un registro particular, que tendría que conservarse hasta que pudiera guardarse o usarse.
Veamos un lenguaje que permite múltiples valores de retorno: Forth. Lo que Forth hace es mantener una pila de retorno (RP) y una pila de datos (SP) separadas, de modo que todo lo que una función tenía que hacer era hacer estallar todos sus parámetros y dejar los valores de retorno en la pila. Como la pila de devolución estaba separada, no se interpuso en el camino.
Como alguien que aprendió lenguaje ensamblador y Forth en los primeros seis meses de experiencia con computadoras, los valores de retorno múltiples me parecen completamente normales. Los operadores como Forth's /mod
, que devuelven la división entera y el resto, parecen obvios. Por otro lado, puedo ver fácilmente cómo alguien cuya experiencia inicial fue C mente encuentra ese concepto extraño: va en contra de sus expectativas arraigadas de lo que es una "función".
En cuanto a las matemáticas ... bueno, estaba programando computadoras mucho antes de llegar a las funciones en las clases de matemáticas. No es toda una sección de CS y lenguajes de programación que está influida por las matemáticas, pero, de nuevo, hay una sección entera que no es.
Por lo tanto, tenemos una confluencia de factores en los que las matemáticas influyeron en el diseño temprano del lenguaje, donde las restricciones de hardware dictaron lo que se implementó fácilmente, y donde los lenguajes populares influyeron en la evolución del hardware (la máquina Lisp y los procesadores de la máquina Forth fueron trucos en este proceso).