Quel est l'avantage d'avoir l'opérateur d'affectation renvoyer une valeur?

27

Je développe un langage que j'ai l'intention de remplacer à la fois Javascript et PHP. (Je ne vois aucun problème avec cela. Ce n'est pas comme si l'une de ces langues avait une grande base d'installation.)

L'une des choses que je voulais changer était de transformer l'opérateur d'affectation en une commande d'affectation, supprimant ainsi la possibilité d'utiliser la valeur renvoyée.

x=1;          /* Assignment. */
if (x==1) {}  /* Comparison. */
x==1;         /* Error or warning, I've not decided yet. */
if (x=1) {}   /* Error. */

Je sais que cela signifierait que ces fonctions en ligne que les gens aiment tant ne fonctionneraient plus. Je me suis dit (avec peu de preuves au-delà de mon expérience personnelle) que la grande majorité des cas, cela était vraiment destiné à être une opération de comparaison.

Ou est-ce? Existe-t-il des utilisations pratiques de la valeur de retour de l'opérateur d'affectation qui n'ont pas pu être réécrites de manière triviale? (Pour toute langue ayant un tel concept.)

billpg
la source
12
JS et PHP n'ont pas une grande "base d'installation"?
2014
47
@mri Je soupçonne un sarcasme.
Andy Hunt
12
Le seul cas utile dont je me souvienne est while((x = getValue()) != null) {}. Les remplacements seront plus moches car vous devrez soit utiliser breaksoit répéter l' x = getValueaffectation.
CodesInChaos
12
@mri Ooh non, j'ai entendu dire que ces deux langues ne sont que des choses triviales sans aucun investissement important. Une fois que les quelques personnes qui insistent pour utiliser JS voient ma langue, elles basculeront vers la mienne et n'auront plus jamais à écrire ===. Je suis également sûr que les fabricants de navigateurs déploieront immédiatement une mise à jour qui inclut ma langue aux côtés de JS. :)
billpg
4
Je vous suggère que si votre intention est d' améliorer une langue existante et que vous souhaitez qu'elle soit largement adoptée, la compatibilité à 100% avec la langue existante est bonne. Voir TypeScript comme exemple. Si votre intention est de fournir une meilleure alternative à une langue existante, vous avez un problème beaucoup plus difficile. Une nouvelle langue doit résoudre un problème réaliste existant beaucoup mieux que la langue existante afin de payer le coût du changement. Apprendre une langue est un investissement et doit être rentable.
Eric Lippert

Réponses:

25

Techniquement, certains sucres syntaxiques peuvent être conservés même s'ils peuvent être remplacés de manière triviale, s'ils améliorent la lisibilité de certaines opérations courantes. Mais l'affectation en tant qu'expression ne relève pas de cela. Le danger de le taper à la place d'une comparaison signifie qu'il est rarement utilisé (parfois même interdit par les guides de style) et provoque une double prise chaque fois qu'il est utilisé. En d'autres termes, les avantages de lisibilité sont faibles en nombre et en ampleur.

Un regard sur les langues existantes qui le font peut être utile.

  • Java et C # conservent l'affectation une expression mais éliminent l'écueil que vous mentionnez en exigeant des conditions pour évaluer les booléens. Cela semble surtout bien fonctionner, bien que les gens se plaignent parfois que cela interdit les conditions comme if (x)à la place if (x != null)ou if (x != 0)selon le type de x.
  • Python fait de l'affectation une instruction appropriée au lieu d'une expression. Des propositions pour changer cela atteignent parfois la liste de diffusion python-ideas, mais mon impression subjective est que cela se produit plus rarement et génère moins de bruit à chaque fois par rapport à d'autres fonctionnalités "manquantes" comme les boucles do-while, les instructions switch, les lambdas multi-lignes, etc.

Cependant, Python permet un cas particulier, l' attribution de plusieurs noms à la fois: a = b = c. Ceci est considéré comme une déclaration équivalente à b = c; a = b, et il est parfois utilisé, il peut donc être utile d'ajouter à votre langue également (mais je ne le suerais pas, car cet ajout devrait être rétrocompatible).


