¿Por qué la mayoría de los lenguajes de programación solo admiten la devolución de un único valor de una función? [cerrado]


118

¿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?


40
porque puedes devolver una matriz ...
nathan hayfield

66
Entonces, ¿por qué no solo permitir un argumento? Sospecho que está ligado al habla. Tome algunas ideas, aliéntelas en una cosa que le devuelva a quien sea afortunado / desafortunado como para estar escuchando. El regreso es casi como una opinión. "esto" es lo que haces con "estos".
Erik Reppen

30
Creo que el enfoque de pitón es bastante simple y elegante: cuando se tiene que devolver múltiples valores devuelva una tupla: 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.
Bakuriu

77
Puedo informarle que Matlab tiene un número variable de valores de retorno. El número de argumentos de salida está determinado por la firma de llamada (p. Ej., [a, b] = f()Vs. [a, b, c] = f()) y se obtiene dentro fde nargout. No soy un gran admirador de Matlab, pero esto a veces resulta bastante útil.
gerrit

55
Creo que si la mayoría de los lenguajes de programación están diseñados de esa manera es discutible. En la historia de los lenguajes de programación, se crearon algunos lenguajes muy populares de esa manera (como Pascal, C, C ++, Java, VB clásico), pero hoy en día también hay muchos otros lenguajes que reciben cada vez más fanáticos que permiten un retorno múltiple valores.
Doc Brown

Respuestas:


58

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:

  • Las funciones que devuelven múltiples valores son difíciles de nombrar con claridad .
  • Es fácil confundir el orden de los valores de retorno

    (password, username) = GetUsernameAndPassword()  
    

    (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!)

  • Los lenguajes OOP ya tienen una mejor alternativa a los múltiples valores de retorno: clases.
    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.


50
Es difícil decir que devolver una tupla es devolver varias cosas. Está regresando una tupla . El código que ha escrito simplemente lo descomprime limpiamente usando un poco de azúcar sintáctica.

10
@Lego: no veo una distinción: una tupla es, por definición, múltiples valores. ¿Qué consideraría "valores de retorno múltiples", si no esto?
BlueRaja - Danny Pflughoeft

19
Es una distinción muy confusa, pero considera una Tupla vacía (). ¿Es esa una cosa o cero cosas? Personalmente, diría que es una cosa. Puedo asignar x = ()muy bien, al igual que puedo asignar x = randomTuple(). En el último, si la tupla devuelta está vacía o no, todavía puedo asignarle la tupla devuelta x.

19
... Nunca dije que las tuplas no pudieran usarse para otras cosas. Pero argumentar que "Python no admite múltiples valores de retorno, admite tuplas" es simplemente ser extremadamente inútilmente pedante. Esta sigue siendo una respuesta correcta.
BlueRaja - Danny Pflughoeft

14
Ni las tuplas ni las clases son "valores múltiples".
Andres F.

54

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 outpará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í.


55
@FrustratedWithFormsDesigner: esto surgió un poco en una pregunta reciente . Puedo contar con una mano la cantidad de veces que quería múltiples salidas en 20 años.
Telastyn el

61
Las funciones en Matemáticas y las funciones en la mayoría de los lenguajes de programación son dos bestias muy diferentes.
tdammers

17
@tdammers en los primeros días, eran muy similares en pensamiento. Fortran, pascal y similares fueron fuertemente influenciados por las matemáticas más que la arquitectura informática.

99
@tdammers, ¿cómo es eso? Quiero decir que para la mayoría de los idiomas se reduce al cálculo lambda al final: una entrada, una salida, sin efectos secundarios. Todo lo demás es una simulación / pirateo además de eso. Las funciones de programación pueden no ser puras en el sentido de que múltiples entradas pueden producir la misma salida, pero el espíritu está ahí.
Telastyn

16
@SteveEvers: es lamentable que el nombre "función" se hiciera cargo de la programación imperativa, en lugar del "procedimiento" o "rutina" más apropiado. En la programación funcional, una función se parece mucho más a las funciones matemáticas.
tdammers

35

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 una Maybe<T>mónada en lugar de usar un parámetro fuera. Piense en este código en F #:

