Perché non è consentito l'overloading con i tipi restituiti? (almeno nelle lingue normalmente utilizzate)


9

Non conosco tutti i linguaggi di programmazione, ma è chiaro che di solito la possibilità di sovraccaricare un metodo prendendo in considerazione il suo tipo restituito (supponendo che i suoi argomenti siano dello stesso numero e tipo) non è supportata.

Intendo qualcosa del genere:

 int method1 (int num)
 {

 }
 long method1 (int num)
 {

 }

Non è che si tratti di un grosso problema per la programmazione, ma in alcune occasioni lo accetterei.

Ovviamente non ci sarebbe modo per quelle lingue di supportarlo senza un modo per differenziare quale metodo viene chiamato, ma la sintassi può essere semplice come qualcosa come [int] method1 (num) o [long] method1 (num) in questo modo il compilatore avrebbe saputo quale sarebbe stato quello che sarebbe stato chiamato.

Non so come funzionano i compilatori, ma questo non sembra essere così difficile da fare, quindi mi chiedo perché qualcosa del genere di solito non sia implementato.

Quali sono i motivi per cui qualcosa del genere non è supportato?


Forse la tua domanda sarebbe migliore con un esempio in cui non esistono conversioni implicite tra i due tipi di ritorno, ad esempio classi Fooe Bar.

Bene, perché una tale funzionalità sarebbe utile?
James Youngman,

3
@JamesYoungman: ad esempio per analizzare le stringhe in diversi tipi, è possibile avere un metodo int read (String s), float read (String s) e così via. Ogni variazione sovraccarica del metodo esegue un'analisi per il tipo appropriato.
Giorgio,

A proposito, questo è solo un problema con le lingue tipizzate staticamente. Avere più tipi di ritorno è piuttosto normale in linguaggi tipicamente dinamici come Javascript o Python.
Gort il robot il

1
@StevenBurnap, um, no. Con, ad esempio, JavaScript, non puoi assolutamente sovraccaricare le funzioni. Quindi questo è in realtà solo un problema con le lingue che supportano il sovraccarico del nome della funzione.
David Arno,

Risposte:


19

Ciò complica il controllo del tipo.

Quando si consente solo il sovraccarico in base ai tipi di argomento e si consente di dedurre solo i tipi di variabili dai loro inizializzatori, tutte le informazioni sui tipi scorrono in una direzione: l'albero della sintassi.

var x = f();

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

Quando si consente alle informazioni di tipo di viaggiare in entrambe le direzioni, ad esempio deducendo il tipo di una variabile dal suo utilizzo, è necessario un risolutore di vincoli (come Algorithm W, per i sistemi di tipo Hindley-Milner) per determinare il 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]

Qui, dovevamo lasciare il tipo di xuna variabile di tipo non risolta ∃T, dove tutto ciò che sappiamo è che è analizzabile. Solo in seguito, quando xviene utilizzato in un tipo concreto, disponiamo di informazioni sufficienti per risolvere il vincolo e determinare quello ∃T = int, che propaga le informazioni sul tipo nella struttura della sintassi dall'espressione della chiamata in x.

Se non riuscissimo a determinare il tipo di x, o anche questo codice verrebbe sovraccaricato (quindi il chiamante determinerebbe il tipo) o dovremmo segnalare un errore sull'ambiguità.

Da questo, un designer linguistico può concludere:

  • Aggiunge complessità all'implementazione.

  • Rende più lento il controllo dei caratteri, in casi patologici, in modo esponenziale.

  • È più difficile produrre buoni messaggi di errore.

  • È troppo diverso dallo status quo.

  • Non mi va di implementarlo.


1
Inoltre: sarà così difficile capire che in alcuni casi il tuo compilatore farà delle scelte che il programmatore non si aspettava assolutamente, portando a trovare bug difficili.
gnasher729,

1
@ gnasher729: non sono d'accordo. Uso regolarmente Haskell, che ha questa funzione, e non sono mai stato morso dalla sua scelta di sovraccarico (vale a dire istanza di typeclass). Se qualcosa è ambiguo, mi impone solo di aggiungere un'annotazione di tipo. E ho ancora scelto di implementare l'inferenza del tipo completo nella mia lingua perché è incredibilmente utile. Questa risposta è stata la mia interpretazione dell'avvocato del diavolo.
Jon Purdy,

4

Perché è ambiguo. Usando C # come esempio:

var foo = method(42);

Quale sovraccarico dovremmo usare?

Ok, forse è stato un po 'ingiusto. Il compilatore non può capire quale tipo usare se non lo diciamo nel tuo linguaggio ipotetico. Quindi, la digitazione implicita è impossibile nella tua lingua e ci sono metodi anonimi e Linq insieme ad essa ...

Che ne dici di questo? (Firme leggermente ridefinite per illustrare il punto.)

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

....

long foo = method(42);

Dovremmo usare il intsovraccarico o il shortsovraccarico? Semplicemente non lo sappiamo, dovremo specificarlo con la tua [int] method1(num)sintassi. Che è un po 'una seccatura analizzare e scrivere per essere onesti.

long foo = [int] method(42);

Il fatto è che è una sintassi sorprendentemente simile a un metodo generico in C #.

long foo = method<int>(42);

(C ++ e Java hanno funzionalità simili.)

In breve, i progettisti linguistici hanno scelto di risolvere il problema in modo diverso al fine di semplificare l'analisi e abilitare funzionalità linguistiche molto più potenti.

Dici di non sapere molto dei compilatori. Consiglio vivamente di imparare grammatiche e parser. Una volta capito cos'è una grammatica libera dal contesto, avrai un'idea molto migliore del perché l'ambiguità sia una cosa negativa.