la source
5
+1 pour avoir évoqué a = b = cce que les autres réponses n'évoquent pas vraiment.
Leo
6
Une troisième résolution consiste à utiliser un symbole différent pour l'affectation. Pascal utilise :=pour l'affectation.
Brian
5
@Brian: En effet, tout comme C #. =est l'affectation, ==est la comparaison.
Marjan Venema
3
En C #, quelque chose comme if (a = true)va lancer un avertissement C4706 ( The test value in a conditional expression was the result of an assignment.). GCC avec C lancera également a warning: suggest parentheses around assignment used as truth value [-Wparentheses]. Ces avertissements peuvent être réduits au silence avec un jeu de parenthèses supplémentaire, mais ils sont là pour encourager à indiquer explicitement que l'affectation était intentionnelle.
Bob
2
@delnan Juste un commentaire quelque peu générique, mais il a été déclenché par "supprimer l'écueil que vous mentionnez en exigeant que les conditions soient évaluées en booléens" - a = true est évalué en booléen et n'est donc pas une erreur, mais il déclenche également un avertissement associé en C #.
Bob
11

Existe-t-il des utilisations pratiques de la valeur de retour de l'opérateur d'affectation qui n'ont pas pu être réécrites de manière triviale?

De manière générale, non. L'idée d'avoir la valeur d'une expression d'affectation comme étant la valeur qui a été attribuée signifie que nous avons une expression qui peut être utilisée à la fois pour son effet secondaire et sa valeur , et qui est considérée par beaucoup comme déroutante.

Les utilisations courantes consistent généralement à rendre les expressions compactes:

x = y = z;

a la sémantique en C # de "convertir z au type de y, affecter la valeur convertie à y, la valeur convertie est la valeur de l'expression, convertir cela au type de x, affecter à x".

Mais nous sommes déjà dans le domaine des effets secondaires impertatifs dans un contexte de déclaration, donc il y a vraiment très peu d'avantages convaincants

y = z;
x = y;

De même avec

M(x = 123);

être un raccourci pour

x = 123;
M(x);

Encore une fois, dans le code d'origine, nous utilisons une expression à la fois pour ses effets secondaires et sa valeur, et nous faisons une déclaration qui a deux effets secondaires au lieu d'un. Les deux sont malodorants; essayez d'avoir un effet secondaire par instruction, et utilisez des expressions pour leurs valeurs, pas pour leurs effets secondaires.

Je développe un langage que j'ai l'intention de remplacer à la fois Javascript et PHP.

Si vous voulez vraiment être audacieux et souligner que l'affectation est une déclaration et non une égalité, alors mon conseil est: faites-en clairement une déclaration d'affectation .

let x be 1;

Le rouge. Ou

x <-- 1;

ou encore mieux:

1 --> x;

Ou encore mieux

1 → x;

Il n'y a absolument aucun moyen de les confondre x == 1.

Eric Lippert
la source
1
Le monde est-il prêt pour les symboles Unicode non-ASCII dans les langages de programmation?
billpg
Autant que j'aimerais ce que vous proposez, l'un de mes objectifs est que la plupart des JavaScript "bien écrits" puissent être portés avec peu ou pas de modifications.
billpg
2
@billpg: Le monde est-il prêt ? Je ne sais pas - le monde était-il prêt pour APL en 1964, des décennies avant l'invention d'Unicode? Voici un programme en APL qui choisit une permutation aléatoire de six nombres sur les 40 premiers: x[⍋x←6?40] APL avait besoin de son propre clavier spécial, mais c'était une langue assez réussie.
Eric Lippert
@billpg: Macintosh Programmer's Workshop a utilisé des symboles non ASCII pour des choses comme les balises regex ou la redirection de stderr. D'un autre côté, MPW avait l'avantage que le Macintosh facilitait la saisie de caractères non ASCII. Je dois avouer une certaine perplexité quant à la raison pour laquelle le pilote de clavier américain ne fournit aucun moyen décent de taper des caractères non ASCII. Non seulement la saisie d'un numéro Alt nécessite la recherche de codes de caractères - dans de nombreuses applications, cela ne fonctionne même pas.
supercat
Hm, pourquoi préférerait-on attribuer "à droite" comme a+b*c --> x? Cela me semble étrange.
Ruslan
9

De nombreux langages choisissent la voie pour faire de l'affectation une instruction plutôt qu'une expression, y compris Python:

foo = 42 # works
if foo = 42: print "hi" # dies
bar(foo = 42) # keyword arg

et Golang:

var foo int
foo = 42 # works
if foo = 42 { fmt.Printn("hi") } # dies

D'autres langues n'ont pas d'affectation, mais plutôt des liaisons de portée, par exemple OCaml:

let foo = 42 in
  if foo = 42 then
    print_string "hi"

Cependant, letc'est une expression elle-même.

L'avantage de permettre l'affectation est que nous pouvons vérifier directement la valeur de retour d'une fonction à l'intérieur du conditionnel, par exemple dans cet extrait de code Perl:

if (my $result = some_computation()) {
  say "We succeeded, and the result is $result";
}
else {
  warn "Failed with $result";
}

Perl étend en outre la déclaration à cette conditionnelle uniquement, ce qui la rend très utile. Il avertira également si vous affectez à l'intérieur d'un conditionnel sans y déclarer une nouvelle variable - if ($foo = $bar)avertira, if (my $foo = $bar)ne le fera pas.

Faire l'affectation dans une autre déclaration est généralement suffisant, mais peut poser des problèmes de portée:

my $result = some_computation()
if ($result) {
  say "We succeeded, and the result is $result";
}
else {
  warn "Failed with $result";
}
# $result is still visible here - eek!

Golang s'appuie fortement sur les valeurs de retour pour la vérification des erreurs. Il permet donc à un conditionnel de prendre une instruction d'initialisation:

if result, err := some_computation(); err != nil {
  fmt.Printf("Failed with %d", result)
}
fmt.Printf("We succeeded, and the result is %d\n", result)

D'autres langues utilisent un système de types pour interdire les expressions non booléennes à l'intérieur d'un conditionnel:

int foo;
if (foo = bar()) // Java does not like this

Bien sûr, cela échoue lors de l'utilisation d'une fonction qui renvoie un booléen.

Nous avons maintenant vu différents mécanismes pour se défendre contre une affectation accidentelle:

  • Interdire l'affectation en tant qu'expression
  • Utiliser la vérification de type statique
  • L'affectation n'existe pas, nous n'avons que des letliaisons
  • Autoriser une instruction d'initialisation, interdire l'affectation sinon
  • Interdire l'affectation dans un conditionnel sans déclaration

Je les ai classés par ordre croissant de préférence - les affectations à l'intérieur des expressions peuvent être utiles (et il est simple de contourner les problèmes de Python en ayant une syntaxe de déclaration explicite et une syntaxe d'argument nommée différente). Mais il est permis de les interdire, car il existe de nombreuses autres options dans le même sens.

Le code sans bogue est plus important que le code laconique.

amon
la source
+1 pour "Interdire l'affectation en tant qu'expression". Les cas d'utilisation de l'affectation en tant qu'expression ne justifient pas le risque de bogues et de problèmes de lisibilité.
poke
7

Vous avez dit: "Je me suis dit (avec peu de preuves au-delà de mon expérience personnelle) que la grande majorité des cas, cela était vraiment destiné à être une opération de comparaison."

Pourquoi ne pas résoudre le problème?

Au lieu de = pour l'affectation et == pour le test d'égalité, pourquoi ne pas utiliser: = pour l'affectation et = (ou même ==) pour l'égalité?

Observer:

if (a=foo(bar)) {}  // obviously equality
if (a := foo(bar)) { do something with a } // obviously assignment

Si vous voulez qu'il soit plus difficile pour le programmeur de confondre affectation et égalité, rendez-le plus difficile.

En même temps, si vous vouliez VRAIMENT résoudre le problème, vous supprimeriez le crock C qui prétendait que les booléens n'étaient que des entiers avec des noms de sucre symboliques prédéfinis. Faites-en un type complètement différent. Ensuite, au lieu de dire

int a = some_value();
if (a) {}

vous forcez le programmeur à écrire:

int a = some_value();
if (a /= 0) {} // Note that /= means 'not equal'.  This is your Ada lesson for today.

Le fait est que l'affectation en tant qu'opérateur est une construction très utile. Nous n'avons pas éliminé les lames de rasoir parce que certaines personnes se coupaient. Au lieu de cela, le roi Gillette a inventé le rasoir de sécurité.