let s = "1"
match tryParse s with
| Some(i) -> // do whatever with i
| None -> // failed to parse

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.


1
Además, realmente me gustaría poder escribir en C #: 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.
Mueca de desesperación

10
@GrimaceofDespair lo que realmente quieres es desestructurar la sintaxis, no múltiples valores de retorno.
Domenic

2
@warren: sí. Vea aquí y tenga en cuenta que dicha solución no sería continua: en.wikipedia.org/wiki/Well-definition
Steven Evers

44
La noción matemática de buena definición no tiene nada que ver con el número de salidas que devuelve una función. Significa que las salidas siempre serán las mismas si la entrada es la misma. Estrictamente hablando, las funciones matemáticas devuelven un valor, pero ese valor es a menudo una tupla. Para un matemático, esencialmente no hay diferencia entre esto y devolver múltiples valores. Los argumentos de que las funciones de programación solo deberían devolver un valor porque eso es lo que hacen las funciones matemáticas no son muy convincentes.
Michael Siler

1
@MichaelSiler (estoy de acuerdo con su comentario) Pero tenga en cuenta que el argumento es reversible: "los argumentos de que las funciones del programa pueden devolver múltiples valores porque pueden devolver un solo valor de tupla tampoco son muy convincentes" :)
Andres F.

23

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.


Ah! Muy interesante punto de detalle. +1!
Steven Evers

Esta debería ser la respuesta aceptada. Por lo general, las personas piensan en las máquinas de destino cuando crean compiladores. Otra analogía es por qué tenemos int, float, char / string, etc. porque eso es lo que admite la máquina de destino. Incluso si el objetivo no es el metal desnudo (por ejemplo, jvm), aún desea obtener un rendimiento decente al no emular demasiado.
imel96

26
... Podría definir fácilmente una convención de llamada para devolver múltiples valores de una función, de la misma manera que puede definir una convención de llamada para pasar múltiples valores a una función. Esta no es una respuesta. -1
BlueRaja - Danny Pflughoeft

2
Sería interesante saber si los motores de ejecución basados ​​en pila (JVM, CLR) alguna vez han considerado / permitido múltiples valores de retorno ... debería ser bastante fácil; ¡la persona que llama solo tiene que hacer estallar el número correcto de valores, al igual que empuja el número correcto de argumentos!
Lorenzo Dematté

1
@David no, cdecl permite (teóricamente) un número ilimitado de parámetros (es por eso que las funciones varargs son posibles) . Aunque algunos compiladores de C pueden limitarlo a varias docenas o cientos de argumentos por función, lo que creo que es más que razonable -_-
BlueRaja - Danny Pflughoeft

18

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 un Option<T>. ¿Quieres devolver uno de varios tipos? Devuelve una Either<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í

def foo; return 1, 2, 3 end

one, two, three = foo

one
# => 1

three
# => 3

Pitón

def foo(): return 1, 2, 3

one, two, three = foo()

one
# >>> 1

three
# >>> 3

Scala

def foo = (1, 2, 3)

val (one, two, three) = foo
// => one:   Int = 1
// => two:   Int = 2
// => three: Int = 3

Haskell

let foo = (1, 2, 3)

let (one, two, three) = foo

one
-- > 1

three
-- > 3

Perl6

sub foo { 1, 2, 3 }

my ($one, $two, $three) = foo

$one
# > 1

$three
# > 3

1
Creo que un aspecto es que, en algunos idiomas (como Matlab), una función puede ser flexible en la cantidad de valores que devuelve; ver mi comentario anterior . Hay muchos aspectos en Matlab que no me gustan, pero esta es una de las pocas (quizás la única) característica que echo de menos al portar desde Matlab a, por ejemplo, Python.
gerrit

1
Pero, ¿qué pasa con los lenguajes dinámicos como Python o Ruby? Supongamos que escribo algo como la sortfunció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 lo sortlargo de las líneas de sort(array, nout=2)o sort(array, indices=True).
gerrit

