È una cattiva idea restituire diversi tipi di dati da una singola funzione in un linguaggio tipizzato dinamicamente?

64

La mia lingua principale è tipizzata staticamente (Java). In Java, devi restituire un singolo tipo da ogni metodo. Ad esempio, non puoi avere un metodo che restituisca condizionalmente un String o che restituisca condizionalmente un Integer . Ma in JavaScript, ad esempio, questo è molto possibile.

In un linguaggio tipizzato staticamente capisco perché questa è una cattiva idea. Se ogni metodo restituisce Object (il genitore comune da cui ereditano tutte le classi) allora tu e il compilatore non hai idea di cosa hai a che fare. Dovrai scoprire tutti i tuoi errori in fase di esecuzione.

Ma in un linguaggio tipizzato dinamicamente, potrebbe non esserci nemmeno un compilatore. In un linguaggio tipizzato dinamicamente, non è ovvio per me perché una funzione che restituisce più tipi è una cattiva idea. Il mio background in linguaggi statici mi evita di scrivere tali funzioni, ma temo di essere preoccupato per una funzionalità che potrebbe rendere il mio codice più pulito in modi che non riesco a vedere.

Modifica : rimuoverò il mio esempio (finché non riesco a pensarne uno migliore). Penso che stia indirizzando le persone a rispondere a un punto che non sto cercando di fare.

    
posta Daniel Kaplan 27.01.2014 - 18:45
fonte

14 risposte

43

Diversamente da altre risposte, ci sono casi in cui è possibile accettare tipi diversi di ritorno.

Esempio 1

sum(2, 3) → int
sum(2.1, 3.7) → float

In alcuni linguaggi tipizzati staticamente, ciò comporta sovraccarichi, quindi possiamo considerare che esistono diversi metodi, ognuno dei quali restituisce il tipo predefinito predefinito. Nei linguaggi dinamici, questa potrebbe essere la stessa funzione, implementata come:

var sum = function (a, b) {
    return a + b;
};

Stessa funzione, diversi tipi di valore di ritorno.

Esempio 2

Immagina di ricevere una risposta da un componente OpenID / OAuth. Alcuni provider OpenID / OAuth possono contenere più informazioni, come l'età della persona.

var user = authProvider.findCurrent();
// user is now:
// {
//     provider: 'Facebook',
//     name: {
//         firstName: 'Hello',
//         secondName: 'World',
//     },
//     email: '[email protected]',
//     age: 27
// }

Gli altri avrebbero il minimo, sarebbe un indirizzo email o lo pseudonimo.

var user = authProvider.findCurrent();
// user is now:
// {
//     provider: 'Google',
//     email: '[email protected]'
// }

Ancora, stessa funzione, risultati diversi.

Qui, il vantaggio di restituire diversi tipi è particolarmente importante in un contesto in cui non ti interessa tipi e interfacce, ma quali oggetti contengono effettivamente. Ad esempio, immaginiamo che un sito web contenga linguaggio maturo. Quindi findCurrent() può essere usato in questo modo:

var user = authProvider.findCurrent();
if (user.age || 0 >= 16) {
    // The person can stand mature language.
    allowShowingContent();
} else if (user.age) {
    // OpenID/OAuth gave the age, but the person appears too young to see the content.
    showParentalAdvisoryRequestedMessage();
} else {
    // OpenID/OAuth won't tell the age of the person. Ask the user himself.
    askForAge();
}

Rifattorizzare questo codice in codice in cui ogni provider avrà una propria funzione che restituirà un tipo fisso ben definito non solo degraderebbe la base del codice e causerebbe la duplicazione del codice, ma non apporterebbe alcun vantaggio. Si può finire per fare orrori come:

var age;
if (['Facebook', 'Yahoo', 'Blogger', 'LiveJournal'].contains(user.provider)) {
    age = user.age;
}
    
risposta data 27.01.2014 - 19:58
fonte
31

In generale, è una cattiva idea per le stesse ragioni l'equivalente morale in un linguaggio tipizzato staticamente è una cattiva idea: non hai idea di quale tipo concreto venga restituito, quindi non hai idea di cosa puoi fare con il risultato ( a parte le poche cose che si possono fare con qualsiasi valore). In un sistema di tipi statici, si hanno annotazioni sul compilatore di tipi restituiti e simili, ma in un linguaggio dinamico esiste ancora la stessa conoscenza: è solo informale e memorizzato nel cervello e nella documentazione piuttosto che nel codice sorgente.

