A sobrecarga de método é algo além de açúcar sintático? [fechadas]


19

O método sobrecarrega um tipo de polimorfismo? Para mim, parece simplesmente a diferenciação de métodos com o mesmo nome e parâmetros diferentes. Portanto, stuff(Thing t)e stuff(Thing t, int n)são métodos totalmente diferentes no que diz respeito ao compilador e ao tempo de execução.

Cria a ilusão, do lado do interlocutor, de que é o mesmo método que age de maneira diferente em diferentes tipos de objetos - polimorfismo. Mas isso é apenas uma ilusão, porque na verdade stuff(Thing t)e stuff(Thing t, int n)são métodos completamente diferentes.

A sobrecarga de método é algo além de açúcar sintático? Estou esquecendo de algo?


Uma definição comum para o açúcar sintático é que é puramente local . Significar alterar um pedaço de código para seu equivalente "adocicado", ou vice-versa, envolve mudanças locais que não afetam a estrutura geral do programa. E acho que a sobrecarga de método se encaixa exatamente nesse critério. Vejamos um exemplo para demonstrar:

Considere uma classe:

class Reader {
    public String read(Book b){
        // .. translate the book to text
    }
    public String read(File b){
        // .. translate the file to text
    }
}

Agora considere outra classe que usa essa classe:

/* might not be the best example */
class FileProcessor {
    Reader reader = new Reader();
    public void process(File file){
        String text = reader.read(file);
        // .. do stuff with the text
    }
}

OK. Agora vamos ver o que precisa ser alterado se substituirmos a sobrecarga do método por métodos regulares:

Os readmétodos em Readermudança para readBook(Book)e readFile(file). Apenas uma questão de mudar seus nomes.

O código de chamada FileProcessormuda um pouco: reader.read(file)muda para reader.readFile(file).

E é isso.

Como você pode ver, a diferença entre usar sobrecarga de método e não usá-lo é puramente local . E é por isso que acho que se qualifica como puro açúcar sintático.

Gostaria de ouvir suas objeções, se você tiver alguma, talvez esteja perdendo alguma coisa.


48
No final, qualquer recurso de linguagem de programação é apenas um açúcar sintático para o assembler bruto.
Philipp

31
@ Philipp: Desculpe, mas essa é uma afirmação realmente estúpida. As linguagens de programação derivam sua utilidade da semântica, não da sintaxe. Recursos como um sistema de tipos oferecem garantias reais, mesmo que possam exigir que você escreva mais .
Aug2

3
Pergunte a si mesmo: A sobrecarga do operador é apenas açúcar sintático? Seja qual for a resposta a essa pergunta você segurar verdade também é a resposta para a pergunta que você fez;)
back2dos

5
@ back2dos: concordo totalmente com você. Li a frase "tudo é apenas açúcar sintático para montador" com muita frequência e está claramente errado. O açúcar sintático é uma sintaxe alternativa (possivelmente melhor) para alguma sintaxe existente que não adiciona nenhuma nova semântica.
Giorgio

6
@Giorgio: certo! Há uma definição precisa no artigo de referência de Matthias Felleisen sobre expressividade. Basicamente: o açúcar sintático é puramente local. Se você precisar alterar a estrutura global do programa para remover o uso do recurso de idioma, não será um açúcar sintático. Ou seja, reescrever o código OO polimórfico no assembler normalmente envolve a adição de lógica de despacho global, que não é puramente local, portanto o OO não é "apenas açúcar sintático para o assembler".
Jörg W Mittag

Respostas:


29

Para responder a isso, primeiro você precisa de uma definição para "açúcar sintático". Eu vou com a Wikipedia :

Na ciência da computação, o açúcar sintático é uma sintaxe dentro de uma linguagem de programação projetada para facilitar a leitura ou a expressão. Torna a linguagem "mais doce" para uso humano: as coisas podem ser expressas de forma mais clara, concisa ou em um estilo alternativo que alguns possam preferir.

[...]

Especificamente, uma construção em um idioma é chamada de açúcar sintático, se puder ser removida do idioma sem afetar o que o idioma pode fazer

Portanto, sob essa definição, recursos como os varargs de Java ou a compreensão de Scala são açúcar sintático: eles se traduzem em recursos de linguagem subjacentes (uma matriz no primeiro caso, chamadas para mapear / flatmap / filtro no segundo) e removê-los Não mude as coisas que você pode fazer com o idioma.

A sobrecarga de método, no entanto, não é um açúcar sintático nesta definição, porque removê-la mudaria fundamentalmente o idioma (você não seria mais capaz de enviar para um comportamento distinto com base em argumentos).

É verdade que você pode simular a sobrecarga do método, desde que tenha alguma maneira de acessar os argumentos de um método, e pode usar uma construção "se" com base nos argumentos fornecidos. Mas se você considerar esse açúcar sintático, teria que considerar qualquer coisa acima de uma máquina de Turing para ser igualmente açúcar sintático.


22
Remover a sobrecarga não mudaria o que o idioma pode fazer. Você ainda pode fazer exatamente as mesmas coisas de antes; você apenas teria que renomear alguns métodos. Essa é uma mudança mais trivial do que desamarrar loops.
Doval

