Y a-t-il une raison pour laquelle les fonctions de la plupart (?) Des langages de programmation sont conçues pour prendre en charge un nombre quelconque de paramètres d'entrée, mais une seule valeur renvoyée?
Dans la plupart des langages, il est possible de "contourner" cette limitation, par exemple en utilisant des paramètres de sortie, en renvoyant des pointeurs ou en définissant / renvoyant des structs / classes. Mais il semble étrange que les langages de programmation n’aient pas été conçus pour prendre en charge plusieurs valeurs de retour de manière plus "naturelle".
Y a-t-il une explication à cela?
def f(): return (1,2,3)
et vous pouvez utiliser-déballage tuple « split » tuple:a,b,c = f() #a=1,b=2,c=3
. Pas besoin de créer un tableau et d'extraire manuellement des éléments, pas besoin de définir une nouvelle classe.[a, b] = f()
vs[a, b, c] = f()
) et obtenu à l'intérieurf
parnargout
. Je ne suis pas un grand fan de Matlab, mais cela s'avère assez pratique parfois.Réponses:
Certaines langues, telles que Python , prennent en charge plusieurs valeurs de retour de manière native, alors que d'autres comme C # les prennent en charge via leurs bibliothèques de base.
Mais en règle générale, même dans les langues qui les prennent en charge, les valeurs de retour multiples ne sont pas utilisées souvent car elles sont négligées:
Il est facile de se tromper dans l'ordre des valeurs de retour
(Pour cette même raison, beaucoup de gens évitent d'avoir trop de paramètres dans une fonction; certains vont même jusqu'à dire qu'une fonction ne devrait jamais avoir deux paramètres du même type!)
Ils sont plus typés, ils conservent les valeurs de retour groupées dans une unité logique et ils conservent les noms des (propriétés de) les valeurs de retour cohérentes pour toutes les utilisations.
Le seul endroit où ils sont assez pratiques est dans les langages (comme Python) où plusieurs valeurs de retour d’une fonction peuvent être utilisées comme paramètres d’entrée multiples. Cependant, les cas d'utilisation où cette conception est meilleure que celle d'une classe sont plutôt minces.
la source
()
. Est-ce une chose ou zéro choses? Personnellement, je dirais que c'est une chose. Je peux assignerx = ()
très bien, tout comme je peux assignerx = randomTuple()
. Dans ce dernier cas, si le tuple renvoyé est vide ou non, je peux toujours affecter le tuple renvoyé àx
.Parce que les fonctions sont des constructions mathématiques qui effectuent un calcul et renvoient un résultat. En effet, beaucoup de langages de programmation "sous le capot" se concentrent uniquement sur une entrée et une sortie, les entrées multiples ne constituant qu'un cache-cache autour de l'entrée - et lorsqu'une seule sortie de valeur ne fonctionne pas, l'utilisation d'une seule structure cohésive (ou tuple, ou
Maybe
) soit la sortie (bien que cette valeur de retour "unique" soit composée de nombreuses valeurs).Cela n'a pas changé, car les programmeurs ont découvert que les
out
paramètres étaient des constructions peu pratiques, utiles dans un nombre limité de scénarios. Comme avec beaucoup d'autres choses, le support n'existe pas parce que le besoin / la demande n'existe pas.la source
En mathématiques, une fonction "bien définie" est une fonction dans laquelle il n'y a qu'une sortie pour une entrée donnée (en tant que note latérale, vous ne pouvez avoir qu'une seule fonction d'entrée, tout en obtenant sémantiquement plusieurs entrées à l'aide du currying ).
Pour les fonctions à valeurs multiples (par exemple, la racine carrée d'un entier positif, par exemple), il suffit de renvoyer une collection ou une séquence de valeurs.
Pour les types de fonctions dont vous parlez (c'est-à-dire les fonctions qui renvoient plusieurs valeurs, de types différents ), je le vois un peu différemment que vous semblez le faire: je vois la nécessité / l'utilisation de paramètres paramétriques structure de données plus utile. Par exemple, je préférerais que les
*.TryParse(...)
méthodes retournent uneMaybe<T>
monade au lieu d’utiliser un paramètre out. Pensez à ce code en F #:Le support du compilateur / IDE / analyse est très bon pour ces constructions. Cela résoudrait une grande partie du "besoin" des paramètres externes. Pour être tout à fait honnête, je ne peux imaginer aucune autre méthode à part où ce ne serait pas la solution.
Pour les autres scénarios, ceux dont je ne me souviens plus, un simple tuple suffit.
la source
var (value, success) = ParseInt("foo");
ce qui serait vérifié au moment de la compilation car elle a(int, bool) ParseInt(string s) { }
été déclarée. Je sais que cela peut être fait avec des génériques, mais cela ferait tout de même un bon ajout au langage.En plus de ce qui a déjà été dit lorsque vous examinez les paradigmes utilisés dans assembly quand une fonction renvoie, elle laisse un pointeur sur l'objet renvoyé dans un registre spécifique. S'ils utilisaient des registres variables / multiples, la fonction appelante ne saurait pas où obtenir la ou les valeurs renvoyées si cette fonction se trouvait dans une bibliothèque. Cela rendrait donc difficile la liaison avec les bibliothèques et, au lieu de définir un nombre arbitraire de pointeurs consignés, ils y allaient avec un. Les langues de niveau supérieur n'ont pas vraiment la même excuse.
la source
Un grand nombre des cas d'utilisation où vous auriez utilisé plusieurs valeurs de retour dans le passé ne sont tout simplement plus nécessaires avec les fonctionnalités de langage modernes. Vous voulez retourner un code d'erreur? Lancer une exception ou renvoyer un
Either<T, Throwable>
. Vous voulez renvoyer un résultat optionnel? Retourner unOption<T>
. Vous voulez renvoyer un ou plusieurs types? Renvoie uneEither<T1, T2>
ou une union marquée.Et même dans les cas où vous devez réellement renvoyer plusieurs valeurs, les langages modernes prennent généralement en charge des n-uplets ou un type de structure de données (liste, tableau, dictionnaire) ou des objets, ainsi qu'une forme de liaison de déstructuration ou de filtrage, qui permet de vos valeurs multiples en une seule valeur et ensuite la structurer à nouveau en plusieurs valeurs triviales.
Voici quelques exemples de langages qui ne prennent pas en charge le renvoi de plusieurs valeurs. Je ne vois pas vraiment comment la prise en charge de plusieurs valeurs de retour les rendrait beaucoup plus expressives pour compenser le coût d'une nouvelle fonctionnalité de langage.
Rubis
Python
Scala
Haskell
Perl6
la source
sort
fonction Matlab :sorted = sort(array)
renvoie uniquement le tableau trié, alors que[sorted, indices] = sort(array)
renvoie les deux. Le seul moyen auquel je puisse penser en Python serait de passer un drapeau danssort
le sens desort(array, nout=2)
ousort(array, indices=True)
.[a, b, c] = func(some, thing)
) et agir en conséquence. Ceci est utile, par exemple, si le calcul du premier argument de sortie est bon marché mais que le calcul du deuxième argument est coûteux. Je ne connais aucune autre langue dans laquelle l'équivalent de Matlabsnargout
est disponible au moment de l'exécution.sorted, _ = sort(array)
.sort
fonction peut dire qu'elle n'a pas besoin de calculer les indices? C'est cool, je ne le savais pas.La véritable raison pour laquelle une valeur de retour unique est si populaire est les expressions utilisées dans de nombreuses langues. Dans toutes les langues où vous pouvez avoir une expression telle que
x + 1
vous pensez déjà en termes de valeurs de retour uniques, vous évaluez une expression dans votre tête en la décomposant en plusieurs morceaux et en décidant la valeur de chaque élément. Vous regardezx
et décidez que sa valeur est 3 (par exemple), et vous regardez 1 et ensuite vous regardezx + 1
et mettez tout cela ensemble pour décider que la valeur du tout est 4. Chaque partie syntaxique de l'expression a une valeur, pas un autre nombre de valeurs; c'est la sémantique naturelle des expressions que tout le monde attend. Même lorsqu'une fonction retourne une paire de valeurs, elle renvoie toujours une valeur qui remplit deux fonctions, car l'idée d'une fonction qui renvoie deux valeurs qui ne sont pas regroupées dans une seule collection est trop étrange.Les gens ne veulent pas traiter de la sémantique alternative qui serait nécessaire pour que les fonctions renvoient plus d'une valeur. Par exemple, dans un langage basé sur la pile, tel que Forth, vous pouvez avoir un nombre quelconque de valeurs de retour, car chaque fonction modifie simplement le haut de la pile, faisant apparaître les entrées et poussant les sorties à volonté. C'est pourquoi Forth n'a pas le genre d'expressions que les langages normaux ont.
Perl est un autre langage qui peut parfois agir comme si les fonctions renvoyaient plusieurs valeurs, même s'il est généralement considéré comme renvoyant une liste. La liste de chemins "interpoler" en Perl nous donne des listes comme celles-
(1, foo(), 3)
ci peuvent comporter 3 éléments comme le voudraient la plupart des personnes ne connaissant pas Perl, mais pouvant tout aussi bien ne comporter que 2 éléments, 4 éléments ou plus grand nombre d'éléments dépendant defoo()
. Les listes en Perl sont aplaties, de sorte qu'une liste syntaxique n'a pas toujours la sémantique d'une liste; il peut s'agir simplement d'un morceau d'une liste plus longue.Une autre méthode permettant aux fonctions de renvoyer plusieurs valeurs consiste à utiliser une sémantique d'expression alternative, chaque expression pouvant avoir plusieurs valeurs et chaque valeur représentant une possibilité. Reprenez
x + 1
, mais imaginez que cette foisx
ait deux valeurs {3, 4}, alors les valeurs dex + 1
seraient {4, 5} et les valeurs dex + x
{6, 8}, ou peut-être {6, 7, 8} , selon qu'une évaluation est autorisée à utiliser plusieurs valeurs pourx
. Un langage comme celui-ci pourrait être implémenté en utilisant le retour arrière, un peu comme le fait Prolog pour donner plusieurs réponses à une requête.En bref, un appel de fonction est une seule unité syntaxique et une seule unité syntaxique a une valeur unique dans la sémantique de l'expression que nous connaissons et aimons tous. Toute autre sémantique vous obligerait à faire des choses étranges, comme Perl, Prolog ou Forth.
la source
Comme suggéré dans cette réponse , il s’agit d’une prise en charge matérielle, bien que la conception traditionnelle du langage joue également un rôle.
Parmi les trois premières langues, le Fortran, le Lisp et le COBOL, la première utilisait une seule valeur de retour car elle était modelée sur les mathématiques. La seconde a renvoyé un nombre arbitraire de paramètres de la même manière qu’elle les a reçus: sous forme de liste (on pourrait également affirmer qu’elle n’a fait que transmettre et renvoyer un seul paramètre: l’adresse de la liste). Le troisième retourne zéro ou une valeur.
Ces premières langues ont beaucoup influencé la conception des langues qui les ont suivies, bien que Lisp, la seule qui ait renvoyé de multiples valeurs, n’ait jamais été très appréciée.
Lorsque C est arrivé, bien que influencé par les langages précédents, il accordait une importance particulière à l’utilisation efficace des ressources matérielles, tout en maintenant une association étroite entre ce que faisait le langage C et le code machine qui l’implémentait. Certaines de ses caractéristiques les plus anciennes, telles que les variables "auto" ou "registre", sont le résultat de cette philosophie de conception.
Il faut également souligner que le langage d'assemblage était très populaire jusque dans les années 80, quand il a finalement commencé à disparaître du développement traditionnel. Les personnes qui écrivaient des compilateurs et créaient des langages étaient habitués à l’assemblage et, pour l’essentiel, restaient fidèles à ce qui fonctionnait le mieux là-bas.
La plupart des langues qui ont divergé par rapport à cette norme n’ont jamais rencontré une grande popularité et n’ont donc jamais joué un rôle important en influençant les décisions des concepteurs de langages (qui, bien sûr, s’inspiraient de ce qu’ils savaient).
Allons donc examiner le langage d'assemblage. Examinons tout d’abord le 6502 , un microprocesseur de 1975, qui était utilisé par les micro-ordinateurs Apple II et VIC-20. Il était très faible par rapport à ce qui était utilisé dans les ordinateurs centraux et les mini-ordinateurs de l’époque, bien que puissant par rapport aux premiers ordinateurs datant de 20 ou 30 ans plus tôt, à l’aube des langages de programmation.
Si vous regardez la description technique, il y a 5 registres plus quelques drapeaux à un bit. Le seul registre "complet" était le compteur de programme (PC) - ce registre pointe vers l'instruction suivante à exécuter. L’autre enregistre l’accumulateur (A), deux registres "index" (X et Y) et un pointeur de pile (SP).
L'appel d'un sous-programme place le PC dans la mémoire indiquée par le SP, puis décrémente le SP. Le retour d'un sous-programme fonctionne en sens inverse. On peut pousser et extraire d'autres valeurs sur la pile, mais il est difficile de faire référence à la mémoire par rapport au SP, il était donc difficile d' écrire des sous-routines réentrantes . Cette chose que nous prenons pour acquis, appeler un sous-programme à tout moment, n’est pas si commune sur cette architecture. Souvent, une "pile" distincte serait créée afin que les paramètres et l'adresse de retour du sous-programme soient conservés séparément.
Si vous regardez le processeur qui a inspiré le 6502, le 6800 , il possédait un registre supplémentaire, le registre d'index (IX), aussi large que le SP, qui pouvait recevoir la valeur du SP.
Sur la machine, appeler un sous-programme rentrant consistait à appliquer les paramètres à la pile, à PC, à changer d’ordinateur à la nouvelle adresse, puis le sous-programme poussait ses variables locales sur la pile . Le nombre de variables et de paramètres locaux étant connu, vous pouvez les adresser par rapport à la pile. Par exemple, une fonction recevant deux paramètres et ayant deux variables locales ressemblerait à ceci:
Il peut être appelé n'importe quel nombre de fois, car tout l'espace temporaire est sur la pile.
Le 8080 , utilisé sur le TRS-80 et une multitude de micro-ordinateurs basés sur CP / M, pourrait faire quelque chose de similaire au 6800, en plaçant SP sur la pile, puis en la plaçant sur son registre indirect, HL.
C’est un moyen très courant d’implémenter les choses, et il bénéficie d’un soutien encore plus grand pour les processeurs plus modernes, avec le pointeur de base qui permet de vider toutes les variables locales avant de revenir facilement.
Le problème, comment est- ce que vous retournez quelque chose ? Les registres de processeurs n’étaient pas très nombreux au début, et il fallait souvent en utiliser certains même pour savoir à quel morceau de mémoire s’adresser. Retourner des choses sur la pile serait compliqué: il faudrait tout sauter, sauvegarder le PC, pousser les paramètres de retour (qui seraient stockés entre-temps?), Puis repousser le PC et revenir.
Donc, ce qui était généralement fait était de réserver un registre pour la valeur de retour. Le code appelant savait que la valeur de retour se trouverait dans un registre particulier, qu'il faudrait conserver jusqu'à ce qu'il puisse être sauvegardé ou utilisé.
Regardons un langage qui autorise plusieurs valeurs de retour: Forth. Forth conserve une pile de retour (RP) et une pile de données (SP) distinctes, de sorte qu’une fonction n’a à faire que de supprimer tous ses paramètres et de laisser les valeurs de retour dans la pile. Étant donné que la pile de retour était séparée, cela ne gênait pas.
En tant que personne ayant appris le langage d'assemblage et Forth au cours des six premiers mois d'expérience en informatique, les valeurs de retour multiples me paraissent tout à fait normales. Des opérateurs tels que celui de Forth
/mod
, qui renvoie la division entière et le reste, semblent évidents. D'autre part, je peux facilement voir comment une personne dont l'expérience initiale était l'esprit C trouve ce concept étrange: cela va à l'encontre de leurs attentes profondes de ce qu'est une "fonction".Pour ce qui est des maths ... eh bien, je programmais bien les ordinateurs avant d’avoir des fonctions dans les cours de mathématiques. Il y a toute une section de CS et de langages de programmation qui sont influencés par les mathématiques, mais, encore une fois, il y a toute une section qui ne l'est pas.
Nous avons donc une confluence de facteurs dans lesquels les mathématiques ont influencé la conception linguistique précoce, où les contraintes matérielles dictaient ce qui était facilement implémenté, et où les langages populaires influençaient l'évolution du matériel (les processeurs Lisp et Forth étaient des destructeurs de processus dans ce processus).
la source
Les langages fonctionnels que je connais peuvent facilement renvoyer plusieurs valeurs en utilisant des n-uplets (dans les langages à typage dynamique, vous pouvez même utiliser des listes). Les tuples sont également supportés dans d'autres langues:
Dans l'exemple ci-dessus,
f
une fonction renvoyant 2 ints.De même, ML, Haskell, F #, etc., peuvent également renvoyer des structures de données (les pointeurs sont trop bas pour la plupart des langues). Je n'ai pas entendu parler d'un langage GP moderne avec une telle restriction:
Enfin, les
out
paramètres peuvent être émulés même dans les langages fonctionnels parIORef
. Il y a plusieurs raisons pour lesquelles il n'y a pas de support natif pour les variables out dans la plupart des langues:Sémantique peu claire : la fonction suivante affiche -t-elle 0 ou 1? Je connais des langues qui afficheraient 0 et des langues 1. Les utilisateurs en retirent des avantages (en termes de performances et de correspondance avec le modèle mental du programmeur):
Effets non localisés : comme dans l'exemple ci-dessus, vous pouvez constater que vous pouvez avoir une longue chaîne et que la fonction la plus interne affecte l'état global. En général, il est plus difficile de déterminer les exigences de la fonction et de déterminer si le changement est légal. Étant donné que la plupart des paradigmes modernes tentent soit de localiser les effets (encapsulation dans la POO), soit d'éliminer les effets secondaires (programmation fonctionnelle), cela entre en conflit avec ces paradigmes.
Être redondant : Si vous avez des n-uplets, vous disposez de 99% des fonctionnalités des
out
paramètres et de 100% d'utilisation idiomatique. Si vous ajoutez des pointeurs au mélange, vous couvrez le 1% restant.J'ai du mal à nommer une langue qui ne peut pas renvoyer plusieurs valeurs en utilisant un tuple, une classe ou un
out
paramètre (et dans la plupart des cas, deux méthodes ou plus sont autorisées).la source
Je pense que c'est à cause d' expressions , telles que
(a + b[i]) * c
.Les expressions sont composées de valeurs "singulières". Une fonction renvoyant une valeur singulière peut donc être directement utilisée dans une expression, à la place de l’une des quatre variables ci-dessus. Une fonction à sorties multiples est au moins quelque peu maladroite dans une expression.
Personnellement , je pense que c'est la chose qui est spécial au sujet d' une valeur de retour singulier. Vous pouvez contourner ce problème en ajoutant une syntaxe permettant de spécifier laquelle des multiples valeurs de retour vous souhaitez utiliser dans une expression, mais elle sera forcément plus maladroite que la bonne vieille notation mathématique, qui est concise et familière à tout le monde.
la source
Cela complique un peu la syntaxe, mais il n'y a aucune bonne raison de ne pas le permettre au niveau de la mise en œuvre. Contrairement à certaines des autres réponses, le renvoi de plusieurs valeurs, le cas échéant, permet d'obtenir un code plus clair et plus efficace. Je ne peux pas compter combien de fois j'ai souhaité pouvoir renvoyer un X et un Y, ou un booléen "success" et une valeur utile.
la source
[out]
paramètre, mais presque toutes renvoient unHRESULT
(code d'erreur). Il serait très pratique de simplement en avoir une paire. Dans les langues qui supportent bien les n-uplets, tels que Python, cela est utilisé dans beaucoup de code que j'ai vu.sort
fonction trie normalement un tableau:sorted_array = sort(array)
. Parfois , je dois aussi les indices correspondants:[sorted_array, indices] = sort(array)
. Parfois, je ne veux que les indices:[~, indices]
= sort (tableau). The function
sort` peut indiquer le nombre d'arguments de sortie nécessaires. Par conséquent, si un travail supplémentaire est nécessaire pour 2 sorties par rapport à un, il ne peut calculer ces sorties que si nécessaire.Dans la plupart des langues où les fonctions sont prises en charge, vous pouvez utiliser un appel de fonction n’importe où une variable de ce type peut être utilisée: -
Si la fonction renvoie plus d'une valeur, cela ne fonctionnera pas. Les langages à typage dynamique tels que python vous permettront de le faire, mais, dans la plupart des cas, une erreur d'exécution sera générée, à moins que cela ne fonctionne bien avec un tuple au milieu d'une équation.
la source
foo()[0]
Je veux juste construire sur la réponse de Harvey. J'ai initialement trouvé cette question sur un site de technologie de l'information (arstechnica) et trouvé une explication étonnante qui, à mon avis, répond vraiment au cœur de la question et manque de toutes les autres réponses (à l'exception de Harvey's):
L'origine du retour simple à partir de fonctions réside dans le code machine. Au niveau du code machine, une fonction peut renvoyer une valeur dans le registre A (accumulateur). Toutes les autres valeurs de retour seront sur la pile.
Un langage qui prend en charge deux valeurs de retour le compilera en tant que code machine qui en renvoie une et place la seconde sur la pile. En d'autres termes, la deuxième valeur renvoyée finirait quand même par être un paramètre out.
C'est comme demander pourquoi l'affectation est une variable à la fois. Vous pouvez avoir une langue qui autorise a, b = 1, 2 par exemple. Mais cela aboutirait au niveau de code machine étant a = 1 suivi de b = 2.
Il y a des raisons de penser que les constructions en langage de programmation ont un semblant de ce qui se passera réellement lorsque le code sera compilé et en cours d'exécution.
la source
Cela a commencé avec les mathématiques. Fortran, nommé pour "traduction de formule" était le premier compilateur. Fortran était et est orienté vers la physique / maths / ingénierie.
COBOL, presque aussi vieux, n'avait pas de valeur de retour explicite; Il y avait à peine des sous-programmes. Depuis lors, c'est principalement l'inertie.
Go , par exemple, a plusieurs valeurs de retour et le résultat est plus net et moins ambigu que l'utilisation de paramètres "out". Après un peu d'utilisation, c'est très naturel et efficace. Je recommande que plusieurs valeurs de retour soient considérées pour toutes les nouvelles langues. Peut-être aussi pour les langues anciennes.
la source
Cela tient probablement davantage à l'héritage laissé par les appels de fonction dans les instructions machine du processeur et au fait que tous les langages de programmation dérivent du code machine: par exemple, C -> Assemblage -> Machine.
Comment les processeurs effectuent les appels de fonction
Les premiers programmes ont été écrits en code machine, puis en assemblage. Les processeurs ont pris en charge les appels de fonction en plaçant une copie de tous les registres actuels dans la pile. Si vous revenez de la fonction, le jeu de registres sauvegardé sortira de la pile. Habituellement, un registre était laissé vierge pour permettre à la fonction renvoyée de renvoyer une valeur.
Maintenant, pour ce qui est de savoir pourquoi les processeurs ont été conçus de cette façon… c'était probablement une question de ressources limitées.
la source