¿Hay alguna razón por la cual las funciones en la mayoría de los lenguajes de programación (?) Están diseñadas para admitir cualquier número de parámetros de entrada pero solo un valor de retorno?
En la mayoría de los idiomas, es posible "evitar" esa limitación, por ejemplo, utilizando parámetros externos, punteros de retorno o definiendo / devolviendo estructuras / clases. Pero parece extraño que los lenguajes de programación no hayan sido diseñados para admitir múltiples valores de retorno de una manera más "natural".
¿Hay alguna explicación para esto?
def f(): return (1,2,3)
y entonces usted puede utilizar tupla-desembalaje "dividir" la tupla:a,b,c = f() #a=1,b=2,c=3
. No es necesario crear una matriz y extraer elementos manualmente, no es necesario definir ninguna clase nueva.[a, b] = f()
Vs.[a, b, c] = f()
) y se obtiene dentrof
denargout
. No soy un gran admirador de Matlab, pero esto a veces resulta bastante útil.Respuestas:
Algunos lenguajes, como Python , admiten múltiples valores de retorno de forma nativa, mientras que algunos lenguajes como C # los admiten a través de sus bibliotecas base.
Pero en general, incluso en los idiomas que los admiten, los valores de retorno múltiples no se usan a menudo porque son descuidados:
Es fácil confundir el orden de los valores de retorno
(Por esta misma razón, muchas personas evitan tener demasiados parámetros para una función; ¡algunos incluso llegan a decir que una función nunca debe tener dos parámetros del mismo tipo!)
Están más fuertemente tipados, mantienen los valores de retorno agrupados como una unidad lógica y mantienen los nombres de (propiedades de) los valores de retorno consistentes en todos los usos.
El único lugar en el que son bastante convenientes es en lenguajes (como Python) donde se pueden usar múltiples valores de retorno de una función como múltiples parámetros de entrada para otra. Pero, los casos de uso donde este es un mejor diseño que usar una clase son bastante delgados.
fuente
()
. ¿Es esa una cosa o cero cosas? Personalmente, diría que es una cosa. Puedo asignarx = ()
muy bien, al igual que puedo asignarx = randomTuple()
. En el último, si la tupla devuelta está vacía o no, todavía puedo asignarle la tupla devueltax
.Porque las funciones son construcciones matemáticas que realizan un cálculo y devuelven un resultado. De hecho, mucho de lo que está "bajo el capó" de no pocos lenguajes de programación se enfoca únicamente en una entrada y una salida, con múltiples entradas que son solo una envoltura delgada alrededor de la entrada, y cuando una salida de valor único no funciona, usando un solo estructura cohesiva (o tupla, o
Maybe
) ser la salida (aunque ese valor de retorno "único" se compone de muchos valores).Esto no ha cambiado porque los programadores han encontrado que los
out
parámetros son construcciones incómodas que son útiles solo en un conjunto limitado de escenarios. Al igual que con muchas otras cosas, el soporte no está allí porque la necesidad / demanda no está allí.fuente
En matemáticas, una función "bien definida" es aquella en la que solo hay 1 salida para una entrada dada (como nota al margen, solo puede tener funciones de entrada única, y aún obtener semánticamente múltiples entradas utilizando currículum ).
Para funciones de valores múltiples (por ejemplo, raíz cuadrada de un entero positivo, por ejemplo), es suficiente devolver una colección o secuencia de valores.
Para los tipos de funciones de las que está hablando (es decir, funciones que devuelven múltiples valores, de diferentes tipos ), lo veo un poco diferente de lo que parece: veo la necesidad / uso de nuestros parámetros como una solución para un mejor diseño o un Estructura de datos más útil. Por ejemplo, preferiría que los
*.TryParse(...)
métodos devolvieran unaMaybe<T>
mónada en lugar de usar un parámetro fuera. Piense en este código en F #:El soporte del compilador / IDE / análisis es muy bueno para estas construcciones. Esto resolvería gran parte de la "necesidad" de nuestros params. Para ser completamente honesto, no puedo pensar en ningún otro método que no sea la solución.
Para otros escenarios, los que no recuerdo, basta una simple tupla.
fuente
var (value, success) = ParseInt("foo");
que sería el tipo de tiempo de compilación verificado porque(int, bool) ParseInt(string s) { }
fue declarado. Yo sé que esto se puede hacer con los genéricos, pero aún así se haría para una buena adición idioma.Además de lo que ya se ha dicho cuando observa los paradigmas utilizados en el ensamblaje cuando una función regresa, deja un puntero al objeto que regresa en un registro específico. Si usaran registros variables / múltiples, la función de llamada no sabría dónde obtener los valores devueltos si esa función estuviera en una biblioteca. Eso dificultaría el enlace a las bibliotecas y, en lugar de establecer un número arbitrario de punteros retornables, fueron con uno. Los idiomas de nivel superior no tienen la misma excusa.
fuente
Muchos de los casos de uso en los que habría utilizado múltiples valores de retorno en el pasado simplemente ya no son necesarios con las características del lenguaje moderno. ¿Quieres devolver un código de error? Lanzar una excepción o devolver un
Either<T, Throwable>
. ¿Quieres devolver un resultado opcional? Devolver unOption<T>
. ¿Quieres devolver uno de varios tipos? Devuelve unaEither<T1, T2>
o una unión etiquetada.E incluso en los casos en que realmente necesita devolver múltiples valores, los lenguajes modernos generalmente admiten tuplas o algún tipo de estructura de datos (lista, matriz, diccionario) u objetos, así como alguna forma de enlace de desestructuración o coincidencia de patrones, lo que hace que el empaquetado sus múltiples valores en un solo valor y luego desestructurarlo nuevamente en múltiples valores triviales.
Aquí hay algunos ejemplos de lenguajes que no admiten la devolución de múltiples valores. Realmente no veo cómo agregar soporte para múltiples valores de retorno los haría significativamente más expresivos para compensar el costo de una nueva función de lenguaje.
Rubí
Pitón
Scala
Haskell
Perl6
fuente
sort
función Matlab :sorted = sort(array)
devuelve solo la matriz ordenada, mientras que[sorted, indices] = sort(array)
devuelve ambas. La única forma en que puedo pensar en Python sería pasar una bandera a losort
largo de las líneas desort(array, nout=2)
osort(array, indices=True)
.[a, b, c] = func(some, thing)
) y actuar en consecuencia. Esto es útil, por ejemplo, si calcular el primer argumento de salida es barato pero calcular el segundo es costoso. No estoy familiarizado con ningún otro idioma donde el equivalente de Matlabsnargout
esté disponible en tiempo de ejecución.sorted, _ = sort(array)
.sort
función puede decir que no necesita calcular los índices? Eso es genial, no lo sabía.La verdadera razón por la que un único valor de retorno es tan popular son las expresiones que se usan en tantos idiomas. En cualquier lenguaje en el que pueda tener una expresión como la
x + 1
que ya está pensando en términos de valores de retorno únicos porque evalúa una expresión en su cabeza dividiéndola en pedazos y decidiendo el valor de cada pieza. Mirasx
y decides que su valor es 3 (por ejemplo), miras 1 y luego mirasx + 1
y poner todo junto para decidir que el valor del todo es 4. Cada parte sintáctica de la expresión tiene un valor, no cualquier otro número de valores; esa es la semántica natural de las expresiones que todos esperan. Incluso cuando una función devuelve un par de valores, en realidad sigue devolviendo un valor que está haciendo el trabajo de dos valores, porque la idea de una función que devuelve dos valores que de alguna manera no están envueltos en una sola colección es demasiado extraña.La gente no quiere lidiar con la semántica alternativa que se requeriría para que las funciones devuelvan más de un valor. Por ejemplo, en un lenguaje basado en la pila como Forth puede tener cualquier cantidad de valores de retorno porque cada función simplemente modifica la parte superior de la pila, haciendo estallar las entradas y empujando las salidas a voluntad. Es por eso que Forth no tiene el tipo de expresiones que tienen los lenguajes normales.
Perl es otro lenguaje que a veces puede actuar como si las funciones devolvieran múltiples valores, aunque generalmente solo se considera devolver una lista. La forma en que las listas "se interpolan" en Perl nos da listas como las
(1, foo(), 3)
que podrían tener 3 elementos, como la mayoría de las personas que no saben que Perl esperaría, pero podrían tener tan solo 2 elementos, 4 elementos o un mayor número de elementos dependiendo defoo()
. Las listas en Perl se aplanan para que una lista sintáctica no siempre tenga la semántica de una lista; puede ser simplemente una parte de una lista más grande.Otra forma de hacer que las funciones devuelvan múltiples valores sería tener una semántica de expresión alternativa donde cualquier expresión puede tener múltiples valores y cada valor representa una posibilidad.
x + 1
Vuelva a tomar , pero esta vez imagine quex
tiene dos valores {3, 4}, entonces los valores dex + 1
serían {4, 5}, y los valores dex + x
serían {6, 8}, o tal vez {6, 7, 8} , dependiendo de si una evaluación puede usar múltiples valores parax
. Un lenguaje como ese podría implementarse utilizando el retroceso al igual que Prolog utiliza para dar múltiples respuestas a una consulta.En resumen, una llamada de función es una unidad sintáctica única y una unidad sintáctica única tiene un valor único en la semántica de expresión que todos conocemos y amamos. Cualquier otra semántica te obligaría a formas extrañas de hacer las cosas, como Perl, Prolog o Forth.
fuente
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.
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í:
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).
fuente
Los lenguajes funcionales que conozco pueden devolver múltiples valores fácilmente mediante el uso de tuplas (en lenguajes dinámicamente escritos, incluso puede usar listas). Las tuplas también son compatibles en otros idiomas:
En el ejemplo anterior,
f
es una función que devuelve 2 ints.Del mismo modo, ML, Haskell, F #, etc., también pueden devolver estructuras de datos (los punteros tienen un nivel demasiado bajo para la mayoría de los idiomas). No he oído hablar de un lenguaje GP moderno con tal restricción:
Finalmente, los
out
parámetros se pueden emular incluso en lenguajes funcionales medianteIORef
. Hay varias razones por las cuales no hay soporte nativo para las variables en la mayoría de los idiomas:Semántica poco clara : ¿la siguiente función imprime 0 o 1? Sé de lenguajes que imprimirían 0, y los que imprimirían 1. Hay beneficios para ambos (ambos en términos de rendimiento, así como también con el modelo mental del programador):
Efectos no localizados : como en el ejemplo anterior, puede encontrar que puede tener una cadena larga y que la función más interna afecta el estado global. En general, hace que sea más difícil razonar sobre cuáles son los requisitos de la función y si el cambio es legal. Dado que la mayoría de los paradigmas modernos intentan localizar los efectos (encapsulación en OOP) o eliminar los efectos secundarios (programación funcional), entra en conflicto con esos paradigmas.
Ser redundante : si tiene tuplas, tiene el 99% de la funcionalidad de los
out
parámetros y el 100% de uso idiomático. Si agrega punteros a la mezcla, cubre el 1% restante.Tengo problemas para nombrar un idioma que no pueda devolver múltiples valores mediante el uso de una tupla, clase o
out
parámetro (y en la mayoría de los casos se permiten 2 o más de esos métodos).fuente
Creo que se debe a expresiones como
(a + b[i]) * c
.Las expresiones están compuestas de valores "singulares". Por lo tanto, una función que devuelve un valor singular puede usarse directamente en una expresión, en lugar de cualquiera de las cuatro variables que se muestran arriba. Una función de salida múltiple es al menos algo torpe en una expresión.
Personalmente, creo que esta es la cosa que hace especial a un valor de retorno singular. Podría solucionar este problema agregando sintaxis para especificar cuál de los múltiples valores de retorno que desea usar en una expresión, pero seguramente será más torpe que la buena notación matemática, que es concisa y familiar para todos.
fuente
Sí complica un poco la sintaxis, pero no hay una buena razón a nivel de implementación para no permitirla. Al contrario de algunas de las otras respuestas, devolver múltiples valores, cuando estén disponibles, conduce a un código más claro y más eficiente. No puedo contar con qué frecuencia he deseado poder devolver una X y una Y, o un booleano de "éxito" y un valor útil.
fuente
[out]
parámetro, pero prácticamente todas devuelven unHRESULT
(código de error). Sería bastante práctico conseguir un par allí. En lenguajes que tienen un buen soporte para tuplas, como Python, esto se usa en una gran cantidad de código que he visto.sort
función normalmente ordena una matriz:sorted_array = sort(array)
. A veces también necesito los índices correspondientes:[sorted_array, indices] = sort(array)
. A veces solo quiero los índices:[~, indices]
= sort (array). The function
sort` en realidad puede decir cuántos argumentos de salida se necesitan, por lo que si se necesita trabajo adicional para 2 salidas en comparación con 1, puede calcular esas salidas solo si es necesario.En la mayoría de los idiomas donde se admiten funciones, puede usar una llamada a función en cualquier lugar donde se pueda usar una variable de ese tipo: -
Si la función devuelve más de un valor, esto no funcionará. Los lenguajes de tipo dinámico, como python, le permitirán hacer esto, pero, en la mayoría de los casos, arrojará un error de tiempo de ejecución a menos que pueda resolver algo sensato que hacer con una tupla en el medio de una ecuación.
fuente
foo()[0]
Solo quiero construir sobre la respuesta de Harvey. Originalmente encontré esta pregunta en un sitio de tecnología de noticias (arstechnica) y encontré una explicación sorprendente que siento que realmente responde al núcleo de esta pregunta y que carece de todas las otras respuestas (excepto la de Harvey):
El origen del retorno único de las funciones reside en el código de la máquina. A nivel de código de máquina, una función puede devolver un valor en el registro A (acumulador). Cualquier otro valor de retorno estará en la pila.
Un lenguaje que admita dos valores de retorno lo compilará como código de máquina que devuelve uno y coloca el segundo en la pila. En otras palabras, el segundo valor de retorno terminaría como un parámetro de salida de todos modos.
Es como preguntar por qué la asignación es una variable a la vez. Podría tener un lenguaje que permitiera a, b = 1, 2 por ejemplo. Pero terminaría en el nivel de código de máquina siendo a = 1 seguido de b = 2.
Hay algo de racional en tener construcciones de lenguaje de programación que tengan cierta semejanza con lo que sucederá realmente cuando el código se compila y se ejecuta.
fuente
Comenzó con las matemáticas. FORTRAN, llamado así por "Formula Translation", fue el primer compilador. FORTRAN estaba y está orientado a física / matemática / ingeniería.
COBOL, casi tan antiguo, no tenía un valor de retorno explícito; Apenas tenía subrutinas. Desde entonces ha sido principalmente inercia.
Go , por ejemplo, tiene múltiples valores de retorno, y el resultado es más limpio y menos ambiguo que el uso de parámetros "out". Después de un poco de uso, es muy natural y eficiente. Recomiendo que se consideren múltiples valores de retorno para todos los idiomas nuevos. Tal vez también para idiomas antiguos.
fuente
Probablemente tenga más que ver con el legado de cómo se realizan las llamadas a funciones en las instrucciones de la máquina del procesador y el hecho de que todos los lenguajes de programación derivan del código de la máquina: por ejemplo, C -> Ensamblaje -> Máquina.
Cómo los procesadores realizan llamadas de función
Los primeros programas fueron escritos en código máquina y luego ensamblados. Los procesadores admitieron llamadas de funciones al enviar una copia de todos los registros actuales a la pila. Al volver de la función, el conjunto de registros guardados se abrirá de la pila. Por lo general, un registro se dejó intacto para permitir que la función de retorno devuelva un valor.
Ahora, en cuanto a por qué los procesadores fueron diseñados de esta manera ... probablemente era una cuestión de limitaciones de recursos.
fuente