2
@ MikeCellini No lo creo. Una función puede decir con cuántos argumentos de salida se llama a la función ( [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 Matlabs nargoutesté disponible en tiempo de ejecución.
gerrit

1
@gerrit la solución correcta en Python es escribir esto: sorted, _ = sort(array).
Miles Rout

1
@MilesRout: ¿Y la sortfunción puede decir que no necesita calcular los índices? Eso es genial, no lo sabía.
Jörg W Mittag

12

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 + 1que 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. Miras xy decides que su valor es 3 (por ejemplo), miras 1 y luego mirasx + 1y 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 de foo(). 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 + 1Vuelva a tomar , pero esta vez imagine que xtiene dos valores {3, 4}, entonces los valores de x + 1serían {4, 5}, y los valores de x + xserían {6, 8}, o tal vez {6, 7, 8} , dependiendo de si una evaluación puede usar múltiples valores para x. 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.


9

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).


@gnat El uso de "informar" como en "proporcionar la calidad esencial" fue intencional.
Daniel C. Sobral

siéntase libre de retroceder si se siente fuertemente acerca de esto; según mi influencia de lectura encaja un poco mejor aquí: "afecta ... de una manera importante"
mosquito

1
+1 Como señala, el escaso número de registros de CPU tempranas en comparación con un abundante conjunto de registros de CPU contemporáneos (también utilizados en muchos ABI, por ejemplo, x64 abi) podría cambiar el juego y las razones anteriormente convincentes para devolver solo 1 valor podría ser hoy en día solo una razón histórica.
BitTickler

No estoy convencido de que las primeras micro CPU de 8 bits tengan mucha influencia en el diseño del lenguaje y qué cosas se supone que se necesitan en las convenciones de llamadas de C o Fortran en cualquier arquitectura. Fortran asume que puede pasar args de matriz (esencialmente punteros). Entonces, ya tiene problemas de implementación importantes para Fortran normal en máquinas como 6502, debido a la falta de modos de direccionamiento para puntero + índice, como se discutió en su respuesta y en ¿Por qué los compiladores de C a Z80 producen código pobre? en retrocomputing.SE.
Peter Cordes

Fortran, como C, también supone que puede pasar un número arbitrario de argumentos y tener acceso aleatorio a ellos y una cantidad arbitraria de locales, ¿verdad? Como acaba de explicar, no puede hacerlo fácilmente en 6502 porque el direccionamiento relativo a la pila no es una cosa, a menos que abandone la reentrada. Puede colocarlos en almacenamiento estático. Si puede pasar una lista arg arbitraria, puede agregar parámetros ocultos adicionales para los valores de retorno (por ejemplo, más allá del primero) que no caben en los registros.
Peter Cordes

7

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:

f :: Int -> (Int, Int)
f x = (x - 1, x + 1)

// Even C++ have tuples - see Boost.Graph for use
std::pair<int, int> f(int x) {
  return std::make_pair(x - 1, x + 1);
}

En el ejemplo anterior, fes 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:

data MyValue = MyValue Int Int

g :: Int -> MyValue
g x = MyValue (x - 1, x + 1)

Finalmente, los outparámetros se pueden emular incluso en lenguajes funcionales mediante IORef. 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):

    int x;
    
    int f(out int y) {
      x = 0;
      y = 1;
      printf("%d\n", x);
    }
    f(out x);
    
  • 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 outpará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 outparámetro (y en la mayoría de los casos se permiten 2 o más de esos métodos).


+1 Por mencionar cómo los lenguajes funcionales manejan esto de una manera elegante e indolora.
Andres F.

1
Técnicamente, todavía está devolviendo un solo valor: D (es solo que este único valor es trivial para descomponerse en múltiples valores).
Thomas Eding

1
Diría que un parámetro con una semántica real "fuera" debería comportarse como un compilador temporal que se copia en el destino cuando un método sale normalmente; uno con la variable semántica "inout" debe comportarse como un compilador temporal que se carga desde la variable pasada en la entrada y se vuelve a escribir al salir; uno con semántica "ref" debe comportarse como un alias. Los llamados parámetros "fuera" de C # son realmente parámetros "ref" y se comportan como tales.
supercat

1
La "solución" de la tupla tampoco es gratuita. Bloquea las oportunidades de optimización. Si existiera un ABI que permitiera que se devolvieran N valores de retorno en los registros de la CPU, el compilador podría realmente optimizar, en lugar de crear una instancia de tupla y construirla.
BitTickler