9
Como eu disse, você pode adotar a abordagem de que todas as linguagens (incluindo linguagens de máquina) são simplesmente açúcar sintático em cima de uma máquina de Turing.
Kdgregory

9
A meu ver, a sobrecarga de método simplesmente permite que você faça sum(numbersArray)e em sum(numbersList)vez de sumArray(numbersArray)e sumList(numbersList). Eu concordo com Doval, parece mero açúcar sintático.
Aviv Cohn

3
A maior parte do idioma. Tente implementar instanceof, classes, herança, interfaces genéricos, reflexão ou especificadores de acesso que utilizam if, whilee operadores booleanos, com exatamente a mesma semântica . Não há esquinas. Observe que não estou desafiando você a calcular as mesmas coisas que os usos específicos dessas construções. Eu já sei que você pode calcular qualquer coisa usando lógica booleana e ramificação / loop. Eu estou pedindo para você implementar cópias perfeitas dos semântica desses recursos de linguagem, incluindo quaisquer garantias estáticos que eles fornecem (cheques tempo de compilação ainda deve ser feito em tempo de compilação.)
Doval

6
@Doval, kdgregory: Para definir o açúcar sintático, você deve defini-lo em relação a algumas semânticas. Se a única semântica que você possui é "O que esse programa calcula?", Fica claro que tudo é apenas açúcar sintático para uma máquina de Turing. Por outro lado, se você possui uma semântica na qual pode falar sobre objetos e certas operações, a remoção de certa sintaxe não permitirá que você expresse mais essas operações, mesmo que o idioma ainda possa estar completo em Turing.
Giorgio

13

O termo açúcar sintático geralmente se refere aos casos em que o recurso é definido por uma substituição. A linguagem não define o que um recurso faz; em vez disso, define que é exatamente equivalente a outra coisa. Por exemplo, para cada loop

for(Object alpha: alphas) {
}

Torna-se:

for(Iterator<Object> iter = alpha.iterator(); iter.hasNext()) {
   alpha = iter.next();
}

Ou use uma função com argumentos variáveis:

void foo(int... args);

foo(3, 4, 5);

O que se torna:

void Foo(int[] args);

foo(new int[]{3, 4, 5});

Portanto, há uma substituição trivial da sintaxe para implementar o recurso em termos de outros recursos.

Vejamos a sobrecarga do método.

void foo(int a);
void foo(double b);

foo(4.5);

Isso pode ser reescrito como:

void foo_int(int a);
void foo_double(double b);

foo_double(4.5);

Mas não é equivalente a isso. Dentro do modelo de Java, isso é algo diferente. foo(int a)não implementa uma foo_intfunção a ser criada. Java não implementa a sobrecarga de método, dando nomes engraçados às funções ambíguas. Para contar como açúcar sintático, o java teria que estar fingindo que você realmente escreveu foo_inte foo_doublefunciona, mas não funciona.


2
Acho que ninguém disse que a transformação para o açúcar da sintaxe deve ser trivial. Mesmo assim, acho a afirmação But, the transformation isn't trivial. At the least, you have to determine the types of the parameters.muito superficial, porque os tipos não precisam ser determinados ; eles são conhecidos em tempo de compilação.
Doval

3
"Para contar como açúcar sintático, o java teria que estar fingindo que você realmente escreveu as funções foo_int e foo_double, mas não o faz." - desde que falemos de métodos de sobrecarga e não de polimorfismos, qual seria a diferença entre foo(int)/ foo(double)e foo_int/ foo_double? Eu realmente não conheço Java muito bem, mas imagino que essa renomeação realmente aconteça na JVM (bem - provavelmente usando foo(args)mais do que isso foo_args- acontece pelo menos em C ++ com manipulação de símbolos (ok - a manipulação de símbolos é tecnicamente um detalhe de implementação e não faz parte) da linguagem).
Maciej Piechotka

2
@Doval: "Acho que ninguém nunca disse que a transformação do açúcar de sintaxe deve ser trivial." - Verdade, mas tem que ser local . A única definição útil de açúcar sintático que conheço é do famoso artigo de Matthias Felleisen sobre expressividade da linguagem, e basicamente diz que se você pode reescrever um programa escrito na linguagem L + y (ou seja, alguma linguagem L com alguma característica y ) em linguagem L (ou seja, um subconjunto desse idioma sem o recurso y ) sem alterar a estrutura global do programa (ou seja, apenas fazendo alterações locais), então y é o açúcar sintático em L + y e faz
Jörg W Mittag

2
... não aumenta a expressividade de L. No entanto, se você não pode fazer isso, ou seja, se você tem que fazer mudanças na estrutura mundial de seu programa, então é não açúcar sintático e faz de fato fazer L + y mais expressiva do que L . Por exemplo, Java com forloop aprimorado não é mais expressivo que Java sem ele. (É mais agradável, mais conciso, mais legível e, em geral, melhor, eu diria, mas não mais expressivo.) Mas não tenho certeza sobre o caso de sobrecarga. Provavelmente terei que reler o artigo para ter certeza. Meu intestino diz que é açúcar sintático, mas não tenho certeza.
Jörg W Mittag

