Pull to refresh

Необычный оператор диапазона

Reading time6 min
Views4.8K
Должен предупредить, что это ещё одна статья, не содержащая никаких откровений. Для тех супер-гиков, которые назубок знают весь perldoc, она будет абсолютно бесполезной, так что, уважаемые супер-гики, можете проходить мимо и не информировать, что всё это есть в доках. Я и так это знаю. :-) Моя статья для всех остальных, для тех, кто весь perldoc целиком либо не осилил, либо осилил, но не понял, либо понял, но не запомнил.

Я думаю, многие знают о так называемом операторе диапазона, записывающемся как .. (две точки), с помощью которого можно быстро создавать массивы из набора последовательных элементов. Например, следующий код создаёт массив из 35 чисел: 3, 4, 5, …, 37:
my @arr = 3 .. 37;
Помимо чисел можно использовать строки: в этом случае для генерации элементов массива будет выполняться так называемый магический инкремент (например, можно задать диапазон букв: 'a' .. 'z').

Однако оператор диапазона может использоваться и в скалярном контексте, принимая в качестве операндов булевские выражения и возвращая булевский результат. И вот здесь начинается самое интересное, потому что это оператор с состоянием: результат операции будет зависеть не только от значений левого и правого операндов, но ещё и от истории вызовов данного выражения!

Можно, конечно, здесь привести формальное определение, описывающее методику вычисления результата для оператора диапазона, но лично мне потребовалось это формальное определение перечитать раз пять или шесть, прежде чем я, наконец, смог понять суть. Поэтому лучше пойти другим путём. Представим, что мы обрабатываем построчно какой-нибудь файл, и нам требуется выполнить определённое действие для каких-то блоков в этом файле (например, пропустить многострочный комментарий). Что такое блок? Это практически произвольный набор строк, заключённый между двумя маркерами, отмечающих начало и конец блока. Для определённости возьмём комментарии в стиле C/C++ (а для простоты будем считать, что в одной строке не могут соседствовать комментарий и полезная команда). Вот пример кода, который мы будем обрабатывать:
01:  int i = 10, j, k;
02:  for (j = i; j < 2 * i; ++j) {
03:      /*
04:        Здесь мы будем выполнять
05:        какие-то очень сложные
06:        и непонятные действия. */

07:      k = j * j;
08:      printf("Result[%d]: %d\n", j, k);
09:      /* Результат выведен. */
10:  }
Будем писать код, который выведет на экран все незакомментаренные строки вышеприведённого текста. Что станет делать программист на C при построчной обработке? Ну, например, создаст переменную, где будет хранить текущее состояние: находимся ли мы внутри комментария (т. е. мы прочитали маркер "/*", но ещё не встретили "*/"), и в зависимости от значения этой переменной выводить или не выводить очередную строку на экран, а также не забывать своевременно менять значение при обнаружении маркера нужного типа. А что сделает программист на Перле? А он воспользуется оператором диапазона и напишет что-то типа следующего:
while (my $line = <FILE>) {
    if (($line =~ m/^\s*\/\*/) .. ($line =~ m/\*\/\s*$/)) {
        # $line - это комментарий, пропускаем.
    }
    else {
        # $line - это код, печатаем.
        print $line;
    }
}
Что делает этот код? Если вкратце, то именно то, что нам нужно. Левый операнд оператора диапазона здесь соответствует началу комментария, правый — окончанию. А сам оператор диапазона возвращает истину в том и только том случае, если в процессе выполнения кода мы находимся в «промежутке» от срабатывания левого операнда и до срабатывания правого. Таким образом, оператор полностью оправдывает своё название: он задаёт логический диапазон.