In molti casi, tuttavia, vi è una rima e una ragione per cui viene restituito il tipo e l'effetto è simile a sovraccarico o polimorfismo parametrico nei sistemi di tipo statico. In altre parole, il tipo di risultato è prevedibile, non è così semplice da esprimere.

Ma si noti che potrebbero esserci altri motivi per cui una funzione specifica è progettata male: ad esempio, una funzione sum che restituisce false su input non validi è una cattiva idea principalmente perché quel valore restituito è inutile e soggetto a errori (0 < - > false confusion).

    
risposta data 27.01.2014 - 19:01
fonte
26

Nelle lingue dinamiche, non devi chiedere se restituisci tipi diversi ma oggetti con API diverse . Poiché la maggior parte dei linguaggi dinamici non si preoccupa veramente dei tipi, usa varie versioni di digitazione anatra .

Quando restituire tipi diversi ha senso

Ad esempio questo metodo ha senso:

def load_file(file): 
    if something: 
       return ['a ', 'list', 'of', 'strings'] 
    return open(file, 'r')

Poiché entrambi i file e un elenco di stringhe sono (in Python) iterabili che restituiscono una stringa. Tipi molto diversi, la stessa API (a meno che qualcuno non provi a chiamare metodi di file in un elenco, ma questa è una storia diversa).

Puoi condizionalmente restituire list o tuple ( tuple è una lista immutabile in python).

Formalmente anche facendo:

def do_something():
    if ...: 
        return None
    return something_else

o:

function do_something(){
   if (...) return null; 
   return sth;
}

restituisce diversi tipi, poiché sia python None che Javascript null sono tipi a se stanti.

Tutti questi casi d'uso avrebbero la loro controparte nei linguaggi statici, la funzione restituirebbe solo un'interfaccia corretta.

Quando restituire oggetti con API diverse in modo condizionale è una buona idea

Se restituire API diverse è una buona idea, nella maggior parte dei casi IMO non ha senso. Solo un esempio ragionevole che viene in mente è qualcosa di simile a che cosa @MainMa ha detto : quando la tua API può fornire una quantità variabile di dettaglio potrebbe avere senso restituire maggiori dettagli quando disponibili.

    
risposta data 28.01.2014 - 00:28
fonte
9

La tua domanda mi fa venir voglia di piangere un po '. Non per l'utilizzo di esempio che hai fornito, ma perché qualcuno intraprenderà involontariamente questo approccio troppo lontano. È a un passo dal codice ridicolmente non mantenibile.

La condizione di errore tipo caso d'uso ha senso, e il modello nullo (tutto deve essere un modello) in lingue tipizzate staticamente fa lo stesso tipo di cosa. La tua chiamata a funzione restituisce un object o restituisce null .

Ma è un breve passo per dire "Lo userò per creare un modello di fabbrica " e restituire foo o bar o baz a seconda dell'umore della funzione . Il debugging diventerà un incubo quando il chiamante prevede foo , ma è stato dato bar .

Quindi non penso che tu stia avendo una mente chiusa. Sei appropriatamente cauto nell'uso delle funzionalità del linguaggio.

Disclosure: il mio background è in lingue tipizzate in modo statico e in genere ho lavorato su team più numerosi e diversificati in cui la necessità di un codice gestibile era abbastanza elevata. Quindi la mia prospettiva è probabilmente distorta.

    
risposta data 27.01.2014 - 19:03
fonte
6

L'uso di Generics in Java consente di restituire un tipo diverso, mantenendo comunque la sicurezza del tipo statico. Basta specificare il tipo che si desidera restituire nel parametro di tipo generico della chiamata di funzione.

Se si possa usare un approccio simile in Javascript è, ovviamente, una domanda aperta. Poiché Javascript è un linguaggio digitato in modo dinamico, restituire un object sembra la scelta più ovvia.

Se vuoi sapere dove può funzionare uno scenario di ritorno dinamico quando sei abituato a lavorare in un linguaggio tipizzato staticamente, prova a guardare la parola chiave dynamic in C #. Rob Conery è riuscito a con successo a scrivere un Object-Relational Mapper in 400 righe di codice utilizzando la parola chiave dynamic .