Un buon punto sui generici, sebbene sembri essere conflittuale shorte int.
Robert Harvey,

Sì, @RobertHarvey, hai ragione. Stavo lottando per un esempio per illustrare il punto. Funzionerebbe meglio se methodrestituito short o int e il tipo fosse definito long.
RubberDuck,

Sembra un po 'meglio.
RubberDuck,

Non compro le tue argomentazioni secondo cui non puoi avere deduzioni di tipo, subroutine anonime o comprensioni monadiche in una lingua con sovraccarico del tipo di ritorno. Haskell lo fa e Haskell ne ha tutti e tre. Ha anche polimorfismo parametrico. Il tuo punto su long/ int/ shortriguarda più la complessità del sottotipo e / o le conversioni implicite piuttosto che il sovraccarico del tipo di ritorno. Dopotutto, i letterali numerici sono sovraccarichi sul loro tipo di ritorno in C ++, Java, C♯ e molti altri, e ciò non sembra presentare un problema. Potresti semplicemente creare una regola: ad esempio, scegli il tipo più specifico / generale.
Jörg W Mittag,

@ JörgWMittag il punto non era che lo avrebbe reso impossibile, solo che rendeva le cose inutilmente complesse.
RubberDuck,

0

Tutte le funzionalità del linguaggio aggiungono complessità, quindi devono offrire vantaggi sufficienti per giustificare gli inevitabili gotcha, casi angolari e la confusione dell'utente che ogni funzionalità crea. Per la maggior parte delle lingue, questo semplicemente non fornisce abbastanza benefici per giustificarlo.

Nella maggior parte delle lingue, ci si aspetterebbe che l'espressione method1(2)abbia un tipo definito e un valore di ritorno più o meno prevedibile. Ma se si consente il sovraccarico dei valori di ritorno, ciò significa che è impossibile dire cosa significa quell'espressione in generale senza considerare il contesto circostante. Considera cosa succede quando hai un unsigned long long foo()metodo la cui implementazione termina return method1(2)? Questo dovrebbe chiamare il longsovraccarico di ritorno o il intsovraccarico di ritorno o semplicemente dare un errore del compilatore?

Inoltre, se devi aiutare il compilatore annotando il tipo restituito, non solo stai inventando una maggiore sintassi (che aumenta tutti i costi di cui sopra per consentire l'esistenza della funzione), ma stai effettivamente facendo la stessa cosa della creazione due metodi con nomi diversi in un linguaggio "normale". È [long] method1(2)più intuitivo di long_method1(2)?


D'altra parte, alcuni linguaggi funzionali come Haskell con sistemi di tipo statico molto potenti consentono questo tipo di comportamento, poiché la loro inferenza di tipo è abbastanza potente che raramente è necessario annotare il tipo restituito in quelle lingue. Ma questo è possibile solo perché quelle lingue applicano davvero la sicurezza dei tipi, più di qualsiasi altra lingua tradizionale, oltre a richiedere che tutte le funzioni siano pure e referenzialmente trasparenti. Non è qualcosa che sarebbe mai possibile nella maggior parte delle lingue OOP.


2
"Oltre a richiedere che tutte le funzioni siano pure e referenzialmente trasparenti": In che modo questo semplifica il sovraccarico del tipo di ritorno?
Giorgio,

@Giorgio Non lo fa - Rust non impone la funzione puritamente e può ancora fare il sovraccarico del tipo di ritorno (anche se il suo sovraccarico in Rust è molto diverso rispetto ad altre lingue (puoi sovraccaricare solo usando i modelli))
Idan Arye

La parte [long] e [int] doveva avere un modo per chiamare esplicitamente il metodo, nella maggior parte dei casi il modo in cui dovrebbe essere chiamato potrebbe essere dedotto direttamente dal tipo di variabile a cui viene assegnata l'esecuzione del metodo.
user2638180,

0

Si è disponibile in Swift e funziona bene lì. Ovviamente non puoi avere un tipo ambiguo su entrambi i lati, quindi deve essere conosciuto a sinistra.

L'ho usato in una semplice API di codifica / decodifica .

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

Ciò significa che le chiamate in cui sono noti tipi di parametri, come ad esempio initun oggetto, funzionano in modo molto semplice:

    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 stai prestando molta attenzione, noterai la dechOptchiamata sopra. Ho scoperto nel modo più duro che sovraccaricare lo stesso nome di funzione in cui il differenziatore stava restituendo un opzionale era troppo incline all'errore poiché il contesto chiamante poteva introdurre un'aspettativa che fosse un opzionale.


-5
int main() {
    auto var1 = method1(1);
}

In tal caso il compilatore potrebbe a) rifiutare la chiamata perché è ambiguo b) selezionare il primo / ultimo c) lasciare il tipo di var1e procedere con l'inferenza del tipo e non appena qualche altra espressione determina il tipo di var1utilizzo che per selezionare il corretta attuazione. Alla fine, mostrare un caso in cui l'inferenza di tipo è non banale dimostra raramente un punto diverso da tale inferenza di tipo è generalmente non banale.
back2dos,

1
Non è un argomento forte. La ruggine, ad esempio, fa un uso pesante dell'inferenza di tipo e, in alcuni casi, specialmente con i generici, non è in grado di dire di quale tipo hai bisogno. In questi casi, devi semplicemente dare esplicitamente il tipo invece di fare affidamento sull'inferenza del tipo.
8bittree,

1
Uh ... questo non dimostra un tipo di ritorno sovraccarico. method1deve essere dichiarato per restituire un tipo particolare.
Gort il robot il
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.