2
@MaciejPiechotka, se fosse parte da definição de linguagem que as funções foram renomeadas e você pudesse acessar a função com esses nomes, acho que seria açúcar sintático. Mas porque está oculto como um detalhe de implementação, acho que o desqualifica de ser um açúcar sintático.
Winston Ewert

8

Dado que a mudança de nome funciona, não precisa ser nada além de açúcar sintático?

Permite ao chamador imaginar que está chamando a mesma função, quando não está. Mas ele sabia os nomes reais de todas as suas funções. Somente se fosse possível obter um polimorfismo atrasado passando uma variável não digitada para uma função digitada e tendo seu tipo estabelecido para que a chamada pudesse ir para a versão correta de acordo com o nome, esse seria um recurso de linguagem real.

Infelizmente, nunca vi um idioma fazer isso. Quando há ambiguidade, esses compiladores não a resolvem, eles insistem em que o escritor a resolva por eles.


O recurso que você está procurando é chamado "Despacho Múltiplo". Muitos idiomas suportam, incluindo Haskell, Scala e (desde 4.0) C #.
Iain Galloway

Eu gostaria de separar os parâmetros nas classes da sobrecarga de método direto. No caso de sobrecarga de método direto, o programador grava todas as versões, o compilador apenas sabe escolher uma. Isso é apenas açúcar sintático, e é resolvido com a simples troca de nomes, mesmo para envio múltiplo. --- Na presença de parâmetros nas classes, o compilador gera o código conforme necessário, e isso muda completamente.
JavaScript é necessário

2
Eu acho que você não entendeu. Por exemplo, em C #, se um dos parâmetros de um método for dynamic, a resolução de sobrecarga ocorrerá no tempo de execução, não no tempo de compilação . É isso que é o despacho múltiplo e não pode ser replicado renomeando funções.
Iain Galloway

Bem legal. No entanto, ainda posso testar o tipo de variável, portanto, essa ainda é apenas uma função interna sobreposta ao açúcar sintático. É um recurso de idioma, mas apenas por pouco.
JavaScript é necessário

7

Dependendo do idioma, é açúcar sintático ou não.

No C ++, por exemplo, você pode fazer coisas usando sobrecarga e modelos, o que não seria possível sem complicações (escreva manualmente todas as instâncias do modelo ou adicione muitos parâmetros).

Observe que o despacho dinâmico é uma forma de sobrecarga, resolvida dinamicamente em alguns parâmetros (para alguns idiomas apenas um especial, este , mas nem todos os idiomas são tão limitados), e eu não chamaria essa forma de sobrecarga de açúcar sintático.


Não tenho certeza de como as outras respostas estão indo muito melhor quando essencialmente incorretas.
Telastyn

5

Para idiomas contemporâneos, é apenas açúcar sintático; de uma maneira completamente independente da linguagem, é mais do que isso.

Anteriormente, essa resposta dizia simplesmente que é mais do que açúcar sintático, mas se você ver nos comentários, Falco levantou a questão de que havia uma peça do quebra-cabeça que as línguas contemporâneas parecem estar faltando; eles não misturam sobrecarga de método com determinação dinâmica de qual função chamar na mesma etapa. Isso será esclarecido mais tarde.

Aqui é por isso que deve haver mais.

Considere uma linguagem que suporte sobrecarga de método e variáveis ​​não tipadas. Você pode ter os seguintes protótipos de método:

bool someFunction(int arg);

bool someFunction(string arg);

Em alguns idiomas, você provavelmente se resignaria a saber, em tempo de compilação, qual deles seria chamado por uma determinada linha de código. Mas em alguns idiomas, nem todas as variáveis ​​são digitadas (ou todas implicitamente digitadas como Objectou o que for), então imagine criar um dicionário cujas chaves sejam mapeadas para valores de diferentes tipos:

dict roomNumber; // some hotels use numbers, some use letters, and some use
                 // alphanumerical strings.  In some languages, built-in dictionary
                 // types automatically use untyped values for their keys to map to,
                 // so it makes more sense then to allow for both ints and strings in
                 // your code.

Agora, então, e se você quisesse aplicar someFunctiona um desses números de quarto? Você chama isso:

someFunction(roomNumber[someSortOfKey]);

É someFunction(int)chamado ou é someFunction(string)chamado? Aqui você vê um exemplo em que esses métodos não são totalmente ortogonais, especialmente em idiomas de nível superior. A linguagem precisa descobrir - durante o tempo de execução - qual delas chamar, por isso ainda precisa considerá-las como sendo pelo menos um pouco o mesmo método.

Por que não simplesmente usar modelos? Por que não usar simplesmente um argumento não digitado?

Flexibilidade e controle mais refinado. Às vezes, usar modelos / argumentos sem tipo é uma abordagem melhor, mas às vezes não.

