Il metodo sta sovraccaricando qualcosa di più dello zucchero sintattico? [chiuso]


19

Il metodo sta sovraccaricando un tipo di polimorfismo? A me sembra semplicemente la differenziazione di metodi con lo stesso nome e parametri diversi. Quindi stuff(Thing t)e stuff(Thing t, int n)sono metodi completamente diversi per quanto riguarda il compilatore e il runtime.

Crea l'illusione, dal lato del chiamante, che sia lo stesso metodo che agisce diversamente su diversi tipi di oggetti: il polimorfismo. Ma questa è solo un'illusione, perché in realtà stuff(Thing t)e stuff(Thing t, int n)sono metodi completamente diversi.

Il metodo sta sovraccaricando qualcosa di più dello zucchero sintattico? Mi sto perdendo qualcosa?


Una definizione comune per lo zucchero sintattico è che è puramente locale . Il significato di cambiare un pezzo di codice nel suo equivalente "addolcito", o viceversa, implica cambiamenti locali che non influiscono sulla struttura generale del programma. E penso che il sovraccarico del metodo si adatti esattamente a questo criterio. Diamo un'occhiata a un esempio per dimostrare:

Prendi in considerazione una lezione:

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

Ora considera un'altra classe che usa questa 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
    }
}

Va bene. Ora vediamo cosa deve cambiare se sostituiamo il sovraccarico del metodo con metodi regolari:

I readmetodi Readercambiano in readBook(Book)e readFile(file). Solo una questione di cambiare il loro nome.

Il codice chiamante FileProcessorcambia leggermente: reader.read(file)cambia in reader.readFile(file).

E questo è tutto.

Come puoi vedere, la differenza tra usare il metodo di overload e non usarlo è puramente locale . Ed è per questo che penso che si qualifichi come puro zucchero sintattico.

Mi piacerebbe sentire le tue obiezioni se ne hai alcune, forse mi manca qualcosa.


48
Alla fine, qualsiasi caratteristica del linguaggio di programmazione è solo zucchero sintattico per l'assemblatore grezzo.
Philipp,

31
@Philipp: Mi dispiace, ma è un'affermazione davvero stupida. I linguaggi di programmazione derivano la loro utilità dalla semantica, non dalla sintassi. Funzionalità come un sistema di tipi ti offrono garanzie reali, anche se potrebbero richiedere di scriverne di più .
back2dos,

3
Chiediti questo: l'operatore sta sovraccaricando solo zucchero sintattico? Qualunque sia la risposta a quella domanda che ritieni vera è anche la risposta alla domanda che hai posto;)
back2dos

5
@ back2dos: totalmente d'accordo con te. Ho letto la frase "tutto è solo zucchero sintattico per assemblatore" troppo spesso, ed è chiaramente sbagliato. Lo zucchero sintattico è una sintassi alternativa (forse più bella) per alcune sintassi esistenti che non aggiungono alcuna nuova semantica.
Giorgio,

6
@Giorgio: giusto! C'è una definizione precisa nel documento di riferimento sull'espressività di Matthias Felleisen. Fondamentalmente: lo zucchero sintattico è puramente locale. Se devi modificare la struttura globale del programma per rimuovere l'utilizzo della funzionalità della lingua, non è zucchero sintattico. Vale a dire riscrivere il codice polimorfico OO nell'assemblatore comporta in genere l'aggiunta della logica di invio globale, che non è puramente locale, quindi OO non è "solo zucchero sintattico per l'assemblatore".
Jörg W Mittag,

Risposte:


29

Per rispondere a questa domanda, devi prima definire una definizione di "zucchero sintattico". Andrò con Wikipedia :

In informatica, lo zucchero sintattico è sintassi all'interno di un linguaggio di programmazione progettato per rendere le cose più facili da leggere o da esprimere. Rende il linguaggio "più dolce" per uso umano: le cose possono essere espresse in modo più chiaro, più conciso o in uno stile alternativo che alcuni potrebbero preferire.

[...]

In particolare, un costrutto in una lingua è chiamato zucchero sintattico se può essere rimosso dalla lingua senza alcun effetto su ciò che la lingua può fare

Quindi, in base a questa definizione, caratteristiche come i vararg di Java o la comprensione di Scala sono zucchero sintattico: si traducono in funzioni linguistiche sottostanti (un array nel primo caso, chiamate a map / flatmap / filter nel secondo) e rimuoverle sarebbe non cambiare le cose che puoi fare con la lingua.

Il sovraccarico del metodo, tuttavia, non è zucchero sintattico in questa definizione, perché rimuoverlo cambierebbe radicalmente la lingua (non si sarebbe più in grado di inviare comportamenti distinti basati su argomenti).

È vero, puoi simulare il sovraccarico del metodo purché tu abbia un modo per accedere agli argomenti di un metodo e puoi usare un costrutto "if" basato sugli argomenti che ti vengono dati. Ma se consideri quello zucchero sintattico, dovresti considerare qualsiasi cosa al di sopra di una macchina di Turing per essere allo stesso modo zucchero sintattico.