Ovviamente, tutto ciò che dynamic fa è racchiudere una variabile object con un certo tipo di sicurezza di runtime.

    
risposta data 27.01.2014 - 18:53
fonte
4

È una cattiva idea, penso, restituire condizionalmente diversi tipi. Uno dei modi in cui questo viene frequentemente per me è se la funzione può restituire uno o più valori. Se è necessario restituire un solo valore, può sembrare ragionevole restituire semplicemente il valore, piuttosto che comprimerlo in una matrice, per evitare di doverlo decomprimere nella funzione di chiamata. Tuttavia, questo (e molti altri esempi di questo) impone al chiamante l'obbligo di distinguere e gestire entrambi i tipi. La funzione sarà più facile da ragionare se restituisce sempre lo stesso tipo.

    
risposta data 27.01.2014 - 19:13
fonte
4

La "cattiva pratica" esiste indipendentemente dal fatto che la tua lingua sia tipizzata staticamente. Il linguaggio statico fa di più per allontanarti da queste pratiche e potresti trovare più utenti che si lamentano di "cattive pratiche" in un linguaggio statico perché è un linguaggio più formale. Tuttavia, i problemi sottostanti sono presenti in un linguaggio dinamico e puoi determinare se sono giustificati.

Ecco la parte discutibile di ciò che proponi. Se non so quale tipo viene restituito, non posso utilizzare immediatamente il valore restituito. Devo "scoprire" qualcosa a riguardo.

total = sum_of_array([20, 30, 'q', 50])
if (type_of(total) == Boolean) {
  display_error(...)
} else {
  record_number(total)
}

Spesso questo tipo di switch in codice è semplicemente una cattiva pratica. Rende il codice più difficile da leggere. In questo esempio, capisci perché lanciare e catturare casi di eccezione è popolare. Detto in altro modo: se la tua funzione non può fare ciò che dice, non dovrebbe tornare con successo . Se chiamo la tua funzione, voglio farlo:

total = sum_of_array([20, 30, 'q', 50])
display_number(total)

Poiché la prima riga restituisce correttamente, presumo che total contenga effettivamente la somma dell'array. Se non ritorna con successo, passiamo ad un'altra pagina del mio programma.

Usiamo un altro esempio che non riguarda solo la propagazione degli errori. Forse sum_of_array cerca di essere intelligente e restituire una stringa leggibile in alcuni casi, come "Questa è la mia combinazione di armadietti!" se e solo se l'array è [11,7,19]. Sto avendo problemi a pensare ad un buon esempio. Ad ogni modo, si applica lo stesso problema. Devi controllare il valore di ritorno prima di poter fare qualsiasi cosa con esso:

total = sum_of_array([20, 30, 40, 50])
if (type_of(total) == String) {
  write_message(total)
} else {
  record_number(total)
}

Si potrebbe sostenere che sarebbe utile per la funzione restituire un intero o un float, ad esempio:

sum_of_array(20, 30, 40) -> int
sum_of_array(23.45, 45.67, 67.789044) -> float

Ma questi risultati non sono di tipo diverso, per quanto ti riguarda. Li tratterai entrambi come numeri, e questo è ciò che ti interessa. Quindi sum_of_array restituisce il tipo di numero. Questo è il polimorfismo.

Quindi ci sono alcune pratiche che potresti violare se la tua funzione può restituire più tipi. Conoscerli ti aiuterà a determinare se la tua particolare funzione dovrebbe restituire più tipi in ogni caso.

    
risposta data 27.01.2014 - 23:52
fonte
4

In realtà, non è affatto raro restituire tipi diversi anche in un linguaggio tipizzato staticamente. Ecco perché abbiamo tipi di unione, ad esempio.

In realtà, i metodi in Java restituiscono quasi sempre uno dei quattro tipi: un tipo di oggetto o null o un'eccezione o non restituiscono affatto.

In molte lingue, le condizioni di errore sono modellate come subroutine che restituiscono un tipo di risultato o un tipo di errore. Ad esempio, in Scala:

def transferMoney(amount: Decimal): Either[String, Decimal]