John R. Strohm
la source
2
(1) :=pour l'affectation et =pour l'égalité pourrait résoudre ce problème, mais au prix d'aliéner tous les programmeurs qui n'ont pas grandi en utilisant un petit ensemble de langages non traditionnels. (2) Les types autres que les bools étant autorisés dans des conditions ne sont pas toujours dus au mélange des bools et des entiers, il suffit de donner une interprétation vrai / faux aux autres types. Un langage plus récent qui n'a pas peur de s'écarter de C l'a fait pour de nombreux types autres que les entiers (par exemple, Python considère que les collections vides sont fausses).
1
Et en ce qui concerne les lames de rasoir: celles-ci servent un cas d'utilisation qui nécessite une netteté. D'un autre côté, je ne suis pas convaincu qu'une bonne programmation nécessite une affectation à des variables au milieu d'une évaluation d'expression. S'il y avait un moyen simple, peu technique, sûr et rentable de faire disparaître les poils sans arêtes vives, je suis sûr que les lames de rasoir auraient été déplacées ou du moins rendues beaucoup plus rares.
1
@delnan: Un homme sage a dit "Rendez-le aussi simple que possible, mais pas plus simple." Si votre objectif est d'éliminer la grande majorité des erreurs a = b vs a == b, restreindre le domaine des tests conditionnels aux booléens et éliminer les règles de conversion de type par défaut pour <autre> -> booléen vous permet à peu près tout le chemin Là. À ce stade, si (a = b) {} n'est syntaxiquement valide que si a et b sont tous deux booléens et a est une valeur légale l.
John R. Strohm
Faire une affectation une déclaration est au moins aussi simple que - sans doute encore plus simple que - les changements que vous proposez, et réalise au moins autant - sans doute encore plus (ne permet même pas if (a = b)pour lvalue a, booléen a, b). Dans une langue sans typage statique, il donne également de bien meilleurs messages d'erreur (au moment de l'analyse par rapport à l'exécution). En outre, la prévention des "erreurs a = b contre a == b" peut ne pas être le seul objectif pertinent. Par exemple, je voudrais également permettre au code if items:de vouloir dire if len(items) != 0, et que je devrais abandonner pour restreindre les conditions aux booléens.
1
@delnan Pascal est une langue non courante? Des millions de personnes ont appris la programmation en utilisant Pascal (et / ou Modula, qui dérive de Pascal). Et Delphi est encore couramment utilisé dans de nombreux pays (peut-être moins dans le vôtre).
jwenting
5

Pour répondre à la question, oui, il existe de nombreuses utilisations, bien qu'elles soient légèrement niches.

Par exemple en Java:

while ((Object ob = x.next()) != null) {
    // This will loop through calling next() until it returns null
    // The value of the returned object is available as ob within the loop
}

L'alternative sans utiliser l'affectation incorporée nécessite la obdéfinition hors de la portée de la boucle et deux emplacements de code distincts qui appellent x.next ().

Il a déjà été mentionné que vous pouvez affecter plusieurs variables en une seule étape.

x = y = z = 3;

Ce genre de chose est l'utilisation la plus courante, mais les programmeurs créatifs en trouveront toujours plus.

Tim B
la source
Est-ce que cette condition de boucle se désallouerait et créerait un nouvel obobjet avec chaque boucle?
user3932000
@ user3932000 Dans ce cas, probablement pas, généralement x.next () itère sur quelque chose. Il est certainement possible que ce soit le cas.
Tim B
1

Puisque vous pouvez inventer toutes les règles, pourquoi autoriser maintenant l'affectation à transformer une valeur, et simplement ne pas autoriser les affectations à l'intérieur des étapes conditionnelles? Cela vous donne le sucre syntaxique pour faciliter les initialisations, tout en évitant une erreur de codage courante.

En d'autres termes, rendez cela légal:

a=b=c=0;

Mais rendez cela illégal:

