web-dev-qa-db-fra.com

Est-ce une mauvaise idée de renvoyer différents types de données à partir d'une seule fonction dans un langage typé dynamiquement?

Ma langue principale est typée statiquement (Java). En Java, vous devez renvoyer un seul type de chaque méthode. Par exemple, vous ne pouvez pas avoir de méthode qui renvoie conditionnellement un String ou renvoie conditionnellement un Integer. Mais en JavaScript, par exemple, c'est très possible.

Dans une langue tapée statiquement, je comprends pourquoi c'est une mauvaise idée. Si chaque méthode a renvoyé Object (le parent commun dont toutes les classes héritent), vous et le compilateur n'avez aucune idée de ce à quoi vous avez affaire. Vous devrez découvrir toutes vos erreurs lors de l'exécution.

Mais dans un langage typé dynamiquement, il peut même ne pas y avoir de compilateur. Dans un langage typé dynamiquement, il n'est pas évident pour moi pourquoi une fonction qui renvoie plusieurs types est une mauvaise idée. Mon expérience dans les langages statiques me fait éviter d'écrire de telles fonctions, mais je crains d'être proche de moi à propos d'une fonctionnalité qui pourrait rendre mon code plus propre d'une manière que je ne peux pas voir.


Edit: Je vais supprimer mon exemple (jusqu'à ce que je puisse penser à un meilleur). Je pense que cela incite les gens à répondre à un point que je n'essaie pas de faire valoir.

68
Daniel Kaplan

Contrairement à d'autres réponses, il existe des cas où le retour de différents types est acceptable.

exemple 1

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

Dans certains langages typés statiquement, cela implique des surcharges, nous pouvons donc considérer qu'il existe plusieurs méthodes, chacune renvoyant le type fixe prédéfini. Dans les langages dynamiques, il peut s'agir de la même fonction, implémentée comme:

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

Même fonction, différents types de valeur de retour.

exemple 2

Imaginez que vous obteniez une réponse d'un composant OpenID/OAuth. Certains fournisseurs OpenID/OAuth peuvent contenir plus d'informations, telles que l'âge de la personne.

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

D'autres auraient le minimum, que ce soit une adresse e-mail ou le pseudonyme.

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

Encore une fois, même fonction, résultats différents.

Ici, l'avantage de renvoyer différents types est particulièrement important dans un contexte où vous ne vous souciez pas des types et des interfaces, mais de ce que les objets contiennent réellement. Par exemple, imaginons qu'un site Web contient un langage mature. Ensuite, la findCurrent() peut être utilisée comme ceci:

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

Refactoriser cela en code où chaque fournisseur aura sa propre fonction qui retournera un type fixe bien défini dégraderait non seulement la base de code et provoquerait la duplication de code, mais n'apporterait aucun avantage. On peut finir par faire des horreurs comme:

var age;
if (['Facebook', 'Yahoo', 'Blogger', 'LiveJournal'].contains(user.provider)) {
    age = user.age;
}
43
Arseni Mourzenko

En général, c'est une mauvaise idée pour les mêmes raisons que l'équivalent moral dans une langue typée statiquement est une mauvaise idée: vous n'avez aucune idée du type concret qui est retourné, vous n'avez donc aucune idée de ce que vous pouvez faire avec le résultat (à part le peu de choses qui peuvent être faites avec n'importe quelle valeur). Dans un système de type statique, vous avez des annotations vérifiées par le compilateur des types de retour et autres, mais dans un langage dynamique, les mêmes connaissances existent toujours - elles sont simplement informelles et stockées dans le cerveau et la documentation plutôt que dans le code source.

Dans de nombreux cas, cependant, il y a rime et raison pour laquelle le type est renvoyé et l'effet est similaire à la surcharge ou au polymorphisme paramétrique dans les systèmes de type statique. En d'autres termes, le type de résultat est prévisible, ce n'est tout simplement pas aussi simple à exprimer.

Mais notez qu'il peut y avoir d'autres raisons pour lesquelles une fonction spécifique est mal conçue: par exemple, une fonction sum renvoyant false sur des entrées invalides est une mauvaise idée principalement parce que cette valeur de retour est inutile et sujette aux erreurs (0 <- > fausse confusion).

31
user7043

Dans les langages dynamiques, vous ne devriez pas demander si retourner différents types mais des objets avec des API différentes . Comme la plupart des langages dynamiques ne se soucient pas vraiment des types, mais utilisent différentes versions de typage du canard .

Lors du retour de différents types de sens

Par exemple, cette méthode est logique:

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

Parce que les deux fichiers et une liste de chaînes sont (en Python) des itérables qui renvoient une chaîne. Types très différents, même API (sauf si quelqu'un essaie d'appeler des méthodes de fichier sur une liste, mais c'est une autre histoire).

Vous pouvez conditionnellement renvoyer list ou Tuple (Tuple est une liste immuable en python).

Faire même officiellement:

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

ou:

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

renvoie différents types, car les deux python None et Javascript null sont des types à part entière.

Tous ces cas d'utilisation auraient leur homologue dans des langages statiques, la fonction renverrait simplement une interface appropriée.

Lorsque renvoyer des objets avec différentes API conditionnellement est une bonne idée

Quant à savoir si le retour de différentes API est une bonne idée, l'OMI dans la plupart des cas, cela n'a pas de sens. Le seul exemple raisonnable qui me vient à l'esprit est quelque chose de proche de ce que @MainMa a dit : lorsque votre API peut fournir une quantité variable de détails, il peut être judicieux de renvoyer plus de détails lorsqu'ils sont disponibles.

26
jb.

Ta question me donne envie de pleurer un peu. Pas pour l'exemple d'utilisation que vous avez fourni, mais parce que quelqu'un poussera involontairement cette approche trop loin. C'est à une courte distance du code ridiculement impossible à maintenir.

Le cas d'utilisation de la condition d'erreur est logique, et le motif nul (tout doit être un motif) dans les langages typés statiquement fait le même genre de chose . Votre appel de fonction renvoie un object ou il renvoie null.

Mais c'est une courte étape pour dire "Je vais l'utiliser pour créer un modèle d'usine " et retourner soit foo ou bar ou baz selon l'humeur de la fonction. Le débogage deviendra un cauchemar lorsque l'appelant attend foo mais a reçu bar.

Je ne pense donc pas que vous soyez fermé d'esprit. Vous êtes prudent dans l'utilisation des fonctionnalités du langage.

Divulgation: mes antécédents sont dans des langages typés statiquement, et j'ai généralement travaillé sur des équipes plus grandes et diverses où le besoin de maintenable le code était assez élevé. Mon point de vue est donc probablement également biaisé.

9
user53019

L'utilisation de génériques dans Java vous permet de renvoyer un type différent, tout en conservant la sécurité du type statique. Vous spécifiez simplement le type que vous souhaitez renvoyer dans le paramètre de type générique de l'appel de fonction.

Si vous pouvez utiliser une approche similaire en Javascript est, bien sûr, une question ouverte. Étant donné que Javascript est un langage typé dynamiquement, renvoyer un object semble être le choix évident.

Si vous souhaitez savoir où un scénario de retour dynamique peut fonctionner lorsque vous avez l'habitude de travailler dans un langage de type statique, envisagez d'examiner le mot clé dynamic en C #. Rob Conery a réussi à écrire un mappeur relationnel-objet dans 400 lignes de code en utilisant le mot clé dynamic.

Bien sûr, tout ce que dynamic fait, c'est envelopper une variable object avec une certaine sécurité de type runtime.

6
Robert Harvey

En fait, il n'est pas très rare de renvoyer différents types même dans une langue typée statiquement. C'est pourquoi nous avons des types d'unions, par exemple.

En fait, les méthodes de Java renvoient presque toujours l'un des quatre types: une sorte d'objet ou null ou une exception ou elles ne reviennent jamais du tout.

Dans de nombreuses langues, les conditions d'erreur sont modélisées comme des sous-programmes renvoyant soit un type de résultat, soit un type d'erreur. Par exemple, à Scala:

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

C'est un exemple stupide, bien sûr. Le type de retour signifie "soit retourner une chaîne ou une décimale". Par convention, le type gauche est le type d'erreur (dans ce cas, une chaîne avec le message d'erreur) et le type droit est le type de résultat.

Ceci est similaire aux exceptions, à l'exception du fait que les exceptions sont également des constructions de flux de contrôle. Ils sont, en fait, équivalents en puissance expressive à GOTO.

4
Jörg W Mittag

Aucune réponse n'a encore mentionné SOLID. En particulier, vous devez suivre le principe de substitution Liskov, que toute classe recevant un type autre que le type attendu peut toujours fonctionner avec tout ce qu'elle obtient, sans rien faire pour tester le type retourné.

Donc, si vous jetez des propriétés supplémentaires sur un objet, ou encapsulez une fonction renvoyée avec une sorte de décorateur qui accomplit toujours ce que la fonction d'origine était censée accomplir, vous êtes bon, tant qu'aucun code appelant votre fonction ne s'appuie sur ce comportement, dans n'importe quel chemin de code.

Au lieu de renvoyer une chaîne ou un entier, un meilleur exemple pourrait être qu'il retourne un système d'arrosage ou un chat. C'est très bien si tout le code appelant va faire, c'est appeler functionInQuestion.hiss (). En effet, vous avez une interface implicite que le code appelant attend, et un langage typé dynamiquement ne vous forcera pas à rendre l'interface explicite.

Malheureusement, vos collègues le feront probablement, vous devrez donc probablement faire le même travail de toute façon dans votre documentation, sauf qu'il n'y a pas de manière universellement acceptée, concise et analysable par machine pour le faire - comme c'est le cas lorsque vous définissez une interface dans une langue qui les a.

4
psr

C'est une mauvaise idée, je pense, de retourner conditionnellement différents types. Pour moi, cela revient souvent si la fonction peut renvoyer une ou plusieurs valeurs. Si une seule valeur doit être renvoyée, il peut sembler raisonnable de simplement renvoyer la valeur, plutôt que de la placer dans un tableau, pour éviter d'avoir à la décompresser dans la fonction appelante. Cependant, cela (et la plupart des autres instances de cela) oblige l'appelant à distinguer et à gérer les deux types. La fonction sera plus facile à raisonner si elle retourne toujours le même type.

4
user116240

La "mauvaise pratique" existe indépendamment du fait que votre langue soit typée statiquement. Le langage statique fait plus pour vous éloigner de ces pratiques, et vous pourriez trouver plus d'utilisateurs qui se plaignent de "mauvaises pratiques" dans un langage statique car c'est un langage plus formel. Cependant, les problèmes sous-jacents sont là dans un langage dynamique, et vous pouvez déterminer s'ils sont justifiés.

Voici la partie répréhensible de ce que vous proposez. Si je ne sais pas quel type est renvoyé, je ne peux pas utiliser la valeur de retour immédiatement. Je dois "découvrir" quelque chose à ce sujet.

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

Souvent, ce type de changement de code est simplement une mauvaise pratique. Cela rend le code plus difficile à lire. Dans cet exemple, vous voyez pourquoi lancer et intercepter des cas d'exception est populaire. Autrement dit: si votre fonction ne peut pas faire ce qu'elle dit, elle ne devrait pas retourner correctement. Si j'appelle votre fonction, je veux le faire:

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

Parce que la première ligne retourne avec succès, je suppose que total contient en fait la somme du tableau. S'il ne revient pas avec succès, nous passons à une autre page de mon programme.

Prenons un autre exemple qui ne concerne pas seulement la propagation des erreurs. Peut-être que sum_of_array essaie d'être intelligent et de renvoyer une chaîne lisible par l'homme dans certains cas, comme "C'est ma combinaison de casiers!" si et seulement si le tableau est [11,7,19]. J'ai du mal à trouver un bon exemple. Quoi qu'il en soit, le même problème s'applique. Vous devez inspecter la valeur de retour avant de pouvoir en faire quoi que ce soit:

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

Vous pourriez faire valoir qu'il serait utile que la fonction retourne un entier ou un flottant, par exemple:

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

Mais ces résultats ne sont pas différents pour vous. Vous allez les traiter tous les deux comme des chiffres, et c'est tout ce qui vous importe. Donc sum_of_array renvoie le type de nombre. C'est à cela que sert le polymorphisme.

Il existe donc certaines pratiques que vous violez si votre fonction peut renvoyer plusieurs types. Les connaître vous aidera à déterminer si votre fonction particulière doit de toute façon retourner plusieurs types.

4
Travis Wilson

Le seul endroit où je me vois envoyer des types différents est pour une entrée invalide ou des "exceptions de pauvre homme" où la condition "exceptionnelle" n'est pas très exceptionnelle. Par exemple, à partir de mon référentiel de fonctions utilitaires PHP cet exemple abrégé:

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 fonction renvoie nominalement un BOOLEAN, mais elle retourne NULL sur une entrée non valide. Notez que depuis PHP 5.3, toutes les fonctions internes PHP se comportent également de cette façon. De plus, certaines fonctions internes PHP renvoient FALSE ou INT sur l'entrée nominale, voir:

strpos('Hello', 'e');  // Returns INT(1)
strpos('Hello', 'q');  // Returns BOOL(FALSE)
3
dotancohen

Je ne pense pas que ce soit une mauvaise idée! Contrairement à cette opinion la plus courante, et comme l'a déjà souligné Robert Harvey, les langages typés statiquement comme Java ont introduit des génériques exactement pour des situations comme celle que vous demandez. En fait Java essayez de conserver (dans la mesure du possible) la sécurité des types au moment de la compilation, et parfois les génériques évitent la duplication de code, pourquoi? parce que vous pouvez écrire la même méthode ou la même classe qui gère/retourne différents types. Je ferai juste un très bref exemple pour montrer cette idée:

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

Dans un langage typé dynamique, puisque vous n'avez aucune sécurité de type au moment de la compilation, vous êtes absolument libre d'écrire le même code qui fonctionne pour de nombreux types. Étant donné que même dans un langage à typage statique, les génériques ont été introduits pour résoudre ce problème, c'est clairement un indice que l'écriture d'une fonction qui renvoie différents types dans un langage dynamique n'est pas une mauvaise idée.

3
thermz

Le développement de logiciels est essentiellement un art et un métier de gestion de la complexité. Vous essayez d'affiner le système aux points que vous pouvez vous permettre et de limiter les options à d'autres points. L'interface de la fonction est un contrat qui aide à gérer la complexité du code en limitant les connaissances requises pour travailler avec n'importe quel morceau de code. En renvoyant différents types, vous développez considérablement l'interface de la fonction en y ajoutant toutes les interfaces des différents types que vous renvoyez ET en ajoutant des règles non évidentes sur l'interface à renvoyer.

2
Kaerber

En terre dynamique, il s'agit de taper du canard. La chose la plus responsable exposée/face au public à faire est d'envelopper des types potentiellement différents dans un wrapper qui leur donne la même interface.

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

Il peut parfois être judicieux de faire caca différents types sans emballage générique, mais honnêtement, en 7 ans d'écriture de JS, ce n'est pas quelque chose que j'ai trouvé raisonnable ou pratique à faire très souvent. C'est surtout quelque chose que je ferais dans le contexte d'environnements fermés comme l'intérieur d'un objet où les choses se mettent en place. Mais ce n'est pas quelque chose que j'ai fait assez souvent pour que des exemples me viennent à l'esprit.

Surtout, je vous conseille de cesser de penser autant aux types. Vous traitez des types lorsque vous en avez besoin dans un langage dynamique. Pas plus. C'est tout le problème. Ne vérifiez pas le type de chaque argument. C'est quelque chose que je serais seulement tenté de faire dans un environnement où la même méthode pourrait donner des résultats incohérents de manière non évidente (donc ne faites certainement pas quelque chose comme ça). Mais ce n'est pas le type qui compte, c'est la façon dont vous me donnez ce que vous voulez.

1
Erik Reppen

Perl l'utilise beaucoup car ce qu'une fonction fait dépend de son contexte . Par exemple, une fonction peut retourner un tableau si elle est utilisée dans un contexte de liste, ou la longueur du tableau si elle est utilisée à un endroit où une valeur scalaire est attendue. De le tutoriel qui est le premier hit pour "Contexte Perl" , si vous le faites:

my @now = localtime();

Alors @now est une variable de tableau (c'est ce que signifie le @), et contiendra un tableau comme (40, 51, 20, 9, 0, 109, 5, 8, 0).

Si à la place vous appelez la fonction d'une manière où son résultat devra être un scalaire, avec (les variables $ sont des scalaires):

my $now = localtime();

alors il fait quelque chose de complètement différent: $ maintenant sera quelque chose comme "Fri Jan 9 20:51:40 2009".

Un autre exemple auquel je peux penser est l'implémentation des API REST, où le format de ce qui est renvoyé dépend de ce que le client veut. Par exemple, HTML, ou JSON, ou XML. Bien que techniquement, ce sont tous flux d'octets, l'idée est similaire.

1
RemcoGerlich