Вот теперь можно привести и формальное определение (вольный перевод выдержки из официальной документации perlop):
Каждый оператор .. содержит собственное булевское состояние. Оно содержит значение «ложь», пока левый операнд является ложным. Как только левый операнд становится истинным, оператор диапазона принимает истинное значение и остаётся таковым до тех пор, пока правый операнд не примет истинное значение. ПОСЛЕ этого оператор диапазона вновь принимает ложное значение.
Попробуем теперь разложить по полочкам всю ту магию, которая превращает это несколько смутное определение в соответствие обычному диапазону. Для этого представим себя отладчиком и будем выполнять программу последовательно, шаг за шагом, построчно считывая входной файл. Для краткости я здесь обозначаю левый и правый операнды (булевские выражения соответствия началу и концу комментария) как MB и ME (сокращение от marker begin / marker end).
  1. int i = 10, j, k;
    На данной строчке как MB, так и ME дают ложь, следовательно, оператор .. также вернёт ложь. Таким образом, данная строчка — не комментарий.
  2. for (j = i; j < 2 * i; ++j) {
    Аналогично, MB и ME дают ложь, так что всё выражение также будет ложным.
  3.     /*
    А вот здесь, наконец, срабатывает MB; ME остаётся ложью. Согласно определению, в этот момент оператор диапазона принимает истинное значение, и мы получаем результат, что считанная строка является комментарием.
  4.       Здесь мы будем выполнять
    На этой строке выражения MB и ME снова ничего не находят и возвращают ложь. Но поскольку оператор уже переключился в истинное состояние, он теперь будет в нём пребывать до тех пор, пока ME не примет истинного значения. Таким образом мы снова получаем истину, т. е. эта строчка является комментарием.
  5.       какие-то очень сложные
    Здесь ME всё ещё не стал истинным, так что оператор .. продолжает выдавать истину, т. е. конца комментария мы ещё не достигли.
  6.       и непонятные действия. */
    А вот здесь, наконец, срабатывает ME. Оператор диапазона вздыхает и в последний раз выдаёт истину, после чего переключается обратно в исходное состояние. Но для этой строчки мы пока получаем всё же истину, что отлично кореллируется с нашими представлениями о структуре многострочных комментариев: данная строка является завершающей, но всё же частью комментария, и выводиться согласно вышеприведённому ТЗ не должна.
  7.     k = j * j;
    Халява кончилась, сэр. MB и ME оба ложны, оператор в своём исходном состоянии и возвращает ложь, так что данная строка комментарием не является.
  8.     printf("Result[%d]: %d\n", j, k);
    …точно так же, как и эта. По тем же причинам.
  9.     /* Результат выведен. */
    А вот этот кусочек весьма любопытен: здесь срабатывают одновременно и MB, и ME. Что это меняет? Да, в общем-то, ничего. Оператор .., согласно определению, возвратит истину, запомнив это на будущее, но, поскольку второй операнд тоже истинен, тут же происходит обратное переключение в исходное состояние: комментарий начался и тут же закончился.
  10. }
    Данная строчка не ловится ни MB, ни ME, и, т. к. оператор успел переключиться обратно, он здесь вернёт ложь, отметив данную строку как незакомментированную.
Надеюсь, что этот простенький пример помог вам разобраться, что к чему. Фактически, внутри оператора содержится та самая локальная переменная состояния, которую вымышленный нами C-программист вынужден был вводить явным образом, а также возиться с управлением её значениями.

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

Стоит также здесь сказать, что кроме двухточечного оператора .. есть ещё и трёхточечный (...). Он ведёт себя точно так же, как двухточечный, но с одним отличием: когда первый операнд в исходном состоянии оператора принимает истинное значение, второй операнд игнорируется. Таким образом, если бы мы для нашего примера попытались использовать оператор ..., то комментарий в девятой строке файла считался бы не законченным, а продолжающимся до конца файла (точнее, до строчки, где попадётся следующий маркер конца комментария, но в нашем файле-примере такой строчки просто нет). В качестве примера использования можно привести ситуацию, когда начало и конец обрабатываемого блока задаются одной и той же спец-строкой. Двухточечный оператор здесь был бы бессилен, а трёхточечный подошёл бы идеально.

Напоследок хочется прочитать небольшую нотацию: пожалуйста, не забывайте о читабельности и самокомментируемости кода. Не нужно использовать возможность языка только из-за того, что она в нём есть. Если у вас, действительно, в последовательной обработке каких-то данных явственно выделяется некий блок, то данный оператор даёт возможность коротко, понятно и изящно этот блок описать. Но если вы начнёте пихать оператор диапазона где ни попадя, только из-за того, что он такой весь из себя оригинальный, необычный, и его больше ни у кого нет, — поверьте, ни к чему хорошему это не приведёт.
Tags:
Hubs:
Total votes 45: ↑36 and ↓9+27
Comments20

Articles