if (a=b) ...
Bryan Oakley
la source
2
Cela semble être une règle plutôt ad hoc. Rendre l'affectation une déclaration et l'étendre pour permettre a = b = csemble plus orthogonal et plus facile à mettre en œuvre aussi. Ces deux approches ne sont pas d'accord sur l'affectation dans les expressions ( a + (b = c)), mais vous n'avez pas pris parti pour celles-ci, donc je suppose qu'elles n'ont pas d'importance.
«facile à mettre en œuvre» ne devrait pas être une grande considération. Vous définissez une interface utilisateur - accordez la priorité aux besoins des utilisateurs. Vous devez simplement vous demander si ce comportement aide ou gêne l'utilisateur.
Bryan Oakley
si vous interdisez la conversion implicite en booléen, vous n'avez pas à vous soucier de l'affectation dans des conditions
ratchet freak
Plus facile à mettre en œuvre n'était qu'un de mes arguments. Qu'en est-il du reste? Du point de vue de l'interface utilisateur, je pourrais ajouter que la conception incohérente à mon humble avis et les exceptions ad-hoc gênent généralement l'utilisateur dans le blocage et l'internalisation des règles.
@ratchetfreak, vous pourriez toujours avoir un problème avec l'attribution de bools réels
jk.
0

Par les sons de celui-ci, vous êtes sur le point de créer un langage assez strict.

Dans cet esprit, obliger les gens à écrire:

a=c;
b=c;

au lieu de:

a=b=c;

peut sembler une amélioration pour empêcher les gens de faire:

