Pourquoi un objet Regexp est-il considéré comme «falsifié» dans Ruby?

16

Ruby a une idée universelle de « véracité » et de « fausseté ».

Ruby n'ont deux classes spécifiques pour les objets Boolean, et , avec les instances singleton désignés par les variables spéciales et , respectivement.TrueClassFalseClasstruefalse

Cependant, la véracité et la fausseté ne sont pas limitées aux instances de ces deux classes, le concept est universel et s'applique à chaque objet unique dans Ruby. Chaque objet est soit vrai soit faux . Les règles sont très simples. En particulier, seuls deux objets sont faux :

Chaque autre objet est véridique . Cela inclut même les objets qui sont considérés comme faux dans d'autres langages de programmation, tels que

Ces règles sont intégrées dans la langue et ne sont pas définissables par l'utilisateur. Il n'y a pas de to_boolconversion implicite ou quelque chose de similaire.

Voici une citation de la spécification ISO Ruby Language :

6.6 Valeurs booléennes

Un objet est classé en un objet vrai ou un objet faux .

Seuls faux et nil sont des objets faux. false est la seule instance de la classe FalseClass(voir 15.2.6) à laquelle une fausse expression est évaluée (voir 11.5.4.8.3). nil est la seule instance de la classe NilClass(voir 15.2.4) à laquelle une expression nil est évaluée (voir 11.5.4.8.2).

Les objets autres que faux et nul sont classés en objets vrais. true est la seule instance de la classe TrueClass(voir 15.2.5) à laquelle une expression vraie est évaluée (voir 11.5.4.8.3).

L'exécutable Ruby / Spec semble d'accord :

it "considers a non-nil and non-boolean object in expression result as true" do
  if mock('x')
    123
  else
    456
  end.should == 123
end

Selon ces deux sources, je suppose que les Regexps sont également véridiques , mais selon mes tests, ils ne le sont pas:

if // then 'Regexps are truthy' else 'Regexps are falsy' end
#=> 'Regexps are falsy'

J'ai testé cela sur YARV 2.7.0-preview1 , TruffleRuby 19.2.0.1 et JRuby 9.2.8.0 . Les trois implémentations sont en accord et en désaccord avec la spécification ISO Ruby Language et mon interprétation de Ruby / Spec.

Plus précisément, les Regexpobjets qui sont le résultat de l'évaluation des Regexp littéraux sont faux , alors que les Regexpobjets qui sont le résultat d'une autre expression sont véridiques :

r = //
if r then 'Regexps are truthy' else 'Regexps are falsy' end
#=> 'Regexps are truthy'

Est-ce un bug ou un comportement souhaité?

Jörg W Mittag
la source
Ce qui Regex.new("a")est intéressant, c'est que c'est vrai.
mrzasa
!!//est faux mais !!/r/vrai. Étrange en effet.
max
@max !!/r/produit falsepour moi en utilisant (RVM) Ruby 2.4.1.
3limin4t0r
Désolé mon mauvais @ 3limin4t0r. Tu as raison. J'ai dû faire quelque chose de vraiment stupide comme laisser de côté un point d'exclamation.
max
2
Une hypothèse, je pense que //dans if // thenest interprété comme un test (un raccourci pour if //=~nil then) (qui est toujours faux quel que soit le motif) et non comme une instance Regexp.
Casimir et Hippolyte

Réponses:

6

Ce n'est pas un bug. Ce qui se passe, c'est que Ruby réécrit le code pour que

if /foo/
  whatever
end

devient effectivement

if /foo/ =~ $_
  whatever
end

Si vous exécutez ce code dans un script normal (et n'utilisez pas l' -eoption), vous devriez voir un avertissement:

warning: regex literal in condition

C'est probablement un peu déroutant la plupart du temps, c'est pourquoi l'avertissement est donné, mais peut être utile pour une ligne utilisant l' -eoption. Par exemple, vous pouvez imprimer toutes les lignes correspondant à une expression rationnelle donnée à partir d'un fichier avec

$ ruby -ne 'print if /foo/' filename

(L'argument par défaut de l' printest $_également.)

