Почему объект Regexp в Ruby считается "ложным"?

У Руби есть универсальное представление о "правдивости" и "ложности".

В Ruby есть два специальных класса для булевых объектов: TrueClass и FalseClass, причем одноэлементные экземпляры обозначаются специальными переменными true и false соответственно.

Однако правдивость и ложность не ограничиваются экземплярами этих двух классов, концепция универсальна и применима к каждому объекту в Ruby. Каждый объект является либо правдивым, либо ложным. Правила очень просты. В частности, только два объекта являются ложными:

  • nil, одиночный экземпляр NilClass и
  • false, одиночный экземпляр FalseClass

Каждый другой объект правдив. Это включает даже объекты, которые считаются ложными в других языках программирования, таких как

Эти правила встроены в язык и не определяются пользователем. to_bool не существует неявного преобразования или чего-либо подобного.

Вот цитата из спецификации языка Ruby ISO:

6.6 Булевы значения

Объект классифицируется как истинный объект или ложный объект.

Только false и nil являются ложными объектами. false - единственный экземпляр класса FalseClass (см. 15.2.6), для которого оценивается ложное выражение (см. 11.5.4.8.3). nil - единственный экземпляр класса NilClass (см. 15.2.4), для которого оценивается nil-выражение (см. 11.5.4.8.2).

Объекты, отличные от false и nil, классифицируются как истинные объекты. true - единственный экземпляр класса TrueClass (см. 15.2.5), для которого оценивается true-выражение (см. 11.5.4.8.3).

Исполняемый Ruby/Spec, похоже, согласен с:

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

Согласно этим двум источникам, я бы предположил, что Regexp также правдивы, но, согласно моим тестам, это не так:

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

Я проверял это на YARV 2.7.0-preview1, TruffleRuby 19.2.0.1 и JRuby 9.2.8.0. Все три реализации согласуются друг с другом и не соответствуют спецификации языка Ruby ISO и моей интерпретации Ruby/Spec.

Точнее говоря, объекты Regexp, которые являются результатом оценки литералов Regexp, являются ложными, тогда как объекты Regexp, которые являются результатом некоторых других выражений, являются правдивыми:

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

Это ошибка или желаемое поведение?

Ответы

Ответ 1

Это не ошибка. Происходит следующее: Ruby переписывает код так, чтобы

if /foo/
  whatever
end

фактически становится

if /foo/ =~ $_
  whatever
end

Если вы запускаете этот код в обычном сценарии (и не используете опцию -e), вы должны увидеть предупреждение:

warning: regex literal in condition

В большинстве случаев это, вероятно, несколько сбивает с толку, поэтому и дается предупреждение, но может быть полезно для одной строки, используя опцию -e. Например, вы можете напечатать все строки, соответствующие заданному регулярному выражению, из файла с

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

(Значением по умолчанию для print также является $_.)

Ответ 2

Это результат (насколько я могу судить) недокументированной особенности языка рубина, которая лучше всего объясняется этой спецификацией:

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

Обычно вы можете думать о $_ как о "последней строке, прочитанной gets"

Чтобы сделать ситуацию еще более запутанной, $_ (вместе с $-) не является not глобальной переменной; он имеет локальный охват.


Когда запускается скрипт ruby, $_ == nil.

Итак, код:

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

интерпретируется как:

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

... что возвращает фальси.

С другой стороны, для не буквального регулярного выражения (например, r = // или Regexp.new('')) эта специальная интерпретация не применяется.

// правдива; как и все другие объекты в ruby, кроме nil и false.


Если сценарий ruby не запущен непосредственно в командной строке (т.е. с флагом -e), анализатор ruby отобразит предупреждение о таком использовании:

предупреждение: регулярное выражение в состоянии

Вы можете использовать это поведение в скрипте, например:

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

... Но было бы более нормально назначить локальную переменную результату gets и выполнить проверку регулярного выражения по этому значению явно.

Я не знаю ни одного варианта использования для выполнения этой проверки с пустым регулярным выражением, особенно когда оно определено как буквальное значение. Выделенный вами результат действительно застал бы врасплох большинство разработчиков ruby.