22
Rimuovere il sovraccarico non cambierebbe ciò che la lingua può fare. Potresti ancora fare esattamente le stesse cose di prima; dovresti solo rinominare alcuni metodi. Questo è un cambiamento più banale rispetto ai loop desugaring.
Doval,

9
Come ho detto, potresti adottare l'approccio secondo cui tutte le lingue (comprese le lingue automatiche) sono semplicemente zucchero sintattico su una macchina di Turing.
kdgregory,

9
Dal mio punto di vista, il sovraccarico del metodo consente semplicemente di fare sum(numbersArray)e sum(numbersList)invece di sumArray(numbersArray)e sumList(numbersList). Concordo con Doval, sembra un semplice zucchero sintatico.
Aviv Cohn,

3
Gran parte della lingua. Prova attuazione instanceof, le classi, l'ereditarietà, le interfacce, i farmaci generici, di riflessione o specificatori di accesso che utilizzano if, whilee gli operatori booleani, con l'esatto stessa semantica . Nessun caso d'angolo. Nota che non ti sfido a calcolare le stesse cose degli usi specifici di quei costrutti. Io già so che è possibile calcolare qualsiasi cosa usando la logica booleana e la ramificazione / loop. Ti sto chiedendo di implementare copie perfette della semantica di tali funzionalità linguistiche, comprese eventuali garanzie statiche che forniscono (i controlli del tempo di compilazione devono ancora essere eseguiti al momento della compilazione).
Doval

6
@Doval, kdgregory: per definire lo zucchero sintattico devi definirlo relativamente ad alcune semantiche. Se l'unica semantica che hai è "Che cosa calcola questo programma?", Allora è chiaro che tutto è solo zucchero sintattico per una macchina di Turing. D'altra parte, se si dispone di una semantica in cui è possibile parlare di oggetti e determinate operazioni su di essi, la rimozione di una certa sintassi non consentirà più di esprimere tali operazioni, anche se la lingua potrebbe ancora essere Turing completa.
Giorgio,

13

Il termine zucchero sintattico si riferisce in genere ai casi in cui la funzionalità è definita da una sostituzione. La lingua non definisce ciò che fa una funzione, ma definisce che è esattamente equivalente a qualcos'altro. Ad esempio, per ogni loop

for(Object alpha: alphas) {
}

diventa:

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

O accetta una funzione con argomenti variabili:

void foo(int... args);

foo(3, 4, 5);

Che diventa:

void Foo(int[] args);

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

Quindi c'è una banale sostituzione della sintassi per implementare la funzionalità in termini di altre funzionalità.

Diamo un'occhiata al sovraccarico del metodo.

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

foo(4.5);

Questo può essere riscritto come:

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

foo_double(4.5);

Ma non è equivalente a quello. Nel modello di Java, questo è qualcosa di diverso. foo(int a)non implementa una foo_intfunzione da creare. Java non implementa il sovraccarico del metodo assegnando a nomi ambigui funzioni ambigue. Per contare come zucchero sintattico, Java dovrebbe far finta di aver davvero scritto foo_inte foo_doublefunzioni ma non lo fa.


2
Non credo che nessuno abbia mai detto che la trasformazione dello zucchero sintattico debba essere banale. Anche se lo facesse, trovo l'affermazione But, the transformation isn't trivial. At the least, you have to determine the types of the parameters.molto imprecisa perché non è necessario determinare i tipi ; sono noti al momento della compilazione.
Doval,

3
"Per contare come zucchero sintattico, Java dovrebbe fingere di aver davvero scritto le funzioni foo_int e foo_double ma non lo fa." - fintanto che parliamo di metodi di sovraccarico e non di polimorfismi quale sarebbe la differenza tra foo(int)/ foo(double)e foo_int/ foo_double? Non conosco molto bene Java, ma immagino che tale ridenominazione avvenga davvero in JVM (beh - probabilmente usando foo(args)piuttosto allora foo_args- lo fa almeno in C ++ con la manipolazione dei simboli (ok - la manipolazione dei simboli è tecnicamente un dettaglio di implementazione e non parte di lingua).
Maciej Piechotka,

2
@Doval: "Non credo che nessuno abbia mai detto che la trasformazione dello zucchero sintattico debba essere banale." - Vero, ma deve essere locale . L'unica definizione utile di zucchero sintattico che conosco proviene dal famoso articolo di Matthias Felleisen sull'espressività del linguaggio, e in sostanza dice che se riesci a riscrivere un programma scritto in lingua L + y (cioè una lingua L con qualche caratteristica y ) in la lingua L (cioè un sottoinsieme di quella lingua senza la funzione y ) senza cambiare la struttura globale del programma (cioè solo apportando modifiche locali), allora y è zucchero sintattico in L + y e lo fa
Jörg W Mittag,

2
... non aumentare l'espressività di L. Tuttavia, se non è possibile farlo, vale a dire se è necessario apportare modifiche alla struttura globale del programma, allora è non è zucchero sintattico e lo fa in effetti make L + y più espressivo di L . Ad esempio, Java con forloop avanzato non è più espressivo di Java senza di esso. (È più bello, più conciso, più leggibile e tutt'altro che meglio, direi, ma non più espressivo.) Non sono sicuro del caso di sovraccarico, però. Probabilmente dovrò rileggere il documento per essere sicuro. Il mio intestino dice che è zucchero sintattico, ma non ne sono sicuro.
Jörg W Mittag,