Questo è un esempio stupido, ovviamente. Il tipo restituito significa "o restituisce una stringa o un decimale". Per convenzione, il tipo di sinistra è il tipo di errore (in questo caso, una stringa con il messaggio di errore) e il tipo corretto è il tipo di risultato.

Questo è simile alle eccezioni, tranne per il fatto che le eccezioni sono anche costrutti di flusso di controllo. Sono, infatti, equivalenti in potenza espressiva a GOTO .

    
risposta data 28.01.2014 - 16:49
fonte
4

Nessuna risposta ha ancora menzionato i principi SOLID. In particolare, dovresti seguire il principio di sostituzione di Liskov, che qualsiasi classe che riceve un tipo diverso dal tipo atteso può ancora funzionare con qualsiasi cosa ottenga, senza fare nulla per testare che tipo viene restituito.

Quindi, se lanci alcune proprietà extra su un oggetto, o avvolgi una funzione restituita con qualche tipo di decoratore che realizza ancora ciò che la funzione originale era destinata a realizzare, sei a posto, finché nessun codice chiama la tua funzione mai si basa su questo comportamento, in qualsiasi percorso di codice.

Invece di restituire una stringa o un intero, un esempio migliore potrebbe essere che restituisce un sistema di irrigazione o un gatto. Questo va bene se tutto il codice chiamante sta per fare è chiamare functionInQuestion.hiss (). Effettivamente hai un'interfaccia implicita che il codice chiamante si aspetta e un linguaggio tipizzato in modo dinamico non ti costringerà a rendere l'interfaccia esplicita.

Tristemente, probabilmente i tuoi collaboratori lo faranno, quindi probabilmente dovrai fare lo stesso lavoro comunque nella tua documentazione, eccetto che non c'è un modo universalmente accettabile, conciso, analizzabile dalla macchina per farlo - come quando tu definire un'interfaccia in una lingua che li ha.

    
risposta data 28.01.2014 - 19:21
fonte
3

L'unico posto in cui mi vedo inviando diversi tipi è per input non validi o "eccezioni di mans mans" dove la condizione "eccezionale" non è eccezionale. Ad esempio, dal mio repository di funzioni di utilità PHP questo esempio abbreviato:

function ensure_fields($consideration)
{
        $args = func_get_args();
        foreach ( $args as $a ) {
                if ( !is_string($a) ) {
                        return NULL;
                }
                if ( !isset($consideration[$a]) || $consideration[$a]=='' ) {
                        return FALSE;
                }
        }

        return TRUE;
}

La funzione restituisce nominalmente una BOOLEAN, tuttavia restituisce NULL su input non validi. Nota che da PHP 5.3, anche tutte le funzioni PHP interne si comportano in questo modo . Inoltre, alcune funzioni PHP interne restituiscono FALSE o INT sull'input nominale, vedi:

strpos('Hello', 'e');  // Returns INT(1)
strpos('Hello', 'q');  // Returns BOOL(FALSE)
    
risposta data 28.01.2014 - 08:23
fonte
3

Non penso che questa sia una cattiva idea! In contrasto con questa opinione comune, e come già sottolineato da Robert Harvey, i linguaggi tipizzati staticamente come Java hanno introdotto Generics esattamente per situazioni come quella che stai chiedendo. In realtà Java tenta di mantenere (ove possibile) la typesafety in fase di compilazione, ea volte i Generics evitano la duplicazione del codice, perché? Perché puoi scrivere lo stesso metodo o la stessa classe che gestisce / restituisce tipi diversi . Farò solo un esempio molto breve per mostrare questa idea:

Java 1.4

public static Boolean getBoolean(String property){
    return (Boolean) properties.getProperty(property);
}
public static Integer getInt(String property){
    return (Integer) properties.getProperty(property);
}

Java 1.5 +

public static <T> getValue(String property, Class<T> clazz) throws WhateverCheckedException{
    return clazz.getConstructor(String.class).newInstance(properties.getProperty(property));
}
//the call will be
Boolean b = getValue("useProxy",Boolean.class);
Integer b = getValue("proxyPort",Integer.class);