mat
la source
Voir aussi -n, -p, -aet -loptions, ainsi que la poignée de méthodes du noyau qui ne sont disponibles que lorsque -nou -psont utilisés ( chomp, chop, gsubet sub).
mat
Il y a aussi une deuxième partie de l'analyseur où cet avertissement est émis. Mais je ne sais pas ce qui se passe là-bas.
mat
Je crois que la "deuxième partie" est celle qui s'applique réellement à cette question. NODE_LITavec type T_REGEXP. Celui que vous avez publié dans votre réponse concerne un littéral dynamiqueRegexp , c'est-à-dire un Regexplittéral qui utilise l'interpolation, par exemple /#{''}/.
Jörg W Mittag
@ JörgWMittag Je pense que vous avez raison. En fouillant dans le compilateur et le bytecode généré, il semble que dans le cas de l'expression rationnelle dynamique, l'arbre d'analyse est réécrit pour ajouter explicitement en $_tant que nœud que le compilateur gère normalement, tandis que dans le cas statique, tout est traité par le compilateur. Ce qui est dommage pour moi car "hé, vous pouvez voir où l'arbre d'analyse est réécrit ici" fait une belle réponse.
mat
4

C'est le résultat (pour autant que je sache) d'une fonctionnalité non documentée du langage ruby, qui s'explique mieux par cette spécification :

it "matches against $_ (last input) in a conditional if no explicit matchee provided" do
  -> {
    eval <<-EOR
    $_ = nil
    (true if /foo/).should_not == true
    $_ = "foo"
    (true if /foo/).should == true
    EOR
  }.should complain(/regex literal in condition/)
end

Vous pouvez généralement penser $_à la "dernière chaîne lue par gets"

Pour rendre les choses encore plus confuses, $_(avec $-) n'est pas une variable globale; il a une portée locale .


Lorsqu'un script Ruby démarre, $_ == nil.

Donc, le code:

// ? 'Regexps are truthy' : 'Regexps are falsey'

S'interprète comme:

(// =~ nil) ? 'Regexps are truthy' : 'Regexps are falsey'

... Ce qui renvoie falsey.

En revanche, pour une expression rationnelle non littérale (par exemple r = //ou Regexp.new('')), cette interprétation spéciale ne s'applique pas.

//est véridique; comme tous les autres objets en rubis en plus nilet false.


À moins d'exécuter un script ruby ​​directement sur la ligne de commande (c'est-à-dire avec le -edrapeau), l'analyseur ruby ​​affichera un avertissement contre une telle utilisation:

avertissement: expression régulière littérale en condition

Vous pouvez utiliser ce comportement dans un script, avec quelque chose comme:

puts "Do you want to play again?"
gets
# (user enters e.g. 'Yes' or 'No')
/y/i ? play_again : back_to_menu

... Mais il serait plus normal d'affecter une variable locale au résultat getset d'effectuer la vérification d'expression régulière par rapport à cette valeur explicitement.

Je ne connais aucun cas d'utilisation pour effectuer cette vérification avec une expression régulière vide , en particulier lorsqu'elle est définie comme une valeur littérale. Le résultat que vous avez mis en évidence surprendrait en effet la plupart des développeurs de rubis.

Tom Lord
la source
J'ai seulement utilisé le conditionnel comme exemple. !// #=> truea le même comportement et n'est pas conditionnel. Je n'ai trouvé aucun contexte booléen (conditionnel ou non), où il se comporte comme prévu.
Jörg W Mittag
@ JörgWMittag Voulez-vous dire par exemple les !// ? true : falseretours true? Je pense que c'est encore le même point - il est interprété comme:!(// =~ nil) ? true : false
Tom Lord
Si vous définissez manuellement $_ = 'hello world'avant d'exécuter le code ci-dessus, vous devriez obtenir un résultat différent - car // =~ 'hello world', mais ne correspond pas nil.
Tom Lord
Non, je veux dire !// sans l' évaluation conditionnelletrue . La spécification que vous avez citée concerne un Regexplittéral dans un conditionnel, mais dans cet exemple, il n'y a pas de conditionnel, donc cette spécification ne s'applique pas.
Jörg W Mittag
2
Ah .. Oui, très surprenant. Le comportement semble cependant lié: puts !//; $_ = ''; puts !//- Je suppose que l'analyseur le développe comme une macro; il n'a pas nécessairement besoin d'être dans un conditionnel?
Tom Lord