Você deve pensar nos casos em que, por exemplo, você pode ter duas assinaturas de método que usam cada um inte a stringcomo argumentos, mas onde a ordem é diferente em cada assinatura. Você pode muito bem ter um bom motivo para fazer isso, pois a implementação de cada assinatura pode fazer basicamente a mesma coisa, mas com um toque um pouco diferente; o registro pode ser diferente, por exemplo. Ou mesmo se eles fizerem exatamente a mesma coisa, você poderá coletar automaticamente determinadas informações apenas pela ordem em que os argumentos foram especificados. Tecnicamente, você pode usar apenas instruções pseudo-switch para determinar o tipo de cada um dos argumentos passados, mas isso fica confuso.

Então, este próximo exemplo é uma prática ruim de programação?

bool stringIsTrue(int arg)
{
    if (arg.toString() == "0")
    {
        return false;
    }
    else
    {
        return true;
    }
}

bool stringIsTrue(Object arg)
{
    if (arg.toString() == "0")
    {
        return false;
    }
    else
    {
        return true;
    }
}

bool stringIsTrue(string arg)
{
    if (arg == "0")
    {
        return false;
    }
    else
    {
        return true;
    }
}

Sim, em geral. Neste exemplo em particular, isso poderia impedir alguém de aplicar isso a certos tipos primitivos e recuperar comportamentos inesperados (o que poderia ser uma coisa boa); mas vamos supor que eu abreviei o código acima e que você, de fato, possui sobrecargas para todos os tipos primitivos, bem como para Objects. Então este próximo pedaço de código é realmente mais apropriado:

bool stringIsTrue(untyped arg)
{
    if (arg.toString() == "0")
    {
        return false;
    }
    else
    {
        return true;
    }
}

Mas e se você só precisava usar isso para ints e strings, e se você quer que ele retornar verdadeiro com base em condições mais simples ou mais complicado nesse sentido? Então você tem um bom motivo para usar a sobrecarga:

bool appearsToBeFirstFloor(int arg)
{
    if (arg.digitAt(0) == 1)
    {
        return true;
    }
    else
    {
        return false;
    }
}

bool appearsToBeFirstFloor(string arg)
{
    string firstCharacter = arg.characterAt(0);
    if (firstCharacter.isDigit())
    {
        return appearsToBeFirstFloor(int(firstCharacter));
    }
    else if (firstCharacter.toUpper() == "A")
    {
        return true;
    }
    else
    {
        return false;
    }
}

Mas ei, por que não dar a essas funções dois nomes diferentes? Você ainda tem a mesma quantidade de controle refinado, não é?

Porque, como afirmado anteriormente, alguns hotéis usam números, alguns usam letras e alguns usam uma mistura de números e letras:

appearsToBeFirstFloor(roomNumber[someSortOfKey]);

// will treat ints and strings differently, without you having to write extra code
// every single spot where the function is being called

Este ainda não é exatamente o mesmo código exato que eu usaria na vida real, mas deve ilustrar o ponto que estou fazendo bem.

Mas ... Eis por que não é mais que açúcar sintático nas línguas contemporâneas.

Falco levantou a questão nos comentários de que os idiomas atuais basicamente não combinam sobrecarga de método e seleção dinâmica de função na mesma etapa. A maneira como eu entendi anteriormente que certas linguagens funcionavam era que você poderia sobrecarregar appearsToBeFirstFloorno exemplo acima e, em seguida, a linguagem determinaria em tempo de execução qual versão da função seria chamada, dependendo do valor em tempo de execução da variável não digitada. Essa confusão resultou parcialmente do trabalho com tipos de linguagens ECMA, como o ActionScript 3.0, no qual você pode facilmente randomizar qual função é chamada em uma determinada linha de código em tempo de execução.

Como você deve saber, o ActionScript 3 não suporta sobrecarga de métodos. Quanto ao VB.NET, você pode declarar e definir variáveis ​​sem atribuir um tipo explicitamente, mas ao tentar passar essas variáveis ​​como argumentos para métodos sobrecarregados, ele ainda não deseja ler o valor do tempo de execução para determinar qual método chamar; em vez disso, deseja encontrar um método com argumentos do tipo Objectou nenhum tipo ou algo assim. Portanto, o exemplo intvs. stringacima também não funcionaria nesse idioma. O C ++ tem problemas semelhantes, pois quando você usa algo como um ponteiro nulo ou algum outro mecanismo como esse, ainda é necessário desambiguar manualmente o tipo em tempo de compilação.

Então, como o primeiro cabeçalho diz ...

Para idiomas contemporâneos, é apenas açúcar sintático; de uma maneira completamente independente da linguagem, é mais do que isso. Tornar a sobrecarga de método mais útil e relevante, como no exemplo acima, pode realmente ser um bom recurso para adicionar a um idioma existente (como tem sido amplamente implicitamente solicitado para o AS3), ou também pode servir como um entre muitos pilares fundamentais diferentes para a criação de uma nova linguagem processual / orientada a objetos.


3
Você pode nomear algum idioma que realmente lida com o Function-Dispatch no tempo de execução e não no tempo de compilação? TODAS AS línguas que conheço necessitam de segurança em tempo de compilação dos quais função é chamada ...
Falco

O @Falco ActionScript 3.0 lida com isso em tempo de execução. Você poderia, por exemplo, usar uma função que retorna um dos três cordas de forma aleatória, e então usar seu valor de retorno para ligar para qualquer uma das três funções de forma aleatória: this[chooseFunctionNameAtRandom](); Se chooseFunctionNameAtRandom()retornos tanto "punch", "kick"ou "dodge", então você pode thusly implementar uma forma muito simples aleatória elemento em, por exemplo, a IA de um inimigo em um jogo em Flash.
Panzercrisis