2
@MaciejPiechotka, se facesse parte della definizione linguistica che le funzioni erano così rinominate, e potevi accedere alla funzione con quei nomi, penso che sarebbe zucchero sintattico. Ma poiché è nascosto come un dettaglio di implementazione, penso che lo squalifica dallo zucchero sintattico.
Winston Ewert,

8

Dato che il nome che ruba funziona, non deve essere altro che lo zucchero sintattico?

Permette al chiamante di immaginare di chiamare la stessa funzione, quando non lo è. Ma poteva conoscere i veri nomi di tutte le sue funzioni. Solo se fosse possibile ottenere un polimorfismo ritardato trasferendo una variabile non tipizzata in una funzione tipizzata e averne il tipo stabilito in modo che la chiamata possa passare alla versione corretta in base al nome, questa sarebbe una caratteristica del linguaggio vero.

Sfortunatamente, non ho mai visto una lingua farlo. Quando c'è ambiguità, questi compilatori non lo risolvono, insistono che lo scrittore lo risolva per loro.


La funzione che stai cercando si chiama "Invio multiplo". Molte lingue lo supportano tra cui Haskell, Scala e (dal 4.0) C #.
Iain Galloway,

Vorrei separare i parametri sulle classi dal sovraccarico del metodo straight. Nel caso di sovraccarico del metodo straight, il programmatore scrive tutte le versioni, il compilatore sa solo come sceglierne una. Questo è solo zucchero sintattico ed è risolto da una semplice modifica del nome, anche per spedizioni multiple. --- In presenza di parametri sulle classi, il compilatore genera il codice secondo necessità e ciò lo modifica completamente.
Jon Jay Obermark,

2
Penso che tu abbia frainteso. Ad esempio, in C #, se uno dei parametri di un metodo è dynamicquindi la risoluzione di sovraccarico si verifica in fase di esecuzione, non in fase di compilazione . Questo è il dispacciamento multiplo e non può essere replicato dalle funzioni di ridenominazione.
Iain Galloway,

Abbastanza fico. Riesco comunque a testare il tipo variabile, quindi questa è ancora solo una funzione integrata sovrapposta allo zucchero sintattico. È una funzione linguistica, ma a malapena.
Jon Jay Obermark,

7

A seconda della lingua, è zucchero sintattico o no.

In C ++ per esempio, puoi fare cose usando overload e template che non sarebbero possibili senza complicazioni (scrivi manualmente tutte le istanze del template o aggiungi molti parametri template).

Si noti che il dispacciamento dinamico è una forma di sovraccarico, risolto dinamicamente su alcuni parametri (per alcune lingue solo una speciale, questa , ma non tutte le lingue sono così limitate), e non chiamerei quella forma di sovraccarico di zucchero sintattico.


Non sono sicuro di come le altre risposte stiano andando molto meglio se sostanzialmente errate.
Telastyn,

5

Per le lingue contemporanee, è solo zucchero sintattico; in un modo completamente indipendente dal linguaggio, è più di questo.

In precedenza questa risposta affermava semplicemente che è più che zucchero sintattico, ma se vedrai nei commenti, Falco ha sollevato il punto che c'era un pezzo del puzzle che le lingue contemporanee sembrano mancare tutti; non mischiano l'overload del metodo con la determinazione dinamica di quale funzione chiamare nello stesso passaggio. Questo sarà chiarito in seguito.

Ecco perché dovrebbe essere di più.

Prendi in considerazione un linguaggio che supporti sia il sovraccarico del metodo sia le variabili non tipizzate. Potresti avere i seguenti prototipi di metodo:

bool someFunction(int arg);

bool someFunction(string arg);

In alcune lingue, verrai probabilmente rassegnato a sapere in fase di compilazione quale di queste verrebbe chiamata da una determinata riga di codice. Ma in alcune lingue, non tutte le variabili vengono digitate (o tutte implicitamente digitate come Objecto qualsiasi altra cosa), quindi immagina di costruire un dizionario le cui chiavi siano associate a valori di diversi tipi:

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.

E allora, se volessi fare domanda someFunctionper uno di quei numeri di stanza? Tu lo chiami:

someFunction(roomNumber[someSortOfKey]);

