Perl

индекс
81,20

Особенности национальных выражений

Захотелось мне поделиться с хабравчанами-перловодами одним интересным случаем, произошедшим у нас на работе. В процессе ревизии старого кода была обнаружена некая загадочная конструкция (здесь и далее реальный код несколько сокращён и приглажен):
sort { my ($x, $y) = ($a, $b); ($x =~ s{/}g) <=> ($y =~ s{/}g); } @array;
Казалось бы, обычная сортировка с переопределённой функцией сравнения. По изначальной задумке автора здесь должна была быть сортировка массива строк по количеству прямых слэшей в этих строках (известно, что выражение замены возвращает в качестве значения количество произведённых замен), однако что-то в этой функции не то. Вроде бы, «заменительная» часть выражения s/pattern/replace/g никогда не была опциональной, однако именно это мы видим в выражениях $a =~ s{/}g. Как же оно работает? Вот с этим мы сейчас и будем разбираться.

Итак, первое, что приходит в голову, — просто попробовать выражение на простеньком примере. Хорошо, запускаем и получаем результат:
$ perl -we 'my $a = "a/b/c"; $a =~ s{/}g; print "$a\n";'
Substitution replacement not terminated at -e line 1.
То есть наши подозрения были верны, выражение невалидно. Как же тогда работает исходный пример? После безуспешных попыток разобраться своими силами мы призвали на помощь модуль Deparse.
$ cat test.pl
my ($a, $b);
sub f {
    return (($a =~ s{/}g) <=> ($b =~ s{/}g));
}
$ perl -MO=Deparse -w test.pl
BEGIN { $^W = 1; }
my($a, $b);
sub f {
return $a =~ s[/][) <=> ($b =~ s{/}];
}
/home/tester/test.pl syntax OK
После недолгого скрипения мозгами, наконец, удалось разобраться, что же делает Перл. В вышеприведённом логе я подсветил синим скобки, на которые нужно обратить внимание. Поведение оказалось вполне логичным и документированным, хотя и несколько неожиданным. Как должно быть известно многим из программистов на Перле, в операторе s/// в качестве скобок вместо слэшей могут использоваться и другие символы (что очень наглядно продемонстрировал Deparse, использовав квадратные скобки). Но до этого момента мне просто не приходило в голову, что точно так же в качестве скобок может быть использована самая обычная буква "g"! Таким образом, в качестве выражения для поиска в переменной $a берётся то, что и должно — один прямой слэш, а вот в качестве выражения для замены берётся текст, находящийся между буквами "g", которые предполагались быть модификаторами операции.

Для наглядности я сейчас снова напишу исходную строку кода, на этот раз подсветив красным цветом выражение поиска, зелёным — замены, а «скобки» обозначив тем же цветом, но более тёмного тона:
(($a =~ s{/}g) <=> ($b =~ s{/}g))
В результате мы имеем одно выражение замены, а не два, и наша функция сравнения на самом деле ничего не сравнивала, а просто всегда выдавала лишь количество слэшей в первом аргументе, и вместо сортировки мы получали просто перемешивание массива (а не заметили этого раньше лишь по той причине, что сортировка была чисто косметическим улучшением, и при использовании программы на порядок вывода сортируемых строк никто не обращал особого внимания).

Итак, какая же будет мораль? Да, наверное, никакая. Лишний раз подтвердилась и без того всем известная истина: не всё то верно, что компиляется.
+32
10 февраля 2010, 10:47
12

комментарии (53)

+4
garex #
Хмм… А зачем вообще так писать? Чтобы никто не понял типа?

Или чтобы ВРАГИ не дай Бог не поняли?

Дак враги то они всегда найдут компетентного товарища, а вот друзья будут вспоминать и автор поикивать.

Надеюсь это кусок кода, который в какой-нить утилитке, которая тока чисто для себя делалась и никогда бы не стала переписываться, ибо даже автор бы скорее всего через год сам бы уже не помнил, что он имел в виду.
0
youlose #
Вообще-то зная Perl, задумка была правильная как это всё сделать (да и выглядит всё достаточно понятно =) ), просто писал неопытный человек (и невнимательный) и не проверил результат.
+2
garex #
Ммм… Признаюсь, что любое выражение на любом языке в одну строку да со столькью-то (!) операторами выглядит мягенько говоря смутненько.

