Por que a sobrecarga com tipos de retorno não é permitida? (pelo menos nos idiomas mais usados)

9

Eu não conheço todas as linguagens de programação, mas é claro que geralmente a possibilidade de sobrecarregar um método levando em consideração seu tipo de retorno (supondo que seus argumentos sejam o mesmo número e tipo) não é suportada.

Quero dizer algo como isto:

 int method1 (int num)
 {

 }
 long method1 (int num)
 {

 }

Não é que seja um grande problema de programação, mas em algumas ocasiões eu gostaria de recebê-lo.

Obviamente, não haveria como essas linguagens suportarem isso, sem uma maneira de diferenciar qual método está sendo chamado, mas a sintaxe para isso pode ser tão simples quanto algo como [int] method1 (num) ou [long] method1 (num) dessa forma, o compilador saberia qual seria o que seria chamado.

Não sei como os compiladores funcionam, mas isso não parece tão difícil de fazer, então me pergunto por que algo assim geralmente não é implementado.

Quais são as razões pelas quais algo assim não é suportado?

user2638180
fonte
Talvez sua pergunta seja melhor com um exemplo em que não existem conversões implícitas entre os dois tipos de retorno - por exemplo, classes Fooe Bar.
Bem, por que esse recurso seria útil?
21716 James Youngman
3
@ JamesYoungman: Por exemplo, para analisar seqüências de caracteres em diferentes tipos, você pode ter um método int read (String s), float read (String s) e assim por diante. Cada variação sobrecarregada do método executa uma análise para o tipo apropriado.
Giorgio
A propósito, esse é apenas um problema das linguagens estaticamente tipadas. Ter vários tipos de retorno é bastante rotineiro em linguagens de tipo dinâmico, como Javascript ou Python.
precisa saber é o seguinte
11
@StevenBurnap, hum, não. Por exemplo, com JavaScript, você não pode sobrecarregar a função. Portanto, esse é apenas um problema nos idiomas que suportam a sobrecarga do nome da função.
David Arno

Respostas:

19

Isso complica a verificação de tipos.

Quando você só permite sobrecarga com base nos tipos de argumento e deduz apenas tipos de variáveis ​​de seus inicializadores, todas as suas informações de tipo fluem em uma direção: na árvore de sintaxe.

var x = f();

given      f   : () -> int  [upward]
given      ()  : ()         [upward]
therefore  f() : int        [upward]
therefore  x   : int        [upward]

Quando você permite que as informações de tipo viajem em ambas as direções, como deduzir o tipo de uma variável de seu uso, você precisa de um solucionador de restrições (como o Algoritmo W, para sistemas do tipo Hindley-Milner) para determinar o tipo.

var x = parse("123");
print_int(x);

given      parse        : string -> T  [upward]
given      "123"        : string       [upward]
therefore  parse("123") : ∃T           [upward]
therefore  x            : ∃T           [upward]
given      print_int    : int -> ()    [upward]
therefore  print_int(x) : ()           [upward]
therefore  int -> ()    = ∃T -> ()     [downward]
therefore  ∃T           = int          [downward]
therefore  x            : int          [downward]

Aqui, precisamos deixar o tipo xcomo uma variável de tipo não resolvida ∃T, onde tudo o que sabemos sobre isso é que é analisável. Somente mais tarde, quando xusado em um tipo concreto, temos informações suficientes para resolver a restrição e determinar isso ∃T = int, que propaga as informações de tipo na árvore da sintaxe da expressão de chamada para x.

Se não formos capazes de determinar o tipo de x, esse código também será sobrecarregado (para que o responsável pela chamada determine o tipo) ou teremos que reportar um erro sobre a ambiguidade.

A partir disso, um designer de linguagem pode concluir:

  • Acrescenta complexidade à implementação.

  • Isso torna a digitação mais lenta - em casos patológicos, exponencialmente.

  • É mais difícil produzir boas mensagens de erro.

  • É muito diferente do status quo.

  • Não tenho vontade de implementá-lo.