1
Sim - mas ambos são métodos semânticos reais para obter o envio dinâmico de funções, o Java também os possui. Mas eles são diferentes de sobrecarga, sobrecarga é estática e apenas açúcar sintático, enquanto despacho e herança dinâmicos são recursos de linguagem real, que oferecem novas funcionalidades!
Falco

1
... Também tentei ponteiros nulos em C ++, bem como ponteiros de classe base, mas o compilador queria que eu desambiguasse antes de passá-lo para uma função. Então agora estou me perguntando se deseja excluir esta resposta. Começa a parecer que os idiomas quase sempre combinam a escolha dinâmica de funções com a sobrecarga de funções na mesma instrução ou instrução, mas depois se afastam no último segundo. Seria um recurso interessante de linguagem; talvez alguém precise criar uma linguagem que possua isso.
Panzercrisis

1
Deixe a resposta ficar, talvez pense em incluir algumas de suas pesquisas nos comentários da resposta?
Falco

2

Realmente depende da sua definição de "açúcar sintático". Vou tentar abordar algumas das definições que me vêm à mente:

  1. Um recurso é um açúcar sintático quando um programa que o usa sempre pode ser traduzido em outro que não o usa.

    Aqui estamos assumindo que existe um conjunto primitivo de recursos que não podem ser traduzidos: em outras palavras, não existem loops do tipo "você pode substituir o recurso X usando o recurso Y" e "você pode substituir o recurso Y pelo recurso X". Se um dos dois for verdadeiro, o outro recurso poderá ser expresso em termos de recursos que não são o primeiro ou é um recurso primitivo.

  2. Igual à definição 1, mas com o requisito extra de que o programa traduzido seja tão seguro quanto o primeiro, ou seja, ao desugar você não perde nenhum tipo de informação.

  3. A definição do OP: um recurso é o açúcar sintático se sua tradução não altera a estrutura do programa, mas requer apenas "alterações locais".

Vamos usar o Haskell como exemplo para sobrecarga. Haskell fornece sobrecarga definida pelo usuário através de classes de tipo. Por exemplo, as operações +e *são definidas na Numclasse type e qualquer tipo que tenha uma instância (completa) dessa classe pode ser usada +. Por exemplo:

instance Num a => Num (b, a) where
    (x, y) + (_, y') = (x, y + y')
    -- other definitions

("Hello", 1) + ("World", 3) -- -> ("Hello", 4)

Uma coisa bem conhecida sobre as classes de tipo de Haskell é que você pode se livrar delas . Ou seja, você pode traduzir qualquer programa que utilize classes de tipos em um programa equivalente que não as utilize.

A tradução é bastante simples:

  • Dada uma definição de classe:

    class (P_1 a, ..., P_n a) => X a where
        op_1 :: t_1   ... op_m :: t_m
    

    Você pode convertê-lo em um tipo de dados algébrico:

    data X a = X {
        X_P_1 :: P_1 a, ... X_P_n :: P_n a,
        X_op_1 :: t_1, ..., X_op_m :: t_m
    }
    

    Aqui X_P_ie X_op_isão seletores . Ou seja, dado um valor do tipo que se X aaplica X_P_1ao valor, ele retornará o valor armazenado nesse campo, portanto, são funções com o tipo X a -> P_i a(ou X a -> t_i).

    Para uma anologia muito grosseira, você pode pensar nos valores para o tipo X acomo structse, em seguida, se xfor do tipo, X aas expressões:

    X_P_1 x
    X_op_1 x
    

    pode ser visto como:

    x.X_P_1
    x.X_op_1
    

    (É fácil usar apenas campos posicionais em vez de campos nomeados, mas os campos nomeados são mais fáceis de manipular nos exemplos e evitam algum código da placa da caldeira).

  • Dada uma declaração de instância:

    instance (C_1 a_1, ..., C_n a_n) => X (T a_1 ... a_n) where
        op_1 = ...; ...;  op_m = ...
    

    Você pode convertê-lo em uma função que, devido aos dicionários das C_1 a_1, ..., C_n a_nclasses, retorne um valor de dicionário (ou seja, um valor do tipo X a) para o tipo T a_1 ... a_n.

    Em outras palavras, a instância acima pode ser traduzida para uma função como:

    f :: C_1 a_1 -> ... -> C_n a_n -> X (T a_1 ... a_n)
    

    (Observe que npode ser 0).

    E, de fato, podemos defini-lo como:

    f c1 ... cN = X {X_P_1=get_P_1_T, X_P_n=get_P_n_T,
                     X_op_1=op_1, ..., X_op_m=op_m}
        where
            op_1 = ...
            ...
            op_m = ...
    

    onde op_1 = ...a op_m = ...é a definição encontrada na instancedeclaração e o get_P_i_Tsão as funções definidas pelo P_iexemplo do Ttipo (estas têm de existir porque P_is são superclasses de X).

  • Dada uma chamada para uma função sobrecarregada:

    add :: Num a => a -> a -> a
    add x y = x + y
    

    Podemos passar explicitamente os dicionários relativos às restrições de classe e obter uma chamada equivalente:

    add :: Num a -> a -> a -> a
    add dictNum x y = ((+) dictNum) x y
    

    Observe como as restrições de classe simplesmente se tornaram um novo argumento. O +programa traduzido é o seletor, conforme explicado anteriormente. Em outras palavras, a addfunção traduzida , dado o dicionário para o tipo de argumento, primeiro "descompactará" a função real para calcular o resultado usando (+) dictNume, em seguida, aplicará essa função aos argumentos.