Я признаюсь при всём честном народе, что я перловку в детстве не любил, но и она крайне полезна.

Но ведь можно ЭТО написать чуть более ясно?

// Хотя здесь условия не ясны — вдруг надо было «вот прям щас и чтобы бла-бла-бла!», когда над душой стоит хрень
+1
CaptainFlint #
Там в коде был комментарий, так что с понятностью проблем не возникло. Тем более, когда имеешь некоторый опыт программирования на Перле, оператор <=> в функции сравнения распознаётся влёт, а справа и слева совершенно идентичные выражения. Затык был не в непонятности кода в целом, а конкретно в составляющих его выражениях.
0
garex #
Эт то да, но сёравно мы все люди и баги случаются опять же в моменты свойственные человеку.

Случай конечно интересный, но всё-таки лучше писать понятнее.

Хотя бы чтобы большинство редакторов подсвечивало правильно.

Хотя тов. touzoku метко подметил, что перл перлом читать надо :)
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
0
garex #
Как же так??? Ведь всё вроде правильно!

:))
+1
lany #
А незнающих людей не жалко?
НЛО прилетело и опубликовало эту надпись здесь
0
crtman #
Хм, у меня печаOH SHI~
0
Bambr #
Жесть. Бедняга и не догадывался, какую свинью замедленного действия он вам подложил. Надолго работу парализовало? :)
0
CaptainFlint #
Да не очень. :) Исправить-то было несложно, гораздо интереснее было разобраться, что же там вообще такое происходит и как получилось, что синтаксически некорректные выражения проходят этап компиляции без ошибок и предупреждений.

Разбирались, кстати, вместе с самим автором — он когда эту строчку увидел, у него так же, как и у меня, глаза на лоб вылезли. :)
+2
lany #
Спасибо :-) Мораль ещё такая: пользоваться редакторами с адекватной подсветкой синтаксиса.

Проверил в Far'овском колорере и в Absolute Perl'е. Оба подсвечивают, к сожалению, неадекватно, но оба выводят достаточно визуальных матюгов, чтобы заподозрить что-то неладное :-) Кто найдёт редактор или онлайн-хайлайтер, который адекватно раскрасит эту строку? :-)

Absolute Perl:


Far:
0
touzoku #
по-моему ни один редактор правильно не подсвечивает такого рода операторы в Perl. В одно время пытались коллективно найти хоть что-то — не нашли :(

P.S. Синтаксис перла можно парсить только на перле :)
+7
lany #
Формальное доказательство того, что Перл невозможно распарсить (задача парсинга Перла сводится к задаче остановки). Код в примере, действительно, невозможно корректно разукрасить:

whatever / 25 ; # / ; die "this dies!";

Если функция whatever принимает аргумент, он получит результат match-оператора / 25; # /, применённый к $_, после чего случится die. Если функция whatever не принимает аргумент, результат её выполнения будет разделен на 25, а die будет закомментирован.
+1
FloppyFormator #
Geany:
+1
touzoku #
Хотя Notepad++ победил таки:

+1
GreyCat #
MC:


Emacs:


VIM:

+1
lany #
Кажется, только Emacs справился? :-) От VIM'а я большего ожидал :-)
0
Pilat #
Eclipse+EPIC выдал то же что и Emacs.
0
alienator #
Komodo Edit тоже не справился.
–1
onix74 #
Честно говоря, всё равно не понял, почему нельзя было использовать более стандартное решение с "/", а искомые "/" экранировать обратными слэшами?
Ну, как бы то ни было, автор — не лентяй, коль разобрался в чужом коде. Я б просто переписал. ;)
+1
CaptainFlint #
Я чуть выше написал — разбирались вместе с автором того кода. Переписать несложно, интереснее было понять, в чём соль (и учесть на будущее).

А что касается слэшей — это врождённая нелюбовь к «заборчикам» (как у автора кода, так и у меня).
0
onix74 #
Понятно. Ну, в любом случае, молодцы. Может кому-нибудь на будущее уроком будет Ваша статья.
+3
lany #
И ещё одна мораль: пишите, наконец, эти долбаные тесты :-)
0
kachkaev #
а не заметили этого раньше лишь по той причине, что сортировка была чисто косметическим улучшением
Чувак, который написал сортировку даже не удосужился два-три раза внимательно посмотреть на результат с разными входными данными? Он, наверное, пока меньше 10 КБ кода нового не напишет, не смотрит, как всё работает, а потом просто запамятовал. :)
+1
youlose #
погольфим?