Jon Purdy
fonte
11
Além disso: será tão difícil entender que, em alguns casos, seu compilador fará escolhas que o programador absolutamente não esperava, levando a erros difíceis de encontrar.
precisa saber é o seguinte
11
@ gnasher729: Eu discordo. Uso regularmente o Haskell, que possui esse recurso, e nunca fui mordido por sua opção de sobrecarga (por exemplo, instância de classe de tipo). Se algo é ambíguo, isso me força a adicionar uma anotação de tipo. E ainda escolhi implementar a inferência de tipo completa no meu idioma, porque é incrivelmente útil. Esta resposta foi eu interpretando o advogado do diabo.
31416 Jon Purdy
4

Porque é ambíguo. Usando C # como um exemplo:

var foo = method(42);

Qual sobrecarga devemos usar?

Ok, talvez isso tenha sido um pouco injusto. O compilador não pode descobrir que tipo usar se não o contarmos na sua linguagem hipotética. Portanto, a digitação implícita é impossível no seu idioma e existem métodos anônimos e o Linq junto com ele ...

Que tal este? (Assinaturas ligeiramente redefinidas para ilustrar o ponto.)

short method(int num) { ... }
int method(int num) { ... }

....

long foo = method(42);

Devemos usar a intsobrecarga ou a shortsobrecarga? Simplesmente não sabemos, teremos que especificá-lo com sua [int] method1(num)sintaxe. O que é um pouco trabalhoso analisar e escrever para ser honesto.

long foo = [int] method(42);

O fato é que é uma sintaxe surpreendentemente semelhante a um método genérico em C #.

long foo = method<int>(42);

(C ++ e Java têm recursos semelhantes.)

Em resumo, os designers de idiomas optaram por resolver o problema de uma maneira diferente, a fim de simplificar a análise e ativar recursos de linguagem muito mais poderosos.

Você diz que não sabe muito sobre compiladores. Eu recomendo aprender sobre gramáticas e analisadores. Depois de entender o que é uma gramática livre de contexto, você terá uma idéia muito melhor sobre por que a ambiguidade é uma coisa ruim.

Pato de borracha
fonte
Bom argumento sobre os genéricos, embora você pareça estar conflitante shorte int.
31816 Robert Harvey
Sim, RobertHarvey, você está certo. Eu estava lutando por um exemplo para ilustrar o ponto. Funcionaria melhor se methodretornasse um short ou int e o tipo fosse definido como um long.
precisa
Isso parece um pouco melhor.
precisa
Eu não compro seus argumentos de que você não pode ter inferência de tipo, sub-rotinas anônimas ou compreensão de mônada em um idioma com sobrecarga de tipo de retorno. Haskell faz isso, e Haskell tem todos os três. Também possui polimorfismo paramétrico. Seu ponto de vista sobre long/ int/ shorté mais sobre as complexidades de subtipagem e / ou conversões implícitas do que sobre a sobrecarga do tipo de retorno. Afinal, literais numéricos estão sobrecarregados em seu tipo de retorno em C ++, Java, C♯ e muitos outros, e isso não parece apresentar um problema. Você pode simplesmente criar uma regra: por exemplo, escolha o tipo mais específico / geral.
Jörg W Mittag
@ JörgWMittag meu argumento não era que isso tornaria impossível, apenas que tornaria as coisas desnecessariamente complexas.
precisa
0

Todos os recursos de idioma adicionam complexidade, portanto, eles precisam fornecer benefícios suficientes para justificar as inevitáveis ​​dicas, casos de canto e confusão do usuário que todo recurso cria. Para a maioria dos idiomas, este simplesmente não fornece benefícios suficientes para justificá-lo.

Na maioria dos idiomas, você esperaria que a expressão method1(2)tivesse um tipo definido e um valor de retorno mais ou menos previsível. Mas se você permitir sobrecarregar os valores de retorno, isso significa que é impossível dizer o que essa expressão significa em geral, sem considerar o contexto ao seu redor. Considere o que acontece quando você tem um unsigned long long foo()método cuja implementação termina return method1(2)? Isso deve chamar a longsobrecarga de retorno ou a intsobrecarga de retorno ou simplesmente fornecer um erro do compilador?

