Sovraccarico di funzioni? Sì o no [chiuso]


16

Sto sviluppando un linguaggio compilato staticamente e fortemente tipizzato, e sto rivisitando l'idea di includere il sovraccarico di funzioni come una caratteristica del linguaggio. Mi sono reso conto che sono un po 'di parte, proveniente principalmente da uno C[++|#]sfondo.

Quali sono gli argomenti più convincenti a favore e contro, incluso il sovraccarico di funzioni in una lingua?


EDIT: non c'è nessuno che abbia un'opinione opposta?

Bertrand Meyer (creatore di Eiffel nel 1985/1986) chiama il metodo sovraccaricandolo: (fonte)

un meccanismo di vanità che non porta nulla al potere semantico di un linguaggio OO, ma ostacola la leggibilità e complica il compito di tutti

Ora quelle sono alcune generalizzazioni radicali, ma è un ragazzo intelligente, quindi penso che sia sicuro dire che potrebbe sostenerle se fosse necessario. In realtà, aveva quasi convinto Brad Abrams (uno degli sviluppatori di CLSv1) che .NET non avrebbe dovuto supportare il sovraccarico dei metodi. (fonte) Sono cose potenti. Qualcuno può fare luce sui suoi pensieri e se il suo punto di vista è ancora giustificato 25 anni dopo?

Risposte:


24

Il sovraccarico delle funzioni è assolutamente fondamentale per il codice modello in stile C ++. Se devo usare nomi di funzioni differenti per tipi diversi, non posso scrivere codice generico. Ciò eliminerebbe una parte grande e ampiamente utilizzata della libreria C ++ e gran parte delle funzionalità di C ++.

Di solito è presente nei nomi delle funzioni membro. A.foo()può chiamare una funzione completamente diversa da B.foo(), ma entrambe le funzioni sono denominate foo. È presente negli operatori, così come +diverse cose quando applicato a numeri interi e in virgola mobile, ed è spesso usato come operatore di concatenazione di stringhe. Sembra strano non permetterlo anche nelle normali funzioni.

Consente l'uso di "multimetodi" in stile Lisp comune, in cui la funzione esatta chiamata dipende da due tipi di dati. Se non hai programmato nel Common Lisp Object System, provalo prima di chiamarlo inutile. È vitale per i flussi C ++.

L'I / O senza sovraccarico di funzioni (o funzioni variadiche, che sono peggio) richiederebbe un numero di funzioni diverse, sia per stampare valori di tipi diversi sia per convertire valori di tipi diversi in un tipo comune (come String).

Senza sovraccarico della funzione, se cambio il tipo di qualche variabile o valore devo cambiare ogni funzione che la utilizza. Rende molto più difficile il refactoring del codice.

Semplifica l'utilizzo delle API quando l'utente non deve ricordare quale tipo di convenzione di denominazione è in uso e l'utente può semplicemente ricordare i nomi delle funzioni standard.

Senza sovraccarico dell'operatore, dovremmo etichettare ogni funzione con i tipi che utilizza, se tale operazione di base può essere utilizzata su più di un tipo. Questa è essenzialmente una notazione ungherese, il cattivo modo di farlo.

Nel complesso, rende una lingua molto più utilizzabile.


1
+1, tutti i punti molto buoni. E credetemi, non penso che i multimetodi siano inutili ... Maledico la stessa tastiera su cui scrivo ogni volta che sono costretto a usare il modello visitatore.
Nota per se stessi - pensa a un nome il

Questo ragazzo non sta descrivendo la funzione prevalente e non sovraccarico?
dragosb,

8

Raccomando almeno di essere a conoscenza delle classi di tipi in Haskell. Le classi di tipi sono state create per essere un approccio disciplinato al sovraccarico dell'operatore, ma hanno trovato altri usi e, in una certa misura, hanno reso Haskell quello che è.

Ad esempio, ecco un esempio di sovraccarico ad hoc (Haskell non del tutto valido):

(==) :: Int -> Int -> Bool
x == y = ...
x /= y = not (x == y)

(==) :: Char -> Char -> Bool
x == y = ...
x /= y = not (x == y)

Ed ecco lo stesso esempio di sovraccarico con le classi di tipi:

class Eq a where
    (==) :: a -> a -> Bool
    (/=) :: a -> a -> Bool

    x /= y  =  not (x == y)

instance Eq Int where
    x == y  = ...

instance Eq Char where
    x == y  = ...

Un aspetto negativo di questo è che si deve venire con nomi funky per tutti i vostri typeclasses (come in Haskell, si ha la piuttosto astratto Monad, Functor, Applicativecosì come il più semplice e riconoscibile Eq, Nume Ord).

Un aspetto positivo è che, una volta che hai familiarità con una typeclass, sai come usare qualsiasi tipo in quella classe. Inoltre, è facile proteggere le funzioni da tipi che non implementano le classi necessarie, come in:

group :: (Eq a) => [a] -> [[a]]
group = groupBy (==)

Modifica: in Haskell, se si desidera un ==operatore che accetta due diversi tipi, è possibile utilizzare una classe di tipo multiparametro:

class Eq a b where
    (==) :: a -> b -> Bool
    (/=) :: a -> b -> Bool

    x /= y  =  not (x == y)

instance Eq Int Int where
    x == y  = ...

instance Eq Char Char where
    x == y  = ...

instance Eq Int Float where
    x == y  = ...

Naturalmente, questa è probabilmente una cattiva idea, dal momento che ti consente esplicitamente di confrontare mele e arance. Tuttavia, potresti voler considerare questo +, dal momento che l'aggiunta di un Word8a Intè davvero una cosa sensata da fare in alcuni contesti.


+1, ho cercato di afferrare questo concetto da quando l'ho letto per la prima volta, e questo mi aiuta. Questo paradigma rende impossibile definire, ad esempio, (==) :: Int -> Float -> Boolovunque? (indipendentemente dal fatto che sia una buona idea, ovviamente)
Nota per sé - pensa a un nome il

Se si autorizzano classi di tipo multiparametro (che Haskell supporta come estensione), è possibile. Ho aggiornato la risposta con un esempio.
Joey Adams,

Mmm, interessante. Quindi, fondamentalmente, class Eq a ...tradotto in pseudo-famiglia C sarebbe interface Eq<A> {bool operator==(A x, A y);}, e invece di usare il codice modello per confrontare oggetti arbitrari, usi questa 'interfaccia'. È giusto?
Nota per se stessi - pensa ad un nome il

Giusto. Potresti anche dare una rapida occhiata alle interfacce in Go. Vale a dire, non devi dichiarare un tipo come implementazione di un'interfaccia, devi solo implementare tutti i metodi per quell'interfaccia.
Joey Adams,

1
@ Notetoself-thinkofaname: per quanto riguarda gli operatori aggiuntivi - sì e no. Permette ==di trovarsi in uno spazio dei nomi diverso ma non consente di sovrascriverlo. Si noti che esiste un singolo spazio dei nomi ( Prelude) incluso per impostazione predefinita, ma è possibile impedire il caricamento utilizzando le estensioni o importandolo in modo esplicito ( import Prelude ()non importa nulla da Preludee import qualified Prelude as Pnon inserisce simboli nello spazio dei nomi corrente).
Maciej Piechotka,

4

Consenti il ​​sovraccarico della funzione, non puoi eseguire le seguenti operazioni con parametri opzionali (o se puoi, non bene).

L'esempio banale non presuppone alcun base.ToString() metodo

string ToString(int i) {}
string ToString(double d) {}
string ToString(DateTime d) {}
...

Per un linguaggio fortemente tipizzato, sì. Per una lingua debolmente tipizzata, no. Puoi fare quanto sopra con una sola funzione in un linguaggio debolmente digitato.
spex,

2

Ho sempre preferito i parametri predefiniti rispetto al sovraccarico della funzione. Le funzioni sovraccaricate in genere chiamano semplicemente una versione "predefinita" con parametri predefiniti. Perché scrivere

int indexOf(char ch)
{
  return self.indexOf(ch, 0);
}

int indexOf(char ch, int fromIndex)
{
  // Do whatever
}

Quando potrei fare:

int indexOf(char ch, int fromIndex=0)
{
  // Do whatever
}

Detto questo, mi rendo conto che a volte le funzioni sovraccaricate fanno cose diverse, piuttosto che chiamare semplicemente un'altra variante con parametri predefiniti ... ma in tal caso, non è una cattiva idea (in effetti, probabilmente è una buona idea) dargli un nome diverso.

(Inoltre, gli argomenti delle parole chiave in stile Python funzionano davvero bene con i parametri predefiniti.)


Ok, proviamo di nuovo, e questa volta proverò a dare un senso ... Che ne dici di essere Array Slice(int start, int length) {...}sovraccarico Array Slice(int start) {return this.Slice(start, this.Count - start);}? Non può essere codificato utilizzando i parametri predefiniti. Pensi che dovrebbero avere nomi diversi? In tal caso, come li chiameresti?
Nota per se stessi: pensa a un nome il

Ciò non riguarda tutti gli usi del sovraccarico.
MetalMikester,

@MetalMikester: quali usi ritieni che mi sia perso?
mipadi,

@mipadi Manca cose come indexOf(char ch)+ indexOf(Date dt)in un elenco. Mi piacciono anche i valori predefiniti ma non sono intercambiabili con la digitazione statica.
Segna il

2

Hai appena descritto Java. O C #.

Perché stai reinventando la ruota?

Assicurati che il tipo restituito faccia parte della firma del metodo e sovraccarica il contenuto del tuo cuore, pulisce davvero il codice quando non devi dirlo.

function getThisFirstWay(int type)
{ ... }
function getThisSecondWay(int type, double limit)
{ ... }
function getThisThirdWay(int type, String match)
{ ... }

7
C'è un motivo per cui il tipo restituito non fa parte della firma del metodo - o almeno non una parte che può essere utilizzata per la risoluzione del sovraccarico - in qualsiasi lingua di cui sia a conoscenza. Quando si chiama una funzione come procedura, senza assegnare il risultato a una variabile o proprietà, in che modo il compilatore dovrebbe capire quale versione invocare se tutti gli altri argomenti sono identici?
Mason Wheeler,

@Mason: è possibile rilevare in modo euristico il tipo di ritorno previsto in base a ciò che dovrebbe essere restituito, tuttavia non mi aspetto che ciò venga fatto.
Josh K,

1
Umm ... come fa il tuo euristico a sapere cosa dovrebbe essere restituito quando non ti aspetti un valore di ritorno?
Mason Wheeler,

1
L'euristica delle aspettative di tipo in realtà è a posto ... puoi fare cose del genere EnumX.Flag1 | Flag2 | Flag3. Non lo implementerò, comunque. Se lo facessi e il tipo restituito non fosse utilizzato, cercherei un tipo restituito di void.
Nota per se stessi - pensa a un nome il

1
@ Mason: Questa è una buona domanda, ma in tal caso cercherò una funzione nulla (come detto). Inoltre, in teoria, puoi sceglierne uno qualsiasi, poiché eseguiranno tutti la stessa funzione, restituendo semplicemente i dati in un formato diverso.
Josh K,

2

Grrr .. privilegio non abbastanza per commentare ancora ..

@Mason Wheeler: fai attenzione quindi ad Ada, che sovraccarica il tipo di ritorno. Anche il mio linguaggio Felix lo fa anche in alcuni contesti, in particolare quando una funzione restituisce un'altra funzione e c'è una chiamata come:

f a b  // application is left assoc: (f a) b

il tipo di b può essere utilizzato per la risoluzione del sovraccarico. Anche il sovraccarico di C ++ sul tipo restituito in alcune circostanze:

int (*f)(int) = g; // choses g based on type, not just signature

In effetti ci sono algoritmi per il sovraccarico sul tipo restituito, usando l'inferenza del tipo. In realtà non è così difficile da fare con una macchina, il problema è che gli umani lo trovano difficile. (Penso che lo schema sia dato nel Dragon Book, l'algoritmo è chiamato l'algoritmo see-saw se ricordo bene)


2

Caso d'uso contro l'implementazione del sovraccarico di funzioni: 25 metodi con lo stesso nome che fanno le stesse cose ma con set di argomenti completamente diversi in un'ampia varietà di schemi.

Caso d'uso per non implementare il sovraccarico delle funzioni: 5 metodi con nomi simili con insiemi di tipi molto simili nello stesso identico schema.

Alla fine della giornata, non vedo l'ora di leggere i documenti per un'API prodotta in entrambi i casi.

Ma in un caso si tratta di ciò che gli utenti potrebbero fare. Nell'altro caso è ciò che gli utenti devono fare a causa di una restrizione linguistica. IMO, è meglio consentire almeno agli autori del programma di essere abbastanza intelligenti da sovraccaricare sensibilmente senza creare ambiguità. Quando schiaffi le loro mani e togli l'opzione, sostanzialmente stai garantendo l'ambiguità. Sono più fiducioso che l'utente faccia la cosa giusta che pensare che faranno sempre la cosa sbagliata. Nella mia esperienza, il protezionismo tende a comportare comportamenti ancora peggiori da parte della comunità di una lingua.


1

Ho scelto di fornire ordinarie classi di sovraccarico e multi-tipo nella mia lingua Felix.

Considero essenziale (aperto) il sovraccarico, specialmente in un linguaggio che come molti tipi numerici (Felix ha tutti i tipi numerici di C). Tuttavia, diversamente dal C ++ che abusa del sovraccarico facendo dipendere i modelli da esso, il polimorfismo di Felix è parametrico: è necessario un sovraccarico per i modelli in C ++ perché i modelli in C ++ sono mal progettati.

Le classi di tipi sono fornite anche in Felix. Per coloro che conoscono il C ++ ma non si lamentano di Haskell, ignorare quelli che lo descrivono come un sovraccarico. Non è un sovraccarico da remoto, piuttosto, è come una specializzazione di modello: dichiari un modello che non implementi, quindi fornisci implementazioni per casi particolari quando ne hai bisogno. La tipizzazione è parametricamente polimorfica, l'implementazione avviene per istanza ad hoc ma non è destinata a non essere vincolata: deve implementare la semantica prevista.

In Haskell (e C ++) non è possibile dichiarare la semantica. In C ++ l'idea "Concetti" è approssimativamente un tentativo di approssimazione della semantica. In Felix, puoi approssimare l'intenzione con assiomi, riduzioni, lemmi e teoremi.

Il vantaggio principale e unico del sovraccarico (aperto) in un linguaggio ben basato su principi come Felix è che semplifica la memorizzazione dei nomi delle funzioni della libreria, sia per chi scrive il programma che per chi esamina il codice.

Lo svantaggio principale del sovraccarico è il complesso algoritmo necessario per implementarlo. Inoltre, non si adatta molto bene all'inferenza di tipo: sebbene i due non siano del tutto esclusivi, l'algoritmo per fare entrambi è abbastanza complesso che il programmatore probabilmente non sarebbe in grado di prevedere i risultati.

In C ++ questo è anche un problema perché ha un algoritmo di adattamento sciatto e supporta anche conversioni di tipo automatiche: in Felix I ho "risolto" questo problema richiedendo una corrispondenza esatta e nessuna conversione di tipo automatica.

Quindi hai una scelta, penso: sovraccarico o inferenza del tipo. L'inferenza è carina, ma è anche molto difficile da implementare in un modo che diagnostica correttamente i conflitti. Ocaml, ad esempio, ti dice da dove rileva un conflitto, ma non da dove ha dedotto il tipo previsto.

Il sovraccarico non è molto meglio, anche se hai un compilatore di qualità che cerca di dirti tutti i candidati, può essere difficile leggere se i candidati sono polimorfici, e ancora peggio se si tratta di un hacker di template C ++.


Sembra interessante. Mi piacerebbe leggere di più, ma il link ai documenti sulla pagina Web di Felix è interrotto.
Nota per se stessi: pensa a un nome il

Sì, l'intero sito è attualmente in costruzione (di nuovo), scusa.
Yttrill

0

Si riduce al contesto, ma penso che il sovraccarico renda una classe molto più utile quando ne uso una scritta da qualcun altro. Spesso si finisce con meno ridondanza.


0

Se stai cercando di avere utenti con familiarità con le lingue della famiglia C, allora sì, perché i tuoi utenti se lo aspettano.

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.