Este é apenas um esboço muito rápido sobre a coisa toda. Se você estiver interessado, leia os artigos de Simon Peyton Jones et al.

Acredito que uma abordagem semelhante também possa ser usada para sobrecarregar em outros idiomas.

No entanto, isso mostra que, se sua definição de açúcar sintático é (1), sobrecarregar é açúcar sintático . Porque você pode se livrar disso.

No entanto, o programa traduzido perde algumas informações sobre o programa original. Por exemplo, não impõe a existência de instâncias para as classes pai. (Mesmo que as operações para extrair os dicionários dos pais ainda devam ser desse tipo, você pode passar undefinedvalores polimórficos ou outros, para poder criar um valor X ysem os valores P_i y, para que a tradução não perca tudo o tipo de segurança). Portanto, não é o açúcar sintático de acordo com (2)

Quanto a (3). Não sei se a resposta deve ser sim ou não.

Eu diria que não, porque, por exemplo, uma declaração de instância se torna uma definição de função. As funções sobrecarregadas recebem um novo parâmetro (o que significa que altera a definição e todas as chamadas).

Eu diria que sim, porque os dois programas ainda mapeiam um para um, então a "estrutura" não é realmente muito alterada.


Dito isto, eu diria que as vantagens pragmáticas introduzidas pela sobrecarga são tão grandes que o uso de um termo "depreciativo" como "açúcar sintático" não parece correto.

Você pode traduzir toda a sintaxe Haskell para uma linguagem Core muito simples (que na verdade é feita durante a compilação); portanto, a maioria da sintaxe Haskell pode ser vista como "açúcar sintático" para algo que é apenas cálculo lambda e algumas novas construções. No entanto, podemos concordar que os programas Haskell são muito mais fáceis de manusear e são muito concisos, enquanto os programas traduzidos são muito mais difíceis de ler ou pensar.


2

Se a expedição for resolvida no tempo de compilação, dependendo apenas do tipo estático da expressão do argumento, você certamente poderá argumentar que é "açúcar sintático" substituindo dois métodos diferentes por nomes diferentes, desde que o programador "conheça" o tipo estático e poderia apenas usar o nome do método correto no lugar do nome sobrecarregado. É também uma forma de polimorfismo estático, mas nessa forma limitada geralmente não é muito poderosa.

É claro que seria um incômodo alterar os nomes dos métodos que você chama sempre que alterar o tipo de uma variável, mas, por exemplo, na linguagem C é considerado um incômodo gerenciável, portanto, C não possui sobrecarga de função (embora agora possui macros genéricas).

Nos modelos C ++ e em qualquer linguagem que deduza o tipo estático não trivial, não é possível argumentar que isso é "açúcar sintático", a menos que você também defenda que a dedução do tipo estático é "açúcar sintático". Seria um incômodo não ter modelos e, no contexto do C ++, seria um "incômodo incontrolável", pois eles são tão idiomáticos para a linguagem e suas bibliotecas padrão. Portanto, em C ++ é mais do que um bom ajudante, é importante para o estilo da linguagem e, portanto, acho que você deve chamá-la mais do que "açúcar sintático".

Em Java, você pode considerar isso mais do que apenas uma conveniência, considerando, por exemplo, quantas sobrecargas existem PrintStream.print e PrintStream.println. Porém, existem tantos DataInputStream.readXmétodos, já que o Java não sobrecarrega no tipo de retorno; portanto, em certo sentido, é apenas por conveniência. Esses são todos para tipos primitivos.

Não me lembro o que acontece em Java se eu tiver aulas Ae Bestendendo O, eu sobrecarregar métodos foo(O), foo(A)e foo(B), em seguida, em um genérico com <T extends O>chamo foo(t)onde té uma instância de T. No caso em que TéA que eu ganho envio com base na sobrecarga ou é como se eu chamasse foo(O)?

No caso anterior, as sobrecargas do método Java são melhores que o açúcar, da mesma forma que as sobrecargas do C ++. Usando sua definição, suponho que em Java eu ​​poderia escrever localmente uma série de verificações de tipo (que seriam frágeis, porque novas sobrecargas fooexigiriam verificações adicionais). Além de aceitar essa fragilidade, não posso fazer uma alteração local no site de chamada para acertar, em vez disso, teria que desistir de escrever código genérico. Eu argumentaria que a prevenção de códigos inchados pode ser um açúcar sintático, mas a prevenção de códigos frágeis é mais do que isso. Por esse motivo, o polimorfismo estático em geral é mais do que apenas açúcar sintático. A situação em um idioma específico pode ser diferente, dependendo de quão longe o idioma permita "não conhecer" o tipo estático.