Além disso, se você precisar ajudar o compilador anotando o tipo de retorno, não apenas estará inventando mais sintaxe (o que aumenta todos os custos acima mencionados de permitir que o recurso exista), mas também fazendo efetivamente o mesmo que criando dois métodos de nomes diferentes em uma linguagem "normal". É [long] method1(2)mais intuitivo do que long_method1(2)?


Por outro lado, algumas linguagens funcionais como Haskell, com sistemas de tipos estáticos muito fortes, permitem esse tipo de comportamento, porque sua inferência de tipo é poderosa o suficiente para que você raramente precise anotar o tipo de retorno nesses idiomas. Mas isso só é possível porque esses idiomas realmente reforçam a segurança de tipos, mais do que qualquer idioma tradicional, além de exigir que todas as funções sejam puras e transparente em termos de referência. Não é algo que jamais seria viável na maioria dos idiomas OOP.

Ixrec
fonte
2
"além de exigir que todas as funções sejam puras e referencialmente transparentes": como isso facilita a sobrecarga do tipo de retorno?
Giorgio
@Giorgio Não - O Rust não impõe a pureza da função e ainda pode sobrecarregar o tipo de retorno (embora a sobrecarga no Rust seja muito diferente de outros idiomas (você só pode sobrecarregar usando modelos))
Idan Arye
A parte [long] e [int] deveria ter uma maneira de chamar explicitamente o método, na maioria dos casos, como ele deveria ser chamado poderia ser diretamente deduzido do tipo de variável à qual a execução do método é atribuída.
usar o seguinte comando
0

Ele é disponível em Swift e funciona muito bem lá. Obviamente, você não pode ter um tipo ambíguo em ambos os lados, por isso deve ser conhecido à esquerda.

Eu usei isso em uma API simples de codificação / decodificação .

public protocol HierDecoder {
  func dech() throws -> String
  func dech() throws -> Int
  func dech() throws -> Bool

Isso significa que as chamadas onde os tipos de parâmetros são conhecidos, como o initde um objeto, funcionam de maneira muito simples:

    private static let typeCode = "ds"
    static func registerFactory() {
        HierCodableFactories.Register(key:typeCode) {
            (from) -> HierCodable in
            return try tgDrawStyle(strokeColor:from.dech(), fillColor:from.dechOpt(), lineWidth:from.dech(), glowWidth: from.dech())
        }
    }
    func typeKey() -> String { return tgDrawStyle.typeCode }
    func encode(to:HierEncoder) {
        to.ench(strokeColor)
        to.enchOpt(fillColor)
        to.ench(lineWidth)
        to.ench(glowWidth)
    }

Se você prestar muita atenção, notará a dechOptligação acima. Descobri da maneira mais difícil que sobrecarregar o mesmo nome de função em que o diferenciador estava retornando um opcional era muito propenso a erros, pois o contexto de chamada poderia introduzir uma expectativa de que era opcional.

Andy Dent
fonte
-5
int main() {
    auto var1 = method1(1);
}
DeadMG
fonte
Nesse caso, o compilador pode: a) rejeitar a chamada porque é ambíguo; b) selecionar a primeira / última; c) deixar o tipo de var1e continuar com a inferência de tipo e assim que alguma outra expressão determinar o tipo de var1uso usado para selecionar implementação correta. No final das contas, mostrar um caso em que a inferência de tipo não é trivial raramente prova um ponto diferente dessa inferência de tipo geralmente não é trivial.
back2dos
11
Não é um argumento forte. A ferrugem, por exemplo, faz uso pesado de inferência de tipo e, em alguns casos, especialmente com genéricos, não pode dizer de que tipo você precisa. Nesses casos, você simplesmente precisa fornecer explicitamente o tipo, em vez de confiar na inferência de tipo.
precisa saber é o seguinte
11
Isso não demonstra um tipo de retorno sobrecarregado. method1deve ser declarado para retornar um tipo específico.
Gort the Robot