Pourquoi la plupart des langages de programmation ne prennent-ils en charge que le renvoi d’une valeur unique à partir d’une fonction? [fermé]

118

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?

M4N
la source
40
parce que vous pouvez retourner un tableau ...
nathan hayfield
6
Alors pourquoi ne pas permettre qu'un seul argument? Je soupçonne que c'est lié à la parole. Prenez quelques idées, insérez-les dans une chose que vous revenez à quiconque est assez chanceux / malheureux pour être à l'écoute. Le retour est presque comme une opinion. "ceci" est ce que vous faites avec "ceux-ci".
Erik Reppen
30
Je crois que l'approche de python est assez simple et élégant: quand vous devez retourner plusieurs valeurs il suffit de retourner un tuple: 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.
Bakuriu
7
Je peux vous informer que Matlab a un nombre variable de valeurs de retour. Le nombre d'arguments en sortie est déterminé par la signature de l'appelant (par exemple, [a, b] = f()vs [a, b, c] = f()) et obtenu à l'intérieur fpar nargout. Je ne suis pas un grand fan de Matlab, mais cela s'avère assez pratique parfois.
gerrit
5
Je pense que si la plupart des langages de programmation sont conçus de cette façon, c'est discutable. Dans l’histoire des langages de programmation, certains langages très populaires ont été créés de cette manière (comme Pascal, C, C ++, Java, VB classique), mais il existe aussi de nombreux autres langages qui attirent de plus en plus de fans et permettent des retours multiples. valeurs.
Doc Brown

Réponses:

58

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:

  • Les fonctions qui renvoient plusieurs valeurs sont difficiles à nommer clairement .
  • Il est facile de se tromper dans l'ordre des valeurs de retour

    (password, username) = GetUsernameAndPassword()  
    

    (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!)

  • Les langues POO ont déjà une meilleure alternative aux valeurs de retour multiples: les classes.
    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.

BlueRaja - Danny Pflughoeft
la source
50
Il est difficile de dire que rendre un tuple revient à renvoyer plusieurs choses. Son retour un tuple . Le code que vous avez écrit vient de le décompresser proprement en utilisant du sucre syntaxique.
10
@Lego: Je ne vois pas de distinction - un tuple est par définition multiple. Que considéreriez-vous comme "valeurs de retour multiples", sinon?
BlueRaja - Danny Pflughoeft
19
C'est une distinction très floue, mais considérons un tuple vide (). Est-ce une chose ou zéro choses? Personnellement, je dirais que c'est une chose. Je peux assigner x = ()très bien, tout comme je peux assigner x = randomTuple(). Dans ce dernier cas, si le tuple renvoyé est vide ou non, je peux toujours affecter le tuple renvoyé à x.
19
... Je n'ai jamais prétendu que les tuples ne pouvaient pas être utilisés pour autre chose. Mais arguer que "Python ne supporte pas les valeurs de retour multiples, il supporte les n-uplets" est simplement extrêmement pédant. C'est toujours une réponse correcte.
BlueRaja - Danny Pflughoeft
14
Ni les tuples ni les classes ne sont des "valeurs multiples".
Andres F.
54

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 outparamè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.

Telastyn
la source
5
@FrustratedWithFormsDesigner - cela est apparu un peu dans une question récente . Je peux compter sur une main le nombre de fois où je voulais plusieurs sorties en 20 ans.
Telastyn
61
Les fonctions en mathématiques et les fonctions dans la plupart des langages de programmation sont deux bêtes très différentes.
tdammers
17
Au début, ils étaient très similaires dans leurs pensées. Fortran, pascal, etc., étaient fortement influencés par les mathématiques plutôt que par l'architecture informatique.
9
@tdammers - comment ça va? Je veux dire que pour la plupart des langues, cela se résume au lambda calcul à la fin - une entrée, une sortie, aucun effet secondaire. Tout le reste est une simulation / bidouille en plus de cela. Les fonctions de programmation peuvent ne pas être pures dans le sens où plusieurs entrées peuvent produire la même sortie mais l’esprit est là.
Telastyn
16
@SteveEvers: il est regrettable que le nom "fonction" ait pris le dessus dans la programmation impérative, au lieu de "procédure" ou "routine" plus appropriée. En programmation fonctionnelle, une fonction ressemble beaucoup plus étroitement aux fonctions mathématiques.
tdammers
35

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 une Maybe<T>monade au lieu d’utiliser un paramètre out. Pensez à ce code en F #:

let s = "1"
match tryParse s with
| Some(i) -> // do whatever with i
| None -> // failed to parse

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.