if (a=b) {

quand ils voulaient faire:

if (a==b) {

mais au final, ce type d'erreurs est facile à détecter et à avertir s'il s'agit ou non d'un code légal.

Cependant, il existe des situations où:

a=c;
b=c;

ne signifie pas que

if (a==b) {

sera vrai.

Si c est en fait une fonction c (), elle pourrait renvoyer des résultats différents à chaque appel. (cela pourrait aussi être coûteux en calcul ...)

De même, si c est un pointeur vers du matériel mappé en mémoire, alors

a=*c;
b=*c;

sont à la fois susceptibles d'être différents et peuvent également avoir des effets électroniques sur le matériel à chaque lecture.

Il existe de nombreuses autres permutations avec le matériel où vous devez être précis sur les adresses mémoire lues, écrites et soumises à des contraintes de temps spécifiques, où effectuer plusieurs affectations sur la même ligne est rapide, simple et évident, sans les risques de temps introduction de variables temporaires

Michael Shaw
la source
4
L'équivalent de a = b = cne l'est pas a = c; b = c, c'est b = c; a = b. Cela évite la duplication des effets secondaires et conserve également la modification de aet bdans le même ordre. De plus, tous ces arguments liés au matériel sont assez stupides: la plupart des langues ne sont pas des langues système et ne sont ni conçues pour résoudre ces problèmes ni utilisées dans des situations où ces problèmes se produisent. Cela vaut doublement pour un langage qui tente de déplacer JavaScript et / ou PHP.
delnan, le problème n'était pas ces exemples artificiels, ils le sont. Le fait est qu'ils montrent les types d'endroits où l'écriture de a = b = c est courante et, dans le cas du matériel, considérée comme une bonne pratique, comme l'OP l'a demandé. Je suis sûr qu'ils seront en mesure de considérer leur pertinence par rapport à leur environnement attendu
Michael Shaw
Avec le recul, mon problème avec cette réponse n'est pas principalement qu'elle se concentre sur les cas d'utilisation de la programmation système (bien que ce soit déjà assez mauvais, la façon dont elle est écrite), mais qu'elle repose sur l'hypothèse d'une réécriture incorrecte. Les exemples ne sont pas des exemples d'endroits où il a=b=cest courant / utile, ce sont des exemples d'endroits où l'ordre et le nombre d'effets secondaires doivent être pris en charge. C'est entièrement indépendant. Réécrivez l'affectation chaînée correctement et les deux variantes sont également correctes.
@delnan: La valeur r est convertie en type de bdans un temp, et qui est convertie en type de adans un autre temp. Le moment relatif du moment où ces valeurs sont réellement stockées n'est pas spécifié. Du point de vue de la conception du langage, je pense qu'il est raisonnable d'exiger que toutes les valeurs l dans une instruction à affectations multiples aient un type correspondant, et éventuellement d'exiger qu'aucune d'entre elles ne soit volatile.
supercat
0

Le plus grand avantage pour moi d'avoir l'affectation comme expression est qu'elle permet à votre grammaire d'être plus simple si l'un de vos objectifs est que "tout est une expression" - un objectif de LISP en particulier.

Python n'a pas cela; il a des expressions et des déclarations, l'affectation étant une déclaration. Mais parce que Python définit un lambdaformulaire comme étant un paramètre unique expression , cela signifie que vous ne pouvez pas affecter de variables à l'intérieur d'un lambda. C'est parfois gênant, mais ce n'est pas un problème critique, et c'est à peu près le seul inconvénient de mon expérience à ce que l'affectation soit une déclaration en Python.

Une façon de permettre à l'affectation, ou plutôt à l'effet de l'affectation, d'être une expression sans introduire le risque d' if(x=1)accidents que C a est d'utiliser une letconstruction de type LISP , telle que celle (let ((x 2) (y 3)) (+ x y))qui, dans votre langage, pourrait être considérée comme 5. L'utilisation de letcette manière n'a pas besoin d'être techniquement affectée du tout dans votre langue, si vous définissez letcomme créant une portée lexicale. Défini de cette façon, une letconstruction peut être compilée de la même manière que la construction et l'appel d'une fonction de fermeture imbriquée avec des arguments.

D'un autre côté, si vous êtes simplement concerné par le if(x=1)cas, mais que vous voulez que l'affectation soit une expression comme en C, peut-être que le simple choix de jetons différents suffira. Affectation: x := 1ou x <- 1. Comparaison: x == 1. Erreur de syntaxe: x = 1.

wberry
la source
1
letdiffère de l'affectation à bien des égards que l'introduction technique d'une nouvelle variable dans une nouvelle portée. Pour commencer, cela n'a aucun effet sur le code en dehors du letcorps de ', et nécessite donc d'imbriquer davantage tout le code (ce qui devrait utiliser la variable), un inconvénient important dans le code à affectation élevée. Si l'on devait emprunter cette voie, ce set!serait le meilleur analogue Lisp - complètement différent de la comparaison, mais ne nécessitant pas d'imbrication ou de nouvelle portée.
@delnan: J'aimerais voir une combinaison de syntaxe de déclaration et d'attribution qui interdirait la réaffectation mais permettrait la redéclaration, sous réserve des règles selon lesquelles (1) la redéclaration ne serait légale que pour les identificateurs de déclaration et d'affectation, et (2 ) la redéclaration «annulerait» une variable dans toutes les étendues englobantes. Ainsi, la valeur de tout identifiant valide serait celle qui a été attribuée dans la déclaration précédente de ce nom. Cela semblerait un peu plus agréable que d'avoir à ajouter des blocs de portée pour les variables qui ne sont utilisées que pour quelques lignes, ou d'avoir à formuler de nouveaux noms pour chaque variable temporaire.
supercat
0

Je sais que cela signifierait que ces fonctions en ligne que les gens aiment tant ne fonctionneraient plus. Je me suis dit (avec peu de preuves au-delà de mon expérience personnelle) que la grande majorité des cas, cela était vraiment destiné à être une opération de comparaison.

Effectivement. Ce n'est pas nouveau, tous les sous-ensembles sûrs du langage C ont déjà tiré cette conclusion.

MISRA-C, CERT-C et ainsi de suite toute interdiction d'affectation dans des conditions, simplement parce que c'est dangereux.

Il n'existe aucun cas où le code reposant sur l'affectation dans des conditions ne peut pas être réécrit.


En outre, ces normes mettent également en garde contre l'écriture de code qui repose sur l'ordre d'évaluation. Affectations multiples sur une seule lignex=y=z; sont un tel cas. Si une ligne avec plusieurs affectations contient des effets secondaires (appeler des fonctions, accéder à des variables volatiles, etc.), vous ne pouvez pas savoir quel effet secondaire se produira en premier.

Il n'y a pas de points de séquence entre l'évaluation des opérandes. Nous ne pouvons donc pas savoir si la sous-expression yest évaluée avant ou aprèsz : il s'agit d'un comportement non spécifié en C. Ainsi, ce code est potentiellement non fiable, non portable et non conforme aux sous-ensembles sûrs mentionnés de C.

La solution aurait été de remplacer le code par y=z; x=y;. Cela ajoute un point de séquence et garantit l'ordre d'évaluation.


Donc, sur la base de tous les problèmes que cela a causés en C, tout langage moderne ferait bien d'interdire l'affectation à l'intérieur des conditions, ainsi que plusieurs affectations sur une seule ligne.


la source