Ao contrário do que os outros estão dizendo, a sobrecarga por tipo de retorno é possível e é feita por alguns idiomas modernos. A objeção usual é que em código como
int func();
string func();
int main() { func(); }
você não pode dizer qual func()
está sendo chamado. Isso pode ser resolvido de algumas maneiras:
- Tenha um método previsível para determinar qual função é chamada nessa situação.
- Sempre que essa situação ocorre, é um erro em tempo de compilação. No entanto, tenha uma sintaxe que permita ao programador desambiguar, por exemplo
int main() { (string)func(); }
.
- Não tem efeitos colaterais. Se você não tiver efeitos colaterais e nunca usar o valor de retorno de uma função, o compilador poderá evitar chamar a função em primeiro lugar.
Dois dos idiomas que eu regularmente ( ab ) usam sobrecarga por tipo de retorno: Perl e Haskell . Deixe-me descrever o que eles fazem.
No Perl , existe uma distinção fundamental entre o contexto escalar e a lista (e outros, mas vamos fingir que existem dois). Toda função interna do Perl pode fazer coisas diferentes, dependendo do contexto em que é chamada. Por exemplo, o join
operador força o contexto da lista (na coisa que está sendo unida) enquanto o scalar
operador força o contexto escalar, então 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.
Todo operador no Perl faz algo no contexto escalar e algo no contexto da lista, e eles podem ser diferentes, como ilustrado. (Isso não é apenas para operadores aleatórios, como localtime
. Se você usa uma matriz @a
no contexto de lista, ela retorna a matriz, enquanto no contexto escalar, retorna o número de elementos. Por exemplo, print @a
imprime os elementos, enquanto print 0+@a
imprime o tamanho. ) Além disso, todo operador pode forçar um contexto, por exemplo, a adição +
força o contexto escalar. Toda entrada em man perlfunc
documentos isso. Por exemplo, aqui faz parte da entrada para glob EXPR
:
No contexto de lista, retorna uma lista (possivelmente vazia) de expansões de nome de arquivo com o valor EXPR
que o shell Unix padrão /bin/csh
faria. No contexto escalar, o glob percorre essas expansões de nome de arquivo, retornando undef quando a lista estiver esgotada.
Agora, qual é a relação entre lista e contexto escalar? Bem man perlfunc
diz
Lembre-se da seguinte regra importante: Não existe uma regra que relacione o comportamento de uma expressão no contexto de lista com seu comportamento no contexto escalar ou vice-versa. Pode fazer duas coisas totalmente diferentes. Cada operador e função decide que tipo de valor seria mais apropriado retornar no contexto escalar. Alguns operadores retornam o comprimento da lista que seria retornada no contexto da lista. Alguns operadores retornam o primeiro valor na lista. Alguns operadores retornam o último valor na lista. Alguns operadores retornam uma contagem de operações bem-sucedidas. Em geral, eles fazem o que você deseja, a menos que você queira consistência.
portanto, não é uma simples questão de ter uma única função, e você faz uma conversão simples no final. De fato, escolhi o localtime
exemplo por esse motivo.
Não são apenas os integrados que têm esse comportamento. Qualquer usuário pode definir essa função usando wantarray
, o que permite distinguir entre lista, escalar e contexto nulo. Assim, por exemplo, você pode decidir não fazer nada se estiver sendo chamado em um contexto nulo.
Agora, você pode reclamar que isso não é uma sobrecarga verdadeira por valor de retorno, porque você tem apenas uma função, a qual é informada no contexto em que é chamada e, em seguida, age sobre essas informações. No entanto, isso é claramente equivalente (e análogo a como o Perl não permite literalmente a sobrecarga usual, mas uma função pode apenas examinar seus argumentos). Além disso, resolve bem a situação ambígua mencionada no início desta resposta. Perl não reclama que não sabe qual método chamar; apenas chama isso. Tudo o que precisa fazer é descobrir em que contexto a função foi chamada, o que é sempre possível:
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: às vezes posso dizer operador Perl quando quero dizer função. Isso não é crucial para esta discussão.)
Haskell adota a outra abordagem, a saber, para não ter efeitos colaterais. Ele também possui um sistema de tipos robusto e, portanto, você pode escrever um código como o seguinte:
main = do n <- readLn
print (sqrt n) -- note that this is aligned below the n, if you care to run this
Esse código lê um número de ponto flutuante da entrada padrão e imprime sua raiz quadrada. Mas o que é surpreendente nisso? Bem, o tipo de readLn
é readLn :: Read a => IO a
. O que isso significa é que, para qualquer tipo que possa ser Read
(formalmente, todo tipo que seja uma instância da Read
classe type), readLn
pode lê-lo. Como Haskell sabia que eu queria ler um número de ponto flutuante? Bem, o tipo de sqrt
é sqrt :: Floating a => a -> a
, o que significa essencialmente que sqrt
só pode aceitar números de ponto flutuante como entradas e, portanto, Haskell inferiu o que eu queria.
O que acontece quando Haskell não pode inferir o que eu quero? Bem, existem algumas possibilidades. Se eu não usar o valor de retorno, Haskell simplesmente não chamará a função em primeiro lugar. No entanto, se eu fazer usar o valor de retorno, então Haskell vai reclamar que não pode inferir o tipo:
main = do n <- readLn
print n
-- this program results in a compile-time error "Unresolved top-level overloading"
Eu posso resolver a ambiguidade especificando o tipo que eu quero:
main = do n <- readLn
print (n::Int)
-- this compiles (and does what I want)
De qualquer forma, o que toda essa discussão significa é que a sobrecarga pelo valor de retorno é possível e está concluída, o que responde a parte da sua pergunta.
A outra parte da sua pergunta é por que mais idiomas não fazem isso. Vou deixar que outros respondam isso. No entanto, alguns comentários: a principal razão é provavelmente que a oportunidade de confusão é realmente maior aqui do que a sobrecarga por tipo de argumento. Você também pode examinar as justificativas de idiomas individuais:
Ada : "Pode parecer que a regra mais simples de resolução de sobrecarga é usar tudo - todas as informações de um contexto tão amplo quanto possível - para resolver a referência sobrecarregada. Essa regra pode ser simples, mas não é útil. Requer o leitor humano digitalizar pedaços de texto arbitrariamente grandes e fazer inferências arbitrariamente complexas (como (g) acima). Acreditamos que uma regra melhor é aquela que torna explícita a tarefa que um leitor ou compilador humano deve executar e que a torna o mais natural possível para o leitor humano ".
C ++ (subseção 7.4.1 da "Linguagem de programação C ++" de Bjarne Stroustrup): "Os tipos de retorno não são considerados na resolução de sobrecarga. O motivo é manter a resolução para um operador individual ou uma chamada de função independente do 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)
}
Se o tipo de retorno fosse levado em consideração, não seria mais possível examinar uma chamada sqrt()
isoladamente e determinar qual função foi chamada. "(Observe, para comparação, que em Haskell não há conversões implícitas ).
Java ( Java Language Specification 9.4.1 ): "Um dos métodos herdados deve ser substituível pelo tipo de retorno para todos os outros métodos herdados; caso contrário, ocorrerá um erro em tempo de compilação." (Sim, eu sei que isso não dá uma justificativa. Tenho certeza de que Gosling é dada na "linguagem de programação Java". Talvez alguém tenha uma cópia? Aposto que é o "princípio da menor surpresa" em essência. ) No entanto, um fato curioso sobre Java: a JVM permite sobrecarga por valor de retorno! Isso é usado, por exemplo, no Scala , e também pode ser acessado diretamente por Java , brincando com os internos.
PS. Como nota final, é realmente possível sobrecarregar pelo valor de retorno em C ++ com um truque. Testemunha:
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
}