In un linguaggio digitato dinamico, dato che non hai alcuna sicurezza di scrittura in fase di compilazione, sei assolutamente libero di scrivere lo stesso codice che funziona per molti tipi. Poiché anche in un linguaggio tipizzato staticamente sono stati introdotti i generici per risolvere questo problema, è chiaramente un suggerimento che scrivere una funzione che restituisce diversi tipi in un linguaggio dinamico non è una cattiva idea.

    
risposta data 28.01.2014 - 10:57
fonte
2

Lo sviluppo di software è fondamentalmente un'arte e un mestiere di gestione della complessità. Cerchi di restringere il sistema nei punti che ti puoi permettere e di limitare le opzioni in altri punti. L'interfaccia di Function è un contratto che aiuta a gestire la complessità del codice limitando una conoscenza necessaria per lavorare con qualsiasi pezzo di codice. Restituendo diversi tipi si espande in modo significativo l'interfaccia della funzione aggiungendo ad essa tutte le interfacce di diversi tipi restituiti e aggiungendo regole non obiettive su quale interfaccia viene restituita.

    
risposta data 28.01.2014 - 09:35
fonte
1

Perl usa molto questo perché ciò che una funzione fa dipende dal suo contesto . Ad esempio, una funzione può restituire un array se utilizzato nel contesto dell'elenco o la lunghezza dell'array se utilizzato in un punto in cui è previsto un valore scalare. Da il tutorial che è il primo hit per "contesto perl" , se lo fai:

my @now = localtime();

Quindi @now è una variabile di array (questo è ciò che @ significa), e conterrà un array come (40, 51, 20, 9, 0, 109, 5, 8, 0).

Se invece si chiama la funzione in un modo in cui il suo risultato dovrà essere uno scalare, con ($ variabili sono scalari):

my $now = localtime();

allora fa qualcosa di completamente diverso: $ ora sarà qualcosa come "Ven 9 gen, 20:51:40 2009".

Un altro esempio che posso pensare è l'implementazione delle API REST, in cui il formato di ciò che viene restituito dipende da ciò che il cliente desidera. E.g, HTML o JSON o XML. Sebbene tecnicamente questi siano tutti flussi di byte, l'idea è simile.

    
risposta data 28.01.2014 - 11:27
fonte
1

In dynamic-land è tutto incentrato sulla tipizzazione delle anatre. La cosa più esposta / rivolta al pubblico da fare è quella di racchiudere tipi potenzialmente diversi in un wrapper che fornisce loro la stessa interfaccia.

function ThingyWrapper(thingy){ //a function constructor (class-like thingy)

    //thingy is effectively private and persistent for ThingyWrapper instances

    if(typeof thingy === 'array'){
        this.alertItems = function(){
            thingy.forEach(function(el){ alert(el); });
        }
    }
    else {
        this.alertItems = function(){
            for(var x in thingy){ alert(thingy[x]); }
        }
    }
}

function gimmeThingy(){
    var
        coinToss = Math.round( Math.random() ),//gives me 0 or 1
        arrayThingy = [1,2,3],
        objectThingy = { item1:1, item2:2, item3:3 }
    ;

    //0 dynamically evaluates to false in JS
    return new ThingyWrapper( coinToss ? arrayThingy : objectThingy );
}

gimmeThingy().alertItems(); //should be same every time except order of numbers - maybe

Occasionalmente potrebbe essere sensato covare diversi tipi senza wrapper generico, ma onestamente in 7 anni di scrittura di JS, non è qualcosa che ho trovato che fosse ragionevole o conveniente fare molto spesso. Per lo più è qualcosa che farei nel contesto di ambienti chiusi come l'interno di un oggetto in cui le cose vengono messe insieme. Ma non è qualcosa che ho fatto abbastanza spesso che mi viene in mente qualche esempio.

Per lo più, ti consiglio di smettere di pensare ai tipi più o meno del tutto. Hai a che fare con i tipi quando ne hai bisogno in un linguaggio dinamico. Non piu. È il punto. Non controllare il tipo di ogni singolo argomento. Questo è qualcosa che sarei tentato di fare solo in un ambiente in cui lo stesso metodo potrebbe dare risultati incoerenti in modo non ovvio (quindi sicuramente non fare qualcosa del genere). Ma non è il tipo che conta, è come qualunque cosa tu mi dai delle opere.

    
risposta data 28.01.2014 - 18:24
fonte

Leggi altre domande sui tag