Steven Evers
la source
1
De plus, j'aimerais vraiment pouvoir écrire en C #: 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.
Grimace of Despair le
10
@GrimaceofDespair ce que vous voulez vraiment, c'est une syntaxe de déstructuration, et non plusieurs valeurs de retour.
Domenic
2
@ Warren: Oui. Voir ici et notez qu'une telle solution ne serait pas continue: fr.wikipedia.org/wiki/Well-definition
Steven Evers
4
La notion mathématique de définition précise n'a rien à voir avec le nombre de sorties renvoyées par une fonction. Cela signifie que les sorties seront toujours les mêmes si l'entrée est la même. Strictement parlant, les fonctions mathématiques renvoient une valeur, mais cette valeur est souvent un tuple. Pour un mathématicien, il n'y a essentiellement aucune différence entre cela et le retour de plusieurs valeurs. Les arguments selon lesquels les fonctions de programmation ne doivent renvoyer qu'une valeur, car ce que font les fonctions mathématiques ne sont pas très convaincants.
Michael Siler
1
@MichaelSiler (Je suis d' accord avec votre commentaire) Mais s'il vous plaît noter que l' argument est réversible: « arguments que les fonctions du programme peuvent retourner des valeurs multiples , car ils peuvent retourner une seule valeur de tuple ne sont pas très convaincants soit » :)
Andres F.
23

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.

David
la source
Ah! Point de détail très intéressant. +1!
Steven Evers le
Cela devrait être la réponse acceptée. Généralement, les gens pensent aux machines cibles lorsqu'ils construisent des compilateurs. Une autre analogie est la raison pour laquelle nous avons int, float, char / string, etc. parce que c'est ce qui est pris en charge par la machine cible. Même si la cible n’est pas du métal nu (jvm, par exemple), vous souhaitez tout de même obtenir des performances décentes en ne pas trop émuler.
Imel96
26
... Vous pouvez facilement définir une convention d'appel pour renvoyer plusieurs valeurs d'une fonction, de la même manière que vous pouvez définir une convention d'appel pour transmettre plusieurs valeurs à une fonction. Ce n'est pas une réponse. -1
BlueRaja - Danny Pflughoeft
2
Il serait intéressant de savoir si les moteurs d’exécution basés sur des piles (JVM, CLR) ont déjà envisagé / autorisé plusieurs valeurs de retour. Cela devrait être assez facile; l'appelant doit simplement faire apparaître le bon nombre de valeurs, tout comme il pousse le bon nombre d'arguments!
Lorenzo Dematté
1
@David Non, cdecl autorise un nombre (théoriquement) illimité de paramètres (c’est pourquoi les fonctions varargs sont possibles) . Certains compilateurs C peuvent vous limiter à plusieurs dizaines ou cent arguments par fonction, ce qui, à mon avis, reste plus que raisonnable -_-
BlueRaja - Danny Pflughoeft
18

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 un Option<T>. Vous voulez renvoyer un ou plusieurs types? Renvoie une Either<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

def foo; return 1, 2, 3 end

one, two, three = foo

one
# => 1

three
# => 3

Python

def foo(): return 1, 2, 3

one, two, three = foo()

one
# >>> 1

three
# >>> 3

Scala

def foo = (1, 2, 3)

val (one, two, three) = foo
// => one:   Int = 1
// => two:   Int = 2
// => three: Int = 3

Haskell

let foo = (1, 2, 3)

let (one, two, three) = foo

one
-- > 1

three
-- > 3

Perl6

sub foo { 1, 2, 3 }

my ($one, $two, $three) = foo

$one
# > 1

$three
# > 3
Jörg W Mittag
la source
1
Je pense qu’un aspect est que, dans certaines langues (telles que Matlab), une fonction peut être flexible quant au nombre de valeurs qu’elle renvoie; voir mon commentaire ci-dessus . Il n’ya pas beaucoup d’aspects dans Matlab que je n’aime pas, mais c’est l’une des rares (peut-être la seule) fonctionnalité qui me manque lors du transfert de Matlab vers, par exemple, Python.
gerrit
1
Mais qu'en est-il des langages dynamiques comme Python ou Ruby? Supposons que j'écrive quelque chose comme la sortfonction 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 dans sortle sens de sort(array, nout=2)ou sort(array, indices=True).
gerrit
2
@ MikeCellini Je ne pense pas. Une fonction peut dire avec combien d' arguments de sortie la fonction est appelée ( [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 Matlabs nargoutest disponible au moment de l'exécution.
Gerrit
1
@gerrit la bonne solution en Python est d'écrire ceci: sorted, _ = sort(array).
Miles Rout
1
@MilesRout: Et la sortfonction peut dire qu'elle n'a pas besoin de calculer les indices? C'est cool, je ne le savais pas.
Jörg W Mittag
12

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 + 1vous 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 regardez xet décidez que sa valeur est 3 (par exemple), et vous regardez 1 et ensuite vous regardezx + 1et 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 de foo(). 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 fois xait deux valeurs {3, 4}, alors les valeurs de x + 1seraient {4, 5} et les valeurs de x + x{6, 8}, ou peut-être {6, 7, 8} , selon qu'une évaluation est autorisée à utiliser plusieurs valeurs pour x. 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.

Géo
la source
9

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.

quand une fonction retourne, elle laisse un pointeur sur l'objet retourné dans un registre spécifique

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:

SP + 8: param 2
SP + 6: param 1
SP + 4: return address
SP + 2: local 2
SP + 0: local 1

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

Daniel C. Sobral
la source
@gnat L'utilisation de "informer" comme dans "fournir la qualité essentielle" était intentionnelle.
Daniel C. Sobral
n'hésitez pas à revenir en arrière si cela vous tient à cœur; par mon influence de lecture convient un peu mieux ici: "affecte ... d'une manière importante"
gnat
1
+1 Comme il est souligné, le nombre insuffisant de registres des processeurs anciens comparé à un ensemble de registres abondant de processeurs modernes (également utilisé dans de nombreux ABI, par exemple x64 abi) pourrait changer la donne et les raisons autrefois convaincantes de ne renvoyer qu'une valeur pourrait de nos jours juste être une raison historique.
BitTickler
Je ne suis pas convaincu que les premiers microprocesseurs 8 bits aient beaucoup d'influence sur la conception de la langue et sur les éléments supposés nécessaires dans les conventions d'appel C ou Fortran, quelle que soit leur architecture. Fortran suppose que vous pouvez passer des arguments de tableau (essentiellement des pointeurs). Vous avez donc déjà de gros problèmes d’implémentation pour Fortran normal sur des machines telles que 6502, en raison d’un manque de modes d’adressage pour pointeur + index, comme indiqué dans votre réponse et dans Pourquoi les compilateurs de C à Z80 produisent-ils un code médiocre? sur retrocomputing.SE.
Peter Cordes
Fortran, comme C, suppose également que vous pouvez passer un nombre arbitraire d'arguments et y avoir un accès aléatoire, ainsi qu'un nombre arbitraire d'habitants, n'est-ce pas? Comme vous venez de l'expliquer, vous ne pouvez pas le faire facilement sur 6502 car l'adressage relatif à la pile n'est pas une chose, à moins d'abandonner la réentrance. Vous pouvez les insérer dans un stockage statique. Si vous pouvez passer une liste d'arguments arbitraire, vous pouvez ajouter des paramètres cachés supplémentaires pour les valeurs de retour (par exemple au-delà de la première) qui ne rentrent pas dans les registres.
Peter Cordes
7

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:

f :: Int -> (Int, Int)
f x = (x - 1, x + 1)

// Even C++ have tuples - see Boost.Graph for use
std::pair<int, int> f(int x) {
  return std::make_pair(x - 1, x + 1);
}

Dans l'exemple ci-dessus, fune 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:

data MyValue = MyValue Int Int

g :: Int -> MyValue
g x = MyValue (x - 1, x + 1)

Enfin, les outparamètres peuvent être émulés même dans les langages fonctionnels par IORef. 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):

    int x;
    
    int f(out int y) {
      x = 0;
      y = 1;
      printf("%d\n", x);
    }
    f(out x);
    
  • 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 outparamè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 outparamètre (et dans la plupart des cas, deux méthodes ou plus sont autorisées).

