Al contrario de lo que otros dicen, la sobrecarga por tipo de retorno es posible y se realiza en algunos idiomas modernos. La objeción habitual es que en código como
int func();
string func();
int main() { func(); }
No se puede saber cuál func()
se llama. Esto se puede resolver de varias maneras:
- Tenga un método predecible para determinar qué función se llama en tal situación.
- Cada vez que ocurre una situación así, es un error en tiempo de compilación. Sin embargo, tiene una sintaxis que permite al programador desambiguar, por ejemplo
int main() { (string)func(); }
.
- No tiene efectos secundarios. Si no tiene efectos secundarios y nunca usa el valor de retorno de una función, entonces el compilador puede evitar llamar a la función en primer lugar.
Dos de los idiomas que uso regularmente ( ab ) usan sobrecarga por tipo de retorno: Perl y Haskell . Déjame describir lo que hacen.
En Perl , hay una distinción fundamental entre el contexto escalar y de lista (y otros, pero pretendemos que hay dos). Cada función incorporada en Perl puede hacer cosas diferentes dependiendo del contexto en el que se llama. Por ejemplo, el join
operador fuerza el contexto de la lista (en la cosa que se está uniendo) mientras que el scalar
operador fuerza el contexto escalar, así que compare:
print join " ", localtime(); # printed "58 11 2 14 0 109 3 13 0" for me right now
print scalar localtime(); # printed "Wed Jan 14 02:12:44 2009" for me right now.
Cada operador en Perl hace algo en contexto escalar y algo en contexto de lista, y pueden ser diferentes, como se ilustra. (Esto no es solo para operadores aleatorios como localtime
. Si usa una matriz @a
en el contexto de la lista, devuelve la matriz, mientras que en el contexto escalar, devuelve el número de elementos. Entonces, por ejemplo, print @a
imprime los elementos, mientras print 0+@a
imprime el tamaño. ) Además, cada operador puede forzar un contexto, por ejemplo, la suma +
fuerza el contexto escalar. Cada entrada en los man perlfunc
documentos esto. Por ejemplo, aquí hay parte de la entrada para glob EXPR
:
En el contexto de la lista, devuelve una lista (posiblemente vacía) de expansiones de nombre de archivo en el valor de lo que haría el EXPR
shell estándar de Unix /bin/csh
. En contexto escalar, glob itera a través de tales expansiones de nombre de archivo, devolviendo undef cuando la lista se agota.
Ahora, ¿cuál es la relación entre la lista y el contexto escalar? Bueno man perlfunc
dice
Recuerde la siguiente regla importante: no existe una regla que relacione el comportamiento de una expresión en el contexto de la lista con su comportamiento en el contexto escalar, o viceversa. Podría hacer dos cosas totalmente diferentes. Cada operador y función decide qué tipo de valor sería más apropiado devolver en contexto escalar. Algunos operadores devuelven la longitud de la lista que habría sido devuelta en el contexto de la lista. Algunos operadores devuelven el primer valor en la lista. Algunos operadores devuelven el último valor en la lista. Algunos operadores devuelven un recuento de operaciones exitosas. En general, hacen lo que quieres, a menos que quieras consistencia.
así que no se trata simplemente de tener una sola función, y luego se realiza una conversión simple al final. De hecho, elegí el localtime
ejemplo por ese motivo.
No son solo los elementos integrados los que tienen este comportamiento. Cualquier usuario puede definir dicha función utilizando wantarray
, lo que le permite distinguir entre lista, escalar y contexto vacío. Entonces, por ejemplo, puedes decidir no hacer nada si te llaman en un contexto vacío.
Ahora, puede quejarse de que esto no es una sobrecarga verdadera por valor de retorno porque solo tiene una función, que se le dice al contexto en el que se invoca y luego actúa sobre esa información. Sin embargo, esto es claramente equivalente (y análogo a cómo Perl no permite la sobrecarga habitual literalmente, pero una función solo puede examinar sus argumentos). Además, resuelve muy bien la ambigua situación mencionada al comienzo de esta respuesta. Perl no se queja de que no sabe a qué método llamar; solo lo llama. Todo lo que tiene que hacer es averiguar en qué contexto se llamó a la función, que siempre es posible:
sub func {
if( not defined wantarray ) {
print "void\n";
} elsif( wantarray ) {
print "list\n";
} else {
print "scalar\n";
}
}
func(); # prints "void"
() = func(); # prints "list"
0+func(); # prints "scalar"
(Nota: a veces puedo decir operador Perl cuando me refiero a la función. Esto no es crucial para esta discusión).
Haskell adopta el otro enfoque, es decir, no tener efectos secundarios. También tiene un sistema de tipo fuerte, por lo que puede escribir código como el siguiente:
main = do n <- readLn
print (sqrt n) -- note that this is aligned below the n, if you care to run this
Este código lee un número de coma flotante de la entrada estándar e imprime su raíz cuadrada. Pero, ¿qué es lo sorprendente de esto? Bueno, el tipo de readLn
es readLn :: Read a => IO a
. Lo que esto significa es que para cualquier tipo que pueda ser Read
(formalmente, cada tipo que sea una instancia de la Read
clase de tipo), readLn
puede leerlo. ¿Cómo sabía Haskell que quería leer un número de coma flotante? Bueno, el tipo de sqrt
es sqrt :: Floating a => a -> a
, lo que esencialmente significa que sqrt
solo puede aceptar números de punto flotante como entradas, por lo que Haskell dedujo lo que quería.
¿Qué sucede cuando Haskell no puede inferir lo que quiero? Bueno, hay algunas posibilidades. Si no uso el valor de retorno, Haskell simplemente no llamará a la función en primer lugar. Sin embargo, si yo hago utilizar el valor de retorno, a continuación, Haskell se quejará de que no se puede inferir el tipo:
main = do n <- readLn
print n
-- this program results in a compile-time error "Unresolved top-level overloading"
Puedo resolver la ambigüedad especificando el tipo que quiero:
main = do n <- readLn
print (n::Int)
-- this compiles (and does what I want)
De todos modos, lo que significa toda esta discusión es que la sobrecarga por valor de retorno es posible y se hace, lo que responde parte de su pregunta.
La otra parte de su pregunta es por qué más idiomas no lo hacen. Dejaré que otros respondan eso. Sin embargo, algunos comentarios: la razón principal es probablemente que la oportunidad de confusión es realmente mayor aquí que en la sobrecarga por tipo de argumento. También puede ver los fundamentos de idiomas individuales:
Ada : "Puede parecer que la regla de resolución de sobrecarga más simple es usar todo, toda la información de un contexto lo más amplio posible, para resolver la referencia sobrecargada. Esta regla puede ser simple, pero no es útil. Requiere el lector humano para escanear fragmentos de texto arbitrariamente grandes y para hacer inferencias arbitrariamente complejas (como (g) arriba). Creemos que una mejor regla es aquella que hace explícita la tarea que debe realizar un lector humano o un compilador, y eso hace que esta tarea tan natural para el lector humano como sea posible ".
C ++ (subsección 7.4.1 de "El lenguaje de programación C ++" de Bjarne Stroustrup): "Los tipos de retorno no se consideran en la resolución de sobrecarga. El motivo es mantener la resolución de un operador individual o llamada de función independiente del contexto. Considere:
float sqrt(float);
double sqrt(double);
void f(double da, float fla)
{
float fl = sqrt(da); // call sqrt(double)
double d = sqrt(da); // call sqrt(double)
fl = sqrt(fla); // call sqrt(float)
d = sqrt(fla); // call sqrt(float)
}
Si se tuviera en cuenta el tipo de retorno, ya no sería posible mirar una llamada de forma sqrt()
aislada y determinar qué función se llamó ". (Tenga en cuenta, para comparar, que en Haskell no hay conversiones implícitas ).
Java ( Java Language Specification 9.4.1 ): "Uno de los métodos heredados debe ser sustituible por el tipo de retorno para cualquier otro método heredado, de lo contrario se produce un error en tiempo de compilación". (Sí, sé que esto no da una razón. Estoy seguro de que la razón es dada por Gosling en "el lenguaje de programación Java". ¿Quizás alguien tiene una copia? Apuesto a que es el "principio de la menor sorpresa" en esencia. ) Sin embargo, un dato curioso sobre Java: ¡la JVM permite la sobrecarga por valor de retorno! Esto se usa, por ejemplo, en Scala , y también se puede acceder directamente a través de Java jugando con elementos internos.
PD. Como nota final, en realidad es posible sobrecargar por valor de retorno en C ++ con un truco. Testigo:
struct func {
operator string() { return "1";}
operator int() { return 2; }
};
int main( ) {
int x = func(); // calls int version
string y = func(); // calls string version
double d = func(); // calls int version
cout << func() << endl; // calls int version
func(); // calls neither
}