Em Java, as sobrecargas são resolvidas no momento da compilação. Dado o uso do tipo de apagamento, seria impossível que eles fossem de outro modo. Além disso, mesmo sem eliminação de tipo, se T:Animalé é tipo SiameseCate sobrecargas existentes Cat Foo(Animal), SiameseCat Foo(Cat)e Animal Foo(SiameseCat), que a sobrecarga deve ser selecionado se Té SiameseCat?
Supercat

@ Supercat: faz sentido. Então, eu poderia ter descoberto a resposta sem lembrar (ou, é claro, executá-la). Portanto, as sobrecargas de Java não são melhores que o açúcar, da mesma forma que as sobrecargas de C ++ estão relacionadas ao código genérico. Continua sendo possível que exista alguma outra maneira pela qual eles são melhores do que apenas uma transformação local. Será que devo mudar meu exemplo para C ++ ou deixá-lo como um Java imaginado que não é Java real?
26514 Steve Joplin

Sobrecargas podem ser úteis nos casos em que os métodos têm argumentos opcionais, mas também podem ser perigosos. Suponha que a linha long foo=Math.round(bar*1.0001)*5seja alterada para long foo=Math.round(bar)*5. Como isso afetaria a semântica se fosse barigual, por exemplo, 123456789L?
supercat

@ supercat Eu diria que o perigo real é a conversão implícita de longpara double.
Doval

@Doval: Para double?
Supercat

1

Parece que "açúcar sintático" soa depreciativo, como inútil ou frívolo. É por isso que a pergunta desencadeia muitas respostas negativas.

Mas você está certo, a sobrecarga de método não adiciona nenhum recurso ao idioma, exceto pela possibilidade de usar o mesmo nome para métodos diferentes. Você pode tornar o tipo de parâmetro explícito, o programa continuará funcionando da mesma forma.

O mesmo se aplica aos nomes dos pacotes. String é apenas açúcar sintático para java.lang.String.

De fato, um método como

void fun(int i, String c);

na classe MyClass deve ser chamado algo como "my_package_MyClass_fun_int_java_lang_String". Isso identificaria o método exclusivamente. (A JVM faz algo assim internamente). Mas você não quer escrever isso. É por isso que o compilador permite que você escreva divertido (1, "um") e identifique qual é o método.

No entanto, há uma coisa que você pode fazer com a sobrecarga: se você sobrecarregar um método com o mesmo número de argumentos, o compilador descobrirá automaticamente qual versão se adapta melhor ao argumento fornecido pelos argumentos correspondentes, não apenas com tipos iguais, mas também onde o argumento fornecido é uma subclasse do argumento declarado.

Se você tiver dois procedimentos sobrecarregados

addParameter(String name, Object value);
addParameter(String name, Date value);

você não precisa saber que existe uma versão específica do procedimento para Datas. addParameter ("hello", "world) chamará a primeira versão, addParameter (" now ", new Date ()) chamará a segunda.

Obviamente, você deve evitar sobrecarregar um método com outro método que faça algo completamente diferente.


1

Curiosamente, a resposta a esta pergunta dependerá do idioma.

Especificamente, há uma interação entre sobrecarga e programação genérica (*) e, dependendo de como a programação genérica é implementada, pode ser apenas açúcar sintático (Rust) ou absolutamente necessário (C ++).

Ou seja, quando a programação genérica é implementada com interfaces explícitas (em Rust ou Haskell, essas seriam classes de tipo), a sobrecarga é apenas um açúcar sintático; ou, na verdade, talvez nem faça parte do idioma.

Por outro lado, quando a programação genérica é implementada com digitação de pato (seja dinâmica ou estática), o nome do método é um contrato essencial e, portanto, a sobrecarga é obrigatória para o sistema funcionar.

(*) Usado no sentido de escrever um método uma vez, para operar sobre vários tipos de maneira uniforme.


0

Em algumas línguas, é sem dúvida apenas o açúcar sintático. No entanto, o que é um açúcar depende do seu ponto de vista. Vou deixar essa discussão para mais adiante nesta resposta.

Por enquanto, gostaria de observar que, em alguns idiomas, certamente não é um açúcar sintático. Pelo menos não sem exigir que você use uma lógica / algoritmo completamente diferente para implementar a mesma coisa. É como afirmar que a recursão é um açúcar sintático (que é porque você pode escrever todo o algoritmo recursivo com um loop e uma pilha).

Um exemplo de uso muito difícil de substituir vem de uma linguagem que ironicamente não chama esse recurso de "sobrecarga de função". Em vez disso, é chamado de "correspondência de padrão" (que pode ser vista como um superconjunto de sobrecarga, porque podemos sobrecarregar não apenas tipos, mas valores).

Aqui está a implementação ingênua clássica da função Fibonacci em Haskell:

fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)

Indiscutivelmente, as três funções podem ser substituídas por um if / else, como geralmente é feito em qualquer outro idioma. Mas isso fundamentalmente torna a definição totalmente simples:

fib n = fib (n-1) + fib (n-2)