Maciej Piechotka
la source
+1 Pour indiquer comment les langages fonctionnels gèrent cela de manière élégante et sans douleur.
Andres F.
1
Techniquement, vous retournez toujours une seule valeur: D (c'est simplement que cette valeur unique est triviale à décomposer en plusieurs valeurs).
Thomas Eding
1
Je dirais qu'un paramètre avec une véritable sémantique "out" devrait se comporter comme un compilateur temporaire qui est copié vers la destination quand une méthode se termine normalement; une variable sémantique "inout" doit se comporter comme un compilateur temporaire chargé à partir de la variable transmise en entrée et réécrit à la sortie; une avec une sémantique "ref" devrait se comporter comme un alias. Les paramètres dits "out" de C # sont en réalité des paramètres "ref" et se comportent comme tels.
Supercat
1
Le tuple "solution de contournement" n'est pas gratuit. Il bloque les opportunités d'optimisation. S'il existait une ABI autorisant le renvoi de N valeurs de retour dans les registres de la CPU, le compilateur pourrait en fait l'optimiser au lieu de créer une instance de tuple et de la construire.
BitTickler
1
@BitTickler rien n'empêche de renvoyer les n premiers champs de struct à transmettre par les registres si vous contrôlez ABI.
Maciej Piechotka
6

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.

Roman Starkov
la source
4

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.

Ddyer
la source
3
Pourriez-vous donner un exemple où plusieurs déclarations fournissent un code plus clair et / ou plus efficace?
Steven Evers le
3
Par exemple, dans la programmation COM C ++, de nombreuses fonctions ont un [out]paramètre, mais presque toutes renvoient un HRESULT(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.
Felix Dombek
Dans certaines langues, vous renverriez un vecteur avec les coordonnées X et Y, et le renvoi de toute valeur utile équivaudrait à "succès", les exceptions pouvant éventuellement porter cette valeur utile étant utilisées en cas d'échec.
Doppelgreener
3
Vous finissez souvent par coder des informations dans la valeur de retour de manière non évidente - c'est-à-dire; les valeurs négatives sont des codes d'erreur, les valeurs positives sont des résultats. Yuk. En accédant à une table de hachage, il est toujours compliqué d'indiquer si l'élément a été trouvé et de le renvoyer.
Ddyer
@SteveEvers La Matlab sortfonction 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.
gerrit
2

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: -

x = n + sqrt(y);

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.

James Anderson
la source
5
Juste n'utilisez pas de fonctions inappropriées. Ce n'est pas différent du "problème" posé par les fonctions qui ne renvoient aucune valeur ou ne renvoient pas de valeurs non numériques.
Ddyer
3
Dans les langues que j'ai utilisées et qui offrent plusieurs valeurs de retour (SciLab, par exemple), la première valeur de retour est privilégiée et sera utilisée dans les cas où une seule valeur est nécessaire. Donc pas de vrai problème là-bas.
Le Photon
Et même s'ils ne le sont pas, comme avec le décompression des foo()[0]
tuples de
Exactement, si une fonction retourne 2 valeurs, son type de retour est 2 valeurs, pas une valeur unique. Le langage de programmation ne doit pas lire dans vos pensées.
Mark E. Haase
1

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.

utilisateur2202911
la source
Si les langages de bas niveau tels que C prenaient en charge plusieurs valeurs de retour en tant que fonction de première classe, les conventions d’appel du langage C incluraient plusieurs registres de valeur de retour, de la même manière que jusqu’à 6 registres sont utilisés pour passer des arguments de fonction entier / pointeur dans x86. 64 ABI System V. (En fait, x86-64 SysV renvoie des structures allant jusqu’à 16 octets dans la paire de registres RDX: RAX. C’est bien si la structure vient d’être chargée et sera stockée, mais coûte un déconditionnement supplémentaire par rapport au fait d’avoir des membres séparés dans des registres séparés même s'ils sont plus petits que 64 bits)
Peter Cordes
La convention évidente serait RAX, puis le règlement qui passe. (RDI, RSI, RDX, RCX, R8, R9). Ou dans la convention Windows x64, RCX, RDX, R8, R9. Mais comme C n’a pas de manière native plusieurs valeurs de retour, les conventions d’appel / ABI de C ne spécifient que plusieurs registres de retour pour les entiers larges et certaines structures. Voir Procédure Appel standard pour l'architecture ARM®: 2 valeurs de retour distinctes, mais liées, pour un exemple d'utilisation d'un int de large afin que le compilateur soit efficace pour la réception de 2 valeurs de retour sur ARM.
Peter Cordes
-1

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.

RickyS
la source
4
cela ne répond pas à la question posée
gnat
@gn en ce qui me concerne il répond. OTOH on doit déjà avoir un peu de fond pour comprendre, et cette personne ne posera probablement pas la question ...
Netch
@Netch n’a guère besoin de beaucoup d’arrière-plan pour comprendre que des déclarations telles que "FORTRAN ... a été le premier compilateur" sont un véritable gâchis. Ce n'est même pas faux. Tout comme le reste de cette "réponse"
gnat
link indique qu'il y avait eu des tentatives précédentes de compilation, mais que "l'équipe de FORTRAN dirigée par John Backus chez IBM est généralement considérée comme ayant introduit le premier compilateur complet en 1957". La question posée était pourquoi un seul? Comme j'ai dit. C'était principalement des mathématiques et de l'inertie. La définition mathématique du terme "Fonction" nécessite exactement une valeur de résultat. C'était donc une forme familière.
RickyS
-2

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.

Harvey
la source