Si someFunction(int)chiama o si someFunction(string)chiama? Qui puoi vedere un esempio in cui questi non sono metodi totalmente ortogonali, specialmente nei linguaggi di livello superiore. Il linguaggio deve capire - durante il runtime - quale di questi chiamare, quindi deve ancora considerarli almeno un po 'lo stesso metodo.

Perché non usare semplicemente i modelli? Perché non usare semplicemente un argomento non tipizzato?

Flessibilità e controllo più accurato. A volte l'uso di template / argomenti non tipizzati è un approccio migliore, ma a volte no.

Devi pensare a casi in cui, ad esempio, potresti avere due firme di metodo che prendono ciascuno un inte un stringcome argomenti, ma in cui l'ordine è diverso in ciascuna firma. Potresti benissimo avere una buona ragione per farlo, dato che l'implementazione di ciascuna firma può fare sostanzialmente la stessa cosa, ma con un tocco leggermente diverso; la registrazione potrebbe essere diversa, ad esempio. O anche se fanno la stessa cosa esatta, potresti essere in grado di raccogliere automaticamente determinate informazioni solo dall'ordine in cui sono stati specificati gli argomenti. Tecnicamente potresti semplicemente usare le istruzioni pseudo-switch per determinare il tipo di ciascuno degli argomenti passati, ma diventa disordinato.

Quindi questo prossimo esempio è una cattiva pratica di programmazione?

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;
    }
}

Sì, nel complesso. In questo esempio particolare, potrebbe impedire a qualcuno di provare ad applicarlo a determinati tipi primitivi e di ottenere comportamenti inaspettati (il che potrebbe essere una buona cosa); ma supponiamo che io abbia abbreviato il codice sopra e che tu, in effetti, hai un sovraccarico per tutti i tipi primitivi, così come per Objects. Quindi questo prossimo bit di codice è davvero più appropriato:

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

Ma cosa succede se avete solo bisogno di utilizzare questo per ints e strings, e che cosa se si vuole che ritorni vero in base alle condizioni più semplici o più complicate di conseguenza? Quindi hai una buona ragione per usare il sovraccarico:

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;
    }
}

Ma hey, perché non dare a queste funzioni due nomi diversi? Hai ancora la stessa quantità di controllo a grana fine, vero?

Perché, come affermato prima, alcuni hotel usano numeri, alcuni usano lettere e altri usano una combinazione di numeri e lettere:

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

Questo non è ancora esattamente lo stesso codice esatto che userei nella vita reale, ma dovrebbe illustrare il punto che sto facendo proprio bene.

Ma ... Ecco perché non è altro che zucchero sintattico nelle lingue contemporanee.

Falco ha sollevato il punto nei commenti secondo cui le lingue attuali fondamentalmente non mescolano il sovraccarico dei metodi e la selezione dinamica delle funzioni all'interno dello stesso passo. Il modo in cui avevo precedentemente compreso il funzionamento di alcune lingue era che potresti sovraccaricare appearsToBeFirstFloornell'esempio sopra, e quindi la lingua avrebbe determinato in fase di esecuzione quale versione della funzione da chiamare, a seconda del valore di runtime della variabile non tipizzata. Questa confusione derivava in parte dal lavorare con una specie di linguaggi ECMA, come ActionScript 3.0, in cui è possibile randomizzare facilmente quale funzione viene chiamata su una determinata riga di codice in fase di esecuzione.

Come forse saprai, ActionScript 3 non supporta il sovraccarico del metodo. Per quanto riguarda VB.NET, puoi dichiarare e impostare variabili senza assegnare esplicitamente un tipo, ma quando provi a passare queste variabili come argomenti a metodi sovraccarichi, non vuole ancora leggere il valore di runtime per determinare quale metodo chiamare; vuole invece trovare un metodo con argomenti di tipo Objecto nessun tipo o qualcos'altro del genere. Quindi l' esempio intvs. stringsopra non funzionerebbe neanche in quella lingua. Il C ++ ha problemi simili, come quando usi qualcosa come un puntatore vuoto o qualche altro meccanismo del genere, ti richiede comunque di chiarire manualmente il tipo al momento della compilazione.

Quindi, come dice la prima intestazione ...

Per le lingue contemporanee, è solo zucchero sintattico; in un modo completamente indipendente dal linguaggio, è più di questo. Rendere il sovraccarico del metodo più utile e pertinente, come nell'esempio sopra, potrebbe effettivamente essere una buona caratteristica da aggiungere a una lingua esistente (come è stato ampiamente implicitamente richiesto per AS3), oppure potrebbe anche servire come uno tra molti diversi pilastri fondamentali per la creazione di un nuovo linguaggio procedurale / orientato agli oggetti.


3
Riesci a nominare le lingue che gestiscono davvero Function-Dispatch in fase di esecuzione e non in fase di compilazione? TUTTE le lingue che conosco richiedono la certezza durante la compilazione di quale funzione è chiamata ...
Falco,