muito mais confuso e não expressa diretamente a noção matemática da sequência de Fibonacci.

Portanto, às vezes, pode ser açúcar de sintaxe, se o único uso é permitir que você chame uma função com argumentos diferentes. Mas, às vezes, é muito mais fundamental que isso.


Agora, vamos discutir o motivo pelo qual a sobrecarga do operador pode ser um açúcar. Você identificou um caso de uso - ele pode ser usado para implementar funções semelhantes que usam argumentos diferentes. Então:

function print (string x) { stdout.print(x) };
function print (number x) { stdout.print(x.toString) };

Como alternativa, pode ser implementado como:

function printString (string x) {...}
function printNumber (number x) {...}

ou até:

function print (auto x) {
    if (x instanceof String) {...}
    if (x instanceof Number) {...}
}

Mas a sobrecarga do operador também pode ser uma ferramenta importante para implementar argumentos opcionais (alguns idiomas têm sobrecarga de operador, mas não argumentos opcionais):

function print (string x) {...}
function print (string x, stream io) {...}

pode ser usado para implementar:

function print (string x, stream io=stdout) {...}

Nessa linguagem (google "Ferite language"), a remoção de sobrecarga do operador remove drasticamente um recurso - argumentos opcionais. Concedido em idiomas com ambos os recursos (c ++), remover um ou outro não terá efeito líquido, pois ambos podem ser usados ​​para implementar argumentos opcionais.


Haskell é um bom exemplo de por que a sobrecarga do operador não é um açúcar sintático, mas acho que um exemplo melhor seria desconstruir um tipo de dados algébrico com correspondência de padrões (algo que eu sei que é impossível sem a correspondência de padrões).
11684

@ 11684: Você pode apontar para um exemplo? Sinceramente, não conheço Haskell, mas achei seu padrão correspondente sublimemente elegante quando vi esse exemplo de mentira (no computerphile no youtube).
precisa saber é

Dado um tipo de dados como data PaymentInfo = CashOnDelivery | Adress String | UserInvoice CustomerInfovocê pode fazer a correspondência de padrões nos construtores de tipos.
11684

Como esta: getPayment :: PaymentInfo -> a getPayment CashOnDelivery = error "Should have been paid already" getPayment (Adress addr) = -- code to notify administration to send a bill getPayment (UserInvoice cust) = --whatever. I took the data type from a Haskell tutorial and have no idea what an invoice is. Espero que este comentário seja um pouco compreensível.
11684

0

Eu acho que é um açúcar sintático simples na maioria dos idiomas (pelo menos tudo o que sei ...), pois todos eles exigem uma chamada de função inequívoca em tempo de compilação. E o compilador simplesmente substitui a chamada de função por um ponteiro explícito para a assinatura de implementação correta.

Exemplo em Java:

String s; int i;
mangle(s);  // Translates to CALL ('mangle':LString):(s)
mangle(i);  // Translates to CALL ('mangle':Lint):(i)

Portanto, no final, poderia ser completamente substituído por uma macro-compilador simples com pesquisa e substituição, substituindo a função sobrecarregada mangle por mangle_String e mangle_int - uma vez que a lista de argumentos faz parte do eventual identificador de função, é praticamente o que acontece -> e portanto, é apenas açúcar sintático.

Agora, se houver uma linguagem em que a função seja realmente determinada em tempo de execução, como nos Métodos substituídos nos objetos, isso seria diferente. Mas não creio que exista essa linguagem, já que method.overloading é propenso a ambiguidade, que o compilador não pode resolver e que deve ser tratado pelo programador com um elenco explícito. Isso não pode ser feito em tempo de execução.


0

No tipo Java, as informações são compiladas e qual das sobrecargas é chamada é decidida no momento da compilação.

A seguir, um trecho de sun.misc.Unsafe(o utilitário Atomics), conforme exibido no editor de arquivos de classe do Eclipse.

  // Method descriptor #143 (Ljava/lang/Object;I)I (deprecated)
  // Stack: 4, Locals: 3
  @java.lang.Deprecated
  public int getInt(java.lang.Object arg0, int arg1);
    0  aload_0 [this]
    1  aload_1 [arg0]
    2  iload_2 [arg1]
    3  i2l
    4  invokevirtual sun.misc.Unsafe.getInt(java.lang.Object, long) : int [231]
    7  ireturn
      Line numbers:
        [pc: 0, line: 213]

como você pode ver as informações de tipo do método que está sendo chamado (linha 4) estão incluídas na chamada.

Isso significa que você pode criar um compilador java que usa informações de tipo. Por exemplo, usando essa notação, a fonte acima seria:

@Deprecated
public in getInt(Object arg0, int arg1){
     return getInt$(Object,long)(arg0, arg1);
}

e o elenco para longo seria opcional.

Em outras linguagens compiladas estaticamente, você verá uma configuração semelhante em que o compilador decidirá qual sobrecarga será chamada dependendo dos tipos e a incluirá na ligação / chamada.

A exceção são as bibliotecas dinâmicas C, nas quais as informações de tipo não são incluídas e a tentativa de criar uma função sobrecarregada fará com que o vinculador reclame.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.