sort { (() = $a =~ /(\/)/g) <=> (() = $b =~ /(\/)/g } @array
+2
youlose #
даже так:

sort { $a =~ y|/|/| <=> $b =~ y|/|/| } @array
+3
FloppyFormator #
sort { split('/', $a) <=> split('/', $b) } @array

Вообще без регулярок.
0
xtender #
только вот длиннее :)
+1
xtender #
Можно просто с минусом:
sort { $a =~ y|/|/|-$b =~ y|/|/| } @array
0
drone #
Известный факт, описанный в perldoc perlre
+1
CaptainFlint #
Да, я же так и написал, что поведение документированное. Но многие ли могут похвастаться тем, что прочитали, усвоили и запомнили весь перлдок? :)

У меня есть в загашнике парочка заготовок, где описаны интересные возможности Перла, которые, несмотря на их документированность, редко попадаются на глаза и, как следствие, известны не всем. Планирую их тоже слегка подрихтовать и выложить на Хабр.
+2
drone #
Жду с нетерпением.
–2
Sauron #
Блин, ну почему у этих скриптовых языков всегда такой долбанутый синтаксис. g вместо скобок — кому такое только в голову прийдёт?..
0
youlose #
Честно говоря очень удобно и наглядно, конечно же если есть редактор с подсветкой синтаксиса.
0
lany #
Мне кажется, было бы разумное ограничение, что скобки у обоих параметров должны быть одинаковыми. Тогда проблемы бы не возникло.

У многих скриптовых языков весьма строгий и понятный синтаксис :-)
0
khizhaster #
Не нужно мерить всё вокруг со своей колокольни.
0
xiaose #
Одна g вместо двух скобок… вполне наглядно. Да и вообще, при непонимании логики незнакомого языка для начала не плохо просто смириться и просто запомнить что так надо. Потом, через время, скорее всего, придет понимание что это таки да — удобно и наглядно. Это относится и к человеческим языкам и к языкам программирования.
0
Sauron #
> Одна g вместо двух скобок… вполне наглядно.
Вообще-то две g.
Вы это серьёзно?

> при непонимании логики незнакомого языка
Я несколько знаком с Perl, автор поста явно более чем знаком, и тем не менее it came as a surprise для нас обоих (-;
0
Sauron #
P.S. И да, синтаксис это синтаксис — логика это логика, не подменяйте понятия.
0
akzhan #
Улыбнуло :)
0
odlbo #
Хроршая статься. Недавно наткнулся на нечто подобное, по началу тоже удивло:

foreach ( qw(file1 file2 file3) ) {
    next unless -e _; # без знака доллара
}
–1
youlose #
ну тут всё просто, use strict и всё сразу понятно
0
odlbo #
Не поможет. Даже с use warnings; ниодного прдупреждения не получите.
0
youlose #
точно… я без цикла проверил, ошибка была
0
lany #
А я, когда хотел написать такой же комментарий, сперва проверил ;-)
0
ksurent #
Ошибки быть не должно) Хотя, конечно, код неправильный.
_
в данном случае — файловый дескриптор, который неявно открывается при вызовах stat() и некоторых filetest-функций (-w, -f, etc.).
Если выше по коду для какого-то файла был заполнен
_
, то в данном цикле -e будет возвращать истину на всех итерациях даже если файла не существует.
Читать по теме: perldoc.perl.org/functions/-X.html
0
ksurent #
Упс, оформление похерилось. Надеюсь осталось ясным, что литерал _ — это файловый дескриптор, а не переменная $_ у которой забыли сигил)
0
odlbo #
Да, тупо, пропущено выражение:
stat($file);
или просто непоставлен багз.

Т.е. всё должно выглядить так:
foreach ( qw(file1 file2 file3) ) {
    stat($_);
    next unless -e _; # без знака доллара
    ...
}
0
sketcher #
Только возвращает оно 0 или 1, тк глобальной замены не происходит
0
pro #
$_ = '456';
s s5sZss # 8-)

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.