@Falco ActionScript 3.0 lo gestisce in fase di esecuzione. Si potrebbe, per esempio, utilizzare una funzione che restituisce una delle tre corde a caso, e quindi utilizzare il suo valore di ritorno di chiamare uno qualsiasi dei tre funzioni a caso: this[chooseFunctionNameAtRandom](); se chooseFunctionNameAtRandom()torna o "punch", "kick"o "dodge", allora si può thusly implementare un caso molto semplice elemento, ad esempio, nell'IA di un nemico in un gioco Flash.
Panzercrisis,

1
Sì, ma entrambi sono veri e propri metodi semantici per ottenere l'invio dinamico di funzioni, anche Java ha questi. Ma sono diversi dal sovraccarico, il sovraccarico è statico e solo zucchero sintattico, mentre l'invio e l'ereditarietà dinamica sono caratteristiche del linguaggio reale, che offrono nuove funzionalità!
Falco,

1
... Ho anche provato i puntatori vuoti in C ++, così come i puntatori della classe base, ma il compilatore voleva che lo chiarissi da solo prima di passarlo a una funzione. Quindi ora mi chiedo se eliminare questa risposta. Comincia a sembrare che le lingue quasi sempre si avvicinino alla combinazione della scelta dinamica della funzione con il sovraccarico della funzione nella stessa istruzione o istruzione, ma poi si allontanano all'ultimo secondo. Sarebbe comunque una bella funzione linguistica; forse qualcuno ha bisogno di creare una lingua che abbia questo.
Panzercrisis,

1
Lascia che la risposta rimanga, forse pensi di includere alcune delle tue ricerche dai commenti nella risposta?
Falco,

2

Dipende molto dalla tua definizione di "zucchero sintattico". Proverò ad affrontare alcune delle definizioni che mi vengono in mente:

  1. Una funzione è zucchero sintattico quando un programma che la utilizza può sempre essere tradotto in un altro che non utilizza la funzione.

    Qui supponiamo che esista un insieme primitivo di funzioni che non possono essere tradotte: in altre parole, nessun loop del tipo "puoi sostituire la funzione X usando la funzione Y" e "puoi sostituire la funzione Y con la funzione X". Se uno dei due è vero rispetto all'altra, può essere espressa in termini di caratteristiche che non sono le prime oppure è una funzione primitiva.

  2. Come nella definizione 1, ma con il requisito aggiuntivo che il programma tradotto sia sicuro per i tipi come il primo, ovvero desugarando non perdi alcun tipo di informazione.

  3. La definizione del PO: una caratteristica è lo zucchero sintattico se la sua traduzione non cambia la struttura del programma ma richiede solo "cambiamenti locali".

Prendiamo Haskell come esempio per il sovraccarico. Haskell fornisce un sovraccarico definito dall'utente tramite classi di tipi. Ad esempio, le operazioni +e *sono definite nella Numclasse type e qualsiasi tipo che ha un'istanza (completa) di tale classe può essere usato con +. Per esempio:

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

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

Una cosa ben nota delle classi di tipi di Haskell è che puoi sbarazzartene . Vale a dire che puoi tradurre qualsiasi programma che utilizza le classi di tipi in un programma equivalente che non le utilizza.

La traduzione è abbastanza semplice:

  • Data una definizione di classe:

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

    Puoi tradurlo in un tipo di dati algebrico:

    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
    }
    

    Qui X_P_ie X_op_isono i selettori . Vale a dire un valore di tipo che si X aapplica X_P_1al valore restituirà il valore memorizzato in quel campo, quindi sono funzioni con tipo X a -> P_i a(o X a -> t_i).

    Per un'anologia molto approssimativa potresti pensare ai valori per type X acome structse quindi se xè di tipo X ale espressioni:

    X_P_1 x
    X_op_1 x
    

    potrebbe essere visto come:

    x.X_P_1
    x.X_op_1
    

    (È facile usare solo i campi posizionali invece dei campi con nome, ma i campi con nome sono più facili da gestire negli esempi ed evitano il codice della piastra della caldaia).

  • Data una dichiarazione di istanza:

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

    È possibile tradurlo in una funzione che, dati i dizionari per le C_1 a_1, ..., C_n a_nclassi, restituisce un valore di dizionario (ovvero un valore di tipo X a) per il tipo T a_1 ... a_n.

    In altre parole, l'istanza sopra può essere tradotta in una funzione come:

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

    (Nota che npotrebbe essere 0).

    E infatti possiamo definirlo come:

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

    dove op_1 = ...a op_m = ...sono le definizioni fornite nella instancedichiarazione e get_P_i_Tsono le funzioni definite dal P_iistanza del Ttipo (questi devono esistono perché P_is sono superclassi di X).

  • Data una chiamata a una funzione sovraccarica:

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

    Possiamo esplicitamente passare i dizionari in relazione ai vincoli di classe e ottenere una chiamata equivalente:

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

    Nota come i vincoli di classe sono diventati semplicemente un nuovo argomento. Il +programma tradotto è il selettore come spiegato prima. In altre parole, la addfunzione tradotta , dato il dizionario per il tipo del suo argomento, prima "spacchetterà" la funzione effettiva per calcolare il risultato usando (+) dictNume poi applicherà questa funzione agli argomenti.

