Si je veux comparer deux nombres (ou d'autres entités bien ordonnées), je le ferais avec x < y
. Si je veux comparer trois d'entre eux, l'élève d'algèbre du secondaire proposera d'essayer x < y < z
. Le programmeur en moi répondra alors par "non, ce n'est pas valide, vous devez faire x < y && y < z
".
La plupart des langues que j'ai rencontrées ne semblent pas prendre en charge cette syntaxe, ce qui est étrange compte tenu de sa fréquence en mathématiques. Python est une exception notable. JavaScript ressemble à une exception, mais ce n'est vraiment qu'un sous-produit regrettable de l'opérateur priorité et conversions implicites; dans node.js, 1 < 3 < 2
correspond à true
, car c'est vraiment (1 < 3) < 2 === true < 2 === 1 < 2
.
Donc, ma question est la suivante: Pourquoi x < y < z
pas couramment disponible dans les langages de programmation, avec la sémantique attendue?
Ce sont des opérateurs binaires qui, lorsqu'ils sont chaînés, produisent normalement et naturellement un arbre de syntaxe abstrait comme:
Une fois évalué (ce que vous faites à partir des feuilles), cela produit un résultat booléen à partir de x < y
, vous obtenez une erreur de type en essayant de faire boolean < z
. Pour x < y < z
pour fonctionner comme vous l'avez expliqué, vous devez créer un cas spécial dans le compilateur pour produire une arborescence de syntaxe comme:
Non pas que ce ne soit pas possible. C'est évidemment le cas, mais cela ajoute une certaine complexité à l'analyseur pour un cas qui ne revient pas vraiment si souvent. Vous créez fondamentalement un symbole qui agit parfois comme un opérateur binaire et parfois efficace comme un opérateur ternaire, avec toutes les implications de la gestion des erreurs et ce que cela implique. Cela ajoute beaucoup d'espace pour que les choses tournent mal que les concepteurs de langage préfèrent éviter si possible.
x < y < z
pas couramment disponible dans les langages de programmation?Dans cette réponse, je conclus que
Je peux parler du point de vue d'un pythoniste sur cette question. Je suis un utilisateur d'une langue avec cette fonctionnalité et j'aime étudier les détails d'implémentation de la langue. Au-delà de cela, je suis un peu familier avec le processus de changement de langages comme C et C++ (la norme ISO est régie par un comité et versionnée par année.) Et j'ai regardé les deux Ruby et Python implémente les changements de rupture.
De la documentation/grammaire, nous voyons que nous pouvons enchaîner n'importe quel nombre d'expressions avec des opérateurs de comparaison:
comparison ::= or_expr ( comp_operator or_expr )* comp_operator ::= "<" | ">" | "==" | ">=" | "<=" | "!=" | "is" ["not"] | ["not"] "in"
et la documentation indique en outre:
Les comparaisons peuvent être chaînées arbitrairement, par exemple, x <y <= z est équivalent à x <y et y <= z, sauf que y n'est évalué qu'une seule fois (mais dans les deux cas, z n'est pas évalué du tout lorsque x <y est trouvé être faux).
Donc
result = (x < y <= z)
est logiquement équivalent en termes d'évaluation de x
, y
et z
, avec le exception que y
est évalué deux fois:
x_lessthan_y = (x < y)
if x_lessthan_y: # z is evaluated contingent on x < y being True
y_lessthan_z = (y <= z)
result = y_lessthan_z
else:
result = x_lessthan_y
Encore une fois, la différence est que y n'est évalué qu'une seule fois avec (x < y <= z)
.
(Remarque, les parenthèses sont complètement inutiles et redondantes, mais je les ai utilisées au profit de celles provenant d'autres langues, et le code ci-dessus est en Python tout à fait légal.)
Nous pouvons examiner comment Python analyse les opérateurs de comparaison chaînés:
>>> import ast
>>> node_obj = ast.parse('"foo" < "bar" <= "baz"')
>>> ast.dump(node_obj)
"Module(body=[Expr(value=Compare(left=Str(s='foo'), ops=[Lt(), LtE()],
comparators=[Str(s='bar'), Str(s='baz')]))])"
Nous pouvons donc voir que ce n'est vraiment pas difficile pour Python ou tout autre langage à analyser.
>>> ast.dump(node_obj, annotate_fields=False)
"Module([Expr(Compare(Str('foo'), [Lt(), LtE()], [Str('bar'), Str('baz')]))])"
>>> ast.dump(ast.parse("'foo' < 'bar' <= 'baz' >= 'quux'"), annotate_fields=False)
"Module([Expr(Compare(Str('foo'), [Lt(), LtE(), GtE()], [Str('bar'), Str('baz'), Str('quux')]))])"
Et contrairement à la réponse actuellement acceptée, l'opération ternaire est une opération de comparaison générique, qui prend la première expression, un itérable de comparaisons spécifiques et un itérable de nœuds d'expression à évaluer si nécessaire. Facile.
Personnellement, je trouve que la sémantique de la plage est assez élégante, et la plupart des Python professionnels que je connais encourageraient l'utilisation de la fonctionnalité, au lieu de la considérer comme dommageable - la sémantique est assez clairement indiquée dans le puits- documentation réputée (comme indiqué ci-dessus).
Notez que le code est lu beaucoup plus qu'il n'est écrit. Les changements qui améliorent la lisibilité du code doivent être adoptés, et non écartés en augmentant les spectres génériques de Peur, Incertitude et Doute .
Je pense qu'il y a une confluence de raisons qui s'articulent autour de l'importance relative de la caractéristique et de l'impulsion/inertie relative du changement permise par les gouverneurs des langues.
Pourquoi l'héritage multiple n'est-il pas disponible en Java ou C #? Il n'y a pas de bonne réponse ici à l'une ou l'autre question. Peut-être que les développeurs étaient trop paresseux, comme le prétend Bob Martin, et les raisons invoquées ne sont que des excuses. Et l'héritage multiple est un sujet assez important en informatique. Il est certainement plus important que le chaînage des opérateurs.
Le chaînage d'opérateurs de comparaison est élégant, mais en aucun cas aussi important que l'héritage multiple. Et tout comme Java et C # ont des interfaces comme solution de contournement, il en va de même pour chaque langue pour les comparaisons multiples - vous enchaînez simplement les comparaisons avec des booléens "et" s, ce qui fonctionne assez facilement.
La plupart des langues évoluent par comité (plutôt que d'avoir un dictateur bienveillant raisonnable pour la vie comme Python a). Et je spécule que ce problème n'a tout simplement pas vu suffisamment de soutien pour le faire sortir de son respectif comités.
Si une langue permet x < y < z
sans la sémantique mathématique attendue, ce serait un changement de rupture. S'il ne le permettait pas en premier lieu, il serait presque trivial d'ajouter.
Concernant les langues avec des changements de rupture: nous mettons à jour les langues avec des changements de comportement de rupture - mais les utilisateurs ont tendance à ne pas aimer cela, en particulier les utilisateurs de fonctionnalités qui peuvent être cassées. Si un utilisateur s'appuie sur l'ancien comportement de x < y < z
, ils protesteraient probablement bruyamment. Et comme la plupart des langues sont régies par un comité, je doute que nous obtiendrions beaucoup de volonté politique pour soutenir un tel changement.
Les langages informatiques essaient de définir les plus petites unités possibles et vous permettent de les combiner. La plus petite unité possible serait quelque chose comme "x <y" qui donne un résultat booléen.
Vous pouvez demander un opérateur ternaire. Un exemple serait x <y <z. Maintenant, quelles combinaisons d'opérateurs permettons-nous? Évidemment, x> y> z ou x> = y> = z ou x> y> = z ou peut-être x == y == z devrait être autorisé. Et x <y> z? x! = y! = z? Que signifie le dernier, x! = Y et y! = Z ou que les trois sont différents?
Maintenant, promotions d'arguments: en C ou C++, les arguments seraient promus en un type commun. Alors, qu'est-ce que x <y <z signifie que x est double mais y et z sont longs et longs int? Tous les trois promus pour doubler? Ou y est pris comme double une fois et aussi longtemps dans l'autre fois? Que se passe-t-il si en C++ un ou les deux opérateurs sont surchargés?
Et enfin, autorisez-vous un certain nombre d'opérandes? Comme un <b> c <d> e <f> g?
Eh bien, tout devient très compliqué. Maintenant, ce qui ne me dérangerait pas, c'est que x <y <z produise une erreur de syntaxe. Parce que son utilité est faible par rapport aux dommages causés aux débutants qui ne peuvent pas comprendre ce que x <y <z fait réellement.
Dans de nombreux langages de programmation, x < y
est une expression binaire qui accepte deux opérandes et donne un résultat booléen unique. Par conséquent, si vous enchaînez plusieurs expressions, true < z
et false < z
n'a pas de sens, et si ces expressions sont évaluées avec succès, elles risquent de produire le mauvais résultat.
Il est beaucoup plus facile de penser à x < y
comme un appel de fonction qui prend deux paramètres et produit un seul résultat booléen. En fait, c'est le nombre de langues qui l'implémentent sous le capot. C'est composable, facilement compilable, et ça marche.
Le x < y < z
le scénario est beaucoup plus compliqué. Désormais, le compilateur doit façonner trois fonctions : x < y
, y < z
, et le résultat de ces deux valeurs combinées, le tout dans le contexte d'une grammaire du langage sans doute = ambiguë .
Pourquoi l'ont-ils fait dans l'autre sens? Parce que c'est une grammaire sans ambiguïté, beaucoup plus facile à mettre en œuvre et beaucoup plus facile à corriger.
La plupart des langages traditionnels sont (au moins partiellement) orientés objet. Fondamentalement, le principe sous-jacent de OO est que les objets envoient des messages à d'autres objets (ou à eux-mêmes) et au récepteur de ce message a un contrôle complet sur la façon de répondre à ce message.
Voyons maintenant comment nous implémenterions quelque chose comme
a < b < c
On pourrait l'évaluer strictement de gauche à droite (associative de gauche):
a.__lt__(b).__lt__(c)
Mais maintenant, nous appelons __lt__
Sur le résultat de a.__lt__(b)
, qui est un Boolean
. Ça n'a aucun sens.
Essayons associatif à droite:
a.__lt__(b.__lt__(c))
Non, ça n'a pas de sens non plus. Maintenant, nous avons a < (something that's a Boolean)
.
D'accord, qu'en est-il de le traiter comme du sucre syntaxique. Faisons une chaîne de n <
Comparaisons envoyer un message n-1-aire. Cela pourrait signifier que nous envoyons le message __lt__
À a
, en passant b
et c
comme arguments:
a.__lt__(b, c)
D'accord, cela fonctionne, mais il y a une étrange asymétrie ici: a
arrive à décider si elle est inférieure à b
. Mais b
ne décide pas si elle est inférieure à c
, mais cette décision est également prise par a
.
Qu'en est-il de l'interpréter comme un message n-aire envoyé à this
?
this.__lt__(a, b, c)
Finalement! Cela peut fonctionner. Cela signifie cependant que l'ordre des objets n'est plus une propriété de l'objet (par exemple, si a
est inférieur à b
n'est ni une propriété de a
ni de b
) mais plutôt une propriété du contexte (ie this
).
D'un point de vue général, cela semble étrange. Cependant, par exemple à Haskell, c'est normal. Il peut y avoir plusieurs implémentations différentes de la classe de types Ord
, par exemple, et si a
est ou non inférieure à b
, cela dépend de l'instance de classe qui se trouve dans la portée.
Mais en réalité, ce n'est pas que bizarre du tout! Java ( Comparator
) et .NET ( IComparer
) ont des interfaces qui vous permettent de injectez votre propre relation d'ordre par exemple dans les algorithmes de tri, ils reconnaissent donc pleinement qu'un ordre n'est pas quelque chose qui est fixé à un type mais dépend plutôt du contexte.
Pour autant que je sache, il n'y a actuellement aucune langue qui effectue une telle traduction. Il y a cependant une priorité: Ioke et Seph ont ce que leur concepteur appelle des "opérateurs trinaires" - des opérateurs qui sont syntaxiquement binaire, mais sémantiquement ternaire. En particulier,
a = b
n'est pas interprété comme envoyant le message =
à a
en passant b
comme argument, mais plutôt comme l'envoi du message =
au "Ground actuel" (un concept similaire mais non identique à this
) en passant a
et b
comme arguments. Ainsi, a = b
Est interprété comme
=(a, b)
et pas
a =(b)
Cela pourrait facilement être généralisé aux opérateurs n-aires.
Notez que ceci est vraiment particulier aux langues OO. Dans OO, nous avons toujours un seul objet qui est finalement responsable de l'interprétation d'un message envoyé, et comme nous l'avons vu, il n'est pas immédiatement évident pour quelque chose comme a < b < c
quel objet cela devrait être.
Cela ne s'applique cependant pas aux langages procéduraux ou fonctionnels. Par exemple, dans Scheme , Common LISP , et Clojure , la fonction <
Est n-aire et peut être appelée avec un nombre arbitraire d'arguments.
En particulier, <
Ne signifie pas "moins que", mais plutôt ces fonctions sont interprétées légèrement différemment:
(< a b c d) ; the sequence a, b, c, d is monotonically increasing
(> a b c d) ; the sequence a, b, c, d is monotonically decreasing
(<= a b c d) ; the sequence a, b, c, d is monotonically non-decreasing
(>= a b c d) ; the sequence a, b, c, d is monotonically non-increasing
C'est simplement parce que les concepteurs de langage n'y ont pas pensé ou ne pensaient pas que c'était une bonne idée. Python le fait comme vous l'avez décrit avec une grammaire simple (presque) LL (1).
Le programme C++ suivant compile avec un aperçu de clang, même avec des avertissements définis au niveau le plus élevé possible (-Weverything
):
#include <iostream>
int main () { std::cout << (1 < 3 < 2) << '\n'; }
D'un autre côté, la suite de compilateurs GNU m'avertit gentiment que comparisons like 'X<=Y<=Z' do not have their mathematical meaning [-Wparentheses]
.
Donc, ma question est la suivante: pourquoi x <y <z n'est-il pas couramment disponible dans les langages de programmation, avec la sémantique attendue?
La réponse est simple: rétrocompatibilité. Il existe une grande quantité de code dans la nature qui utilise l'équivalent de 1<3<2
et attendez-vous à ce que le résultat soit vrai.
Un concepteur de langage n'a qu'une chance d'obtenir ce "bon", et c'est à ce moment-là que le langage est conçu pour la première fois. Le "mal" signifie initialement que d'autres programmeurs profiteront assez rapidement de ce "mauvais" comportement. Le faire "bien" la deuxième fois brisera cette base de code existante.