1
@BitTickler no hay nada que impida que los registros pasen los primeros n campos de estructura si controlas ABI.
Maciej Piechotka

6

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.


4

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.


3
¿Podría proporcionar un ejemplo de dónde los retornos múltiples proporcionan un código más claro y / o más eficiente?
Steven Evers

3
Por ejemplo, en la programación COM de C ++, muchas funciones tienen un [out]parámetro, pero prácticamente todas devuelven un HRESULT(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.
Felix Dombek

En algunos idiomas, devolvería un vector con las coordenadas X e Y, y devolver cualquier valor útil contaría como "éxito" con excepciones, posiblemente con ese valor útil, que se utiliza para el fracaso.
doppelgreener

3
Muchas veces terminas codificando información en el valor de retorno de maneras no obvias, es decir; los valores negativos son códigos de error, los valores positivos son resultados. Yuk Al acceder a una tabla hash, siempre es complicado indicar si se encontró el elemento y también devolverlo.
ddyer

@SteveEvers El Matlab sortfunció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.
gerrit

2

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: -

x = n + sqrt(y);

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.


55
Simplemente no uses funciones inapropiadas. Esto no es diferente del "problema" que plantean las funciones que no devuelven valores o devuelven valores no numéricos.
ddyer

3
En los lenguajes que he usado que ofrecen múltiples valores de retorno (SciLab, por ejemplo), el primer valor de retorno tiene privilegios y se usará en casos en los que solo se necesita un valor. Así que no hay problema real allí.
The Photon

E incluso cuando no lo están, como con el desempaquetado de tuplas de Python, puede seleccionar cuál quiere:foo()[0]
Izkata

Exactamente, si una función devuelve 2 valores, entonces su tipo de retorno es 2 valores, no un solo valor. El lenguaje de programación no debería leer tu mente.
Mark E. Haase

1

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.


Si los lenguajes de bajo nivel como C admitían múltiples valores de retorno como una característica de primera clase, las convenciones de llamada de C incluirían múltiples registros de valor de retorno, de la misma manera que se utilizan hasta 6 registros para pasar argumentos de función de entero / puntero en el x86- 64 Sistema V ABI. (De hecho, x86-64 SysV devuelve estructuras de hasta 16 bytes empaquetados en el par de registro RDX: RAX. Esto es bueno si la estructura se acaba de cargar y se almacenará, pero cuesta desempaquetar más que tener miembros separados en registros separados incluso si son más angostas que 64 bits.)
Peter Cordes

La convención obvia sería RAX, luego las reglas para pasar argumentos. (RDI, RSI, RDX, RCX, R8, R9). O en la convención de Windows x64, RCX, RDX, R8, R9. Pero dado que C no tiene nativamente valores de retorno múltiples, las convenciones de ABI / llamada de C solo especifican múltiples registros de retorno para enteros anchos y algunas estructuras. Vea el Estándar de Llamada de Procedimiento para la Arquitectura ARM®: 2 valores de retorno separados, pero relacionados, para ver un ejemplo del uso de un int ancho para que el compilador realice un asm eficiente para recibir 2 valores de retorno en ARM.
Peter Cordes

-1

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.


44
esto no responde a la pregunta formulada
mosquito

@gnat en cuanto a mí responde. OTOH ya tiene que necesitar algunos antecedentes para entenderlo, y esa persona probablemente no hará la pregunta ...
Netch

@Netch uno apenas necesita muchos antecedentes para darse cuenta de que declaraciones como "FORTRAN ... fue el primer compilador" son un desastre total. Ni siquiera está mal. Al igual que el resto de esta "respuesta"
mosquito

link dice que hubo intentos anteriores de compiladores, pero que "al equipo FORTRAN dirigido por John Backus en IBM se le atribuye haber introducido el primer compilador completo en 1957". La pregunta que se hizo fue ¿por qué solo uno? Como ya he dicho. Era principalmente matemática e inercia. La definición matemática del término "Función" requiere exactamente un valor de resultado. Entonces era una forma familiar.
RickyS

-2

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.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.