Questo è solo uno schizzo molto veloce di tutto. Se sei interessato, leggi gli articoli di Simon Peyton Jones et al.

Credo che un approccio simile potrebbe essere usato per sovraccaricare anche in altre lingue.

Tuttavia, ciò dimostra che, se la definizione di zucchero sintattico è (1), il sovraccarico è lo zucchero sintattico . Perché puoi liberartene.

Tuttavia, il programma tradotto perde alcune informazioni sul programma originale. Ad esempio, non impone l'esistenza delle istanze per le classi parent. (Anche se le operazioni per estrarre i dizionari del genitore devono essere ancora di quel tipo, puoi passare undefinedo altri valori polimorfici in modo da poter costruire un valore X ysenza costruire i valori per P_i y, quindi la traduzione non perde tutto il tipo di sicurezza). Quindi non è zucchero sintattico secondo (2)

Per quanto riguarda (3). Non so se la risposta dovrebbe essere un sì o un no.

Direi di no perché, ad esempio, una dichiarazione di istanza diventa una definizione di funzione. Le funzioni sovraccaricate ottengono un nuovo parametro (il che significa che cambia sia la definizione che tutte le chiamate).

Direi di sì perché i due programmi sono ancora mappati uno a uno, quindi la "struttura" non è cambiata molto.


Detto questo, direi che i vantaggi pragmatici introdotti dal sovraccarico sono così grandi che l'uso di un termine "dispregiativo" come "zucchero sintattico" non sembra corretto.

Puoi tradurre tutta la sintassi di Haskell in un linguaggio Core molto semplice (che in realtà viene fatto durante la compilazione), quindi la maggior parte della sintassi di Haskell potrebbe essere vista come "zucchero sintattico" per qualcosa che è solo lambda-calcolo più un po 'di nuovi costrutti. Tuttavia possiamo concordare sul fatto che i programmi Haskell sono molto più facili da gestire e molto concisi, mentre i programmi tradotti sono piuttosto difficili da leggere o da pensare.


2

Se l'invio viene risolto in fase di compilazione, in base solo al tipo statico dell'espressione dell'argomento, si può certamente sostenere che si tratta di "zucchero sintattico" che sostituisce due metodi diversi con nomi diversi, a condizione che il programmatore "conosca" il tipo statico e potrebbe semplicemente usare il nome del metodo corretto al posto del nome sovraccarico. È anche una forma di polimorfismo statico, ma in quella forma limitata di solito non è molto potente.

Ovviamente sarebbe fastidioso cambiare i nomi dei metodi che chiami ogni volta che cambi il tipo di una variabile, ma ad esempio nel linguaggio C è considerato un fastidio gestibile, quindi C non ha un sovraccarico di funzioni (sebbene ora ha macro generiche).

Nei modelli C ++ e in qualsiasi linguaggio che deduca un tipo statico non banale, non si può davvero sostenere che si tratta di "zucchero sintatico" a meno che non si sostenga anche che la detrazione di tipo statico è "zucchero sintattico". Sarebbe una seccatura non avere modelli, e nel contesto di C ++ sarebbe una "seccatura ingestibile", dal momento che sono così idiomatici con il linguaggio e le sue librerie standard. Quindi in C ++ è piuttosto più di un buon aiuto, è importante per lo stile del linguaggio, e quindi penso che tu debba chiamarlo più di "zucchero sintattico".

In Java potresti considerarlo più di una semplice comodità considerando ad esempio quanti sovraccarichi ci sono di PrintStream.printe PrintStream.println. Ma poi ce ne sono altrettantiDataInputStream.readX metodi poiché Java non sovraccarica il tipo restituito, quindi in un certo senso è solo per comodità. Questi sono tutti per i tipi primitivi.

Non ricordo che cosa accade in Java se ho le classi Aed Bestende O, io sovraccaricare i metodi foo(O), foo(A)e foo(B), e poi in un generico con <T extends O>chiamo foo(t)dove tè un'istanza di T. Nel caso in cui Tè Aposso ottenere la spedizione sulla base del sovraccarico o è come se ho chiamato foo(O)?

Se il primo, i sovraccarichi del metodo Java sono migliori dello zucchero allo stesso modo dei sovraccarichi C ++. Usando la tua definizione, suppongo che in Java potrei scrivere localmente una serie di controlli di tipo (che sarebbe fragile, perché nuovi sovraccarichi foorichiederebbero controlli aggiuntivi). A parte accettare questa fragilità, non posso fare una modifica locale sul sito della chiamata per farlo bene, invece dovrei rinunciare a scrivere codice generico. Direi che prevenire il codice gonfio potrebbe essere zucchero sintattico, ma prevenire il codice fragile è più di questo. Per questo motivo, il polimorfismo statico in generale è molto più di un semplice zucchero sintattico. La situazione in una determinata lingua potrebbe essere diversa, a seconda di quanto la lingua ti consente di "non conoscere" il tipo statico.


In Java, i sovraccarichi vengono risolti al momento della compilazione. Dato l'uso della cancellazione del tipo, sarebbe impossibile per loro essere diversamente. Inoltre, anche senza cancellazione del tipo, se T:Animalè è tipo SiameseCate sovraccarichi esistenti Cat Foo(Animal), SiameseCat Foo(Cat)e Animal Foo(SiameseCat)che il sovraccarico dovrebbero essere selezionati se Tè SiameseCat?
supercat,

@supercat: ha senso. Quindi avrei potuto capire la risposta senza ricordare (o, ovviamente, eseguirla). Pertanto, i sovraccarichi Java non sono migliori dello zucchero nello stesso modo in cui i sovraccarichi C ++ si riferiscono al codice generico. Resta possibile che ci sia un altro modo in cui sono migliori di una semplice trasformazione locale. Mi chiedo se dovrei cambiare il mio esempio in C ++ o lasciarlo come un po 'immaginato-Java-che-non-è-reale-Java.
Steve Jessop,

I sovraccarichi possono essere utili nei casi in cui i metodi hanno argomenti opzionali, ma possono anche essere pericolosi. Supponiamo che la riga long foo=Math.round(bar*1.0001)*5venga cambiata in long foo=Math.round(bar)*5. In che modo ciò influirebbe sulla semantica se baruguale, ad esempio, 123456789L?
supercat

@supercat Direi che il vero pericolo è la conversione implicita da longa double.
Doval,

@Doval: To double?
supercat,

1

Sembra che lo "zucchero sintattico" sembri dispregiativo, inutile o frivolo. Ecco perché la domanda innesca molte risposte negative.

Ma hai ragione, il sovraccarico del metodo non aggiunge alcuna funzionalità alla lingua tranne la possibilità di utilizzare lo stesso nome per metodi diversi. È possibile rendere esplicito il tipo di parametro, il programma continuerà a funzionare allo stesso modo.

Lo stesso vale per i nomi dei pacchetti. String è solo zucchero sintattico per java.lang.String.

In effetti, un metodo simile

void fun(int i, String c);

in classe MyClass dovrebbe essere chiamato qualcosa del tipo "my_package_MyClass_fun_int_java_lang_String". Ciò identificherebbe il metodo in modo univoco. (La JVM fa qualcosa di simile internamente). Ma non vuoi scriverlo. Ecco perché il compilatore ti permetterà di scrivere in modo divertente (1, "uno") e di identificare quale metodo è.

C'è comunque una cosa che puoi fare con il sovraccarico: se sovraccarichi un metodo con lo stesso numero di argomenti, il compilatore scoprirà automaticamente quale versione si adatta meglio all'argomento fornito dagli argomenti corrispondenti, non solo con tipi uguali, ma anche dove l'argomento dato è una sottoclasse dell'argomento dichiarato.

Se hai due procedure sovraccaricate

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

non è necessario sapere che esiste una versione specifica della procedura per Date. addParameter ("hello", "world) chiamerà la prima versione, addParameter (" now ", new Date ()) chiamerà la seconda.

Ovviamente, dovresti evitare di sovraccaricare un metodo con un altro metodo che fa una cosa completamente diversa.


1

È interessante notare che la risposta a questa domanda dipenderà dalla lingua.

In particolare, esiste un'interazione tra sovraccarico e programmazione generica (*) e, a seconda dell'implementazione della programmazione generica, potrebbe essere solo zucchero sintattico (Rust) o assolutamente necessario (C ++).

Cioè, quando la programmazione generica viene implementata con interfacce esplicite (in Rust o Haskell, quelle sarebbero classi di tipo), quindi il sovraccarico è solo zucchero sintattico; o in realtà potrebbe anche non essere parte della lingua.

D'altra parte, quando la programmazione generica è implementata con la tipizzazione anatra (sia essa dinamica o statica), il nome del metodo è un contratto essenziale e quindi il sovraccarico è obbligatorio per il funzionamento del sistema.

(*) Usato nel senso di scrivere un metodo una volta, per operare su vari tipi in modo uniforme.


0

In alcune lingue è senza dubbio solo zucchero sintattico. Tuttavia, ciò che è uno zucchero dipende dal tuo punto di vista. Lascerò questa discussione per più avanti in questa risposta.

Per ora vorrei solo notare che in alcune lingue non è certamente zucchero sintattico. Almeno non senza la necessità di utilizzare una logica / algoritmo completamente diverso per implementare la stessa cosa. È come affermare che la ricorsione è zucchero sintattico (che è dato che puoi scrivere tutti gli algoritmi ricorsivi con un ciclo e uno stack).

Un esempio di utilizzo molto difficile da sostituire viene da un linguaggio che ironicamente non definisce questa funzione "sovraccarico di funzioni". Invece si chiama "pattern matching" (che può essere visto come un superset di sovraccarico perché possiamo sovraccaricare non solo tipi ma valori).

Ecco la classica ingenua implementazione della funzione Fibonacci in Haskell:

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

Probabilmente le tre funzioni possono essere sostituite con un if / else come è comunemente fatto in qualsiasi altra lingua. Ma ciò rende fondamentalmente la definizione assolutamente semplice:

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

molto più disordinato e non esprime direttamente la nozione matematica della sequenza di Fibonacci.

Quindi a volte può essere zucchero di sintassi se l'unico uso è quello di consentire di chiamare una funzione con argomenti diversi. Ma a volte è molto più fondamentale di così.


Ora per il discssion di ciò che il sovraccarico dell'operatore può essere uno zucchero per. Hai identificato un caso d'uso: può essere utilizzato per implementare funzioni simili che accettano argomenti diversi. Così:

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

in alternativa può essere implementato come:

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

o anche:

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

Ma il sovraccarico dell'operatore può anche essere uno zucchero per l'implementazione di argomenti facoltativi (alcune lingue hanno un sovraccarico dell'operatore ma non argomenti facoltativi):

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

può essere utilizzato per implementare:

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

In una tale lingua (google "lingua ferita") la rimozione del sovraccarico dell'operatore rimuove drasticamente una caratteristica: argomenti opzionali. Concesso in lingue con entrambe le funzionalità (c ++) rimuovendo l'una o l'altra non avrà alcun effetto netto poiché entrambi possono essere utilizzati per implementare argomenti opzionali.


Haskell è un buon esempio del motivo per cui il sovraccarico dell'operatore non è lo zucchero sintattico, ma penso che un esempio migliore sarebbe la decostruzione di un tipo di dati algebrico con il pattern matching (qualcosa che per quanto ne so impossibile senza il pattern matching).
11684,

@ 11684: puoi indicare un esempio? Sinceramente non conosco affatto Haskell, ma ho trovato il suo schema abbinabile in modo sublime ed elegante quando ho visto quell'esempio fib (su computerphile su youtube).
Slebetman,

Dato un tipo di dati come quello data PaymentInfo = CashOnDelivery | Adress String | UserInvoice CustomerInfoche puoi modellare la corrispondenza sui costruttori di tipi.
11684,

Come questo: 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. Spero che questo commento sia in qualche modo comprensibile.
11684,

0

Penso che sia semplice zucchero sintattico nella maggior parte delle lingue (almeno tutto quello che so ...) poiché tutti richiedono una chiamata di funzione non ambigua al momento della compilazione. E il compilatore sostituisce semplicemente la chiamata di funzione con un puntatore esplicito alla firma dell'implementazione corretta.

Esempio in Java:

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

Quindi alla fine potrebbe essere completamente sostituito da una semplice macro-compilatore con ricerca e sostituzione, sostituendo il mangle Function sovraccaricato con mangle_String e mangle_int - poiché l'elenco degli argomenti fa parte dell'eventuale identificatore di funzioni, questo è praticamente ciò che accade -> e quindi è solo zucchero sintattico.

Ora, se esiste un linguaggio, in cui la funzione è realmente determinata in fase di esecuzione, come con i metodi ignorati negli oggetti, questo sarebbe diverso. Ma non credo che esista un linguaggio del genere, poiché method.overloading è soggetto ad ambiguità, che il compilatore non può risolvere e che deve essere gestito dal programmatore con un cast esplicito. Questo non può essere fatto in fase di esecuzione.


0

Nel tipo Java le informazioni sono compilate e quale dei sovraccarichi viene chiamato decide al momento della compilazione.

Di seguito è riportato un frammento di sun.misc.Unsafe(l'utilità per Atomics) come visualizzato nell'editor di file di classe di 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]

come puoi vedere, le informazioni sul tipo del metodo chiamato (linea 4) sono incluse nella chiamata.

Ciò significa che è possibile creare un compilatore Java che accetta informazioni sul tipo. Ad esempio, usando una tale notazione, la fonte di cui sopra sarebbe quindi:

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

e il cast a lungo sarebbe facoltativo.

In altri linguaggi compilati digitati staticamente vedrai un'impostazione simile in cui il compilatore deciderà quale sovraccarico verrà chiamato in base ai tipi e lo includerà nell'associazione / chiamata.

L'eccezione sono le librerie dinamiche C in cui le informazioni sul tipo non sono incluse e il tentativo di creare una funzione sovraccaricata farà lamentare il linker.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.