Каждая из наших программ первым делом получает числовой параметр из командной строки:
Perlmy $n=shift // die "Нужно неотрицательное число!\n";
Здесь требуется пояснение. Выражение
возвращает значение
A
//
B
A
, если оно определено (напоминаем, что значения
выражений в Perl могут быть неопределёнными; в частности, неопределённое
значение имеет переменная после своего объявления, и до
инициализации — первоначальное
присвоения значений этой переменной). Если же значение
A
неопределённое, оператор //
возвращает значение B
.
В нашем случае A
— выражение my $n=shift
. Его значение — это то, что присваивается
переменной $n
, а присваивается то, что выдаёт команда
shift
. А выдаёт эта команда первый элемент массива
@ARGV
. Если массив пуст (отсутствуют параметры в командной
строке), левая часть оператора // окажется неопределённой,
и потребуется вычислить правую часть B
(у нас это
die …
), чтобы возвратить полученное значение.
Вычисление правой части приведёт к завершению программы и выдаче сообщения
об ошибке.
Вывод очередной строки сплошного квадрата будем выполнять в цикле. Всё, что
требуется от такого цикла, это то, что он должен прокрутиться
$n
раз. В цикле можно устроить изменение счётчика
$j
(это номер строки, если нумерация начинается с нуля):
Perlfor(my $j=0; $j<$n; $j++) { вывести$n
звёздочек и перейти на новую строку }
Чтобы вывести $n
звёздочек, тоже организуем цикл (теперь
меняется счётчик $i
— номер столбца):
Perlfor(my $i=0; $i<$n; $i++) { print '*'; }
Осталось поместить второй цикл внутрь первого, и не забыть про вывод символа конца строки:
Perlfor(my $j=0; $j<$n; $j++) { for(my $i=0; $i<$n; $i++) { print '*'; } print "\n"; }
Можно было бы вместо обычных циклов for использовать переборные:
Perlfor my $j(0..$n-1) { for my $i(0..$n-1) { print '*'; } print "\n"; }
или
Perlfor my $j(1..$n) { for my $i(1..$n) { print '*'; } print "\n"; }
Так короче.
Наконец, поскольку счётчики $j
и $i
никак
не упоминаются в телах обоих циклов, уберём их из заголовков циклов. Тогда
вместо этих переменных в циклах будет меняться переменная по умолчанию
$_
, которая, впрочем, всё равно никак не используется:
Perlfor(1..$n) { for(1..$n) { print '*'; } print "\n"; }
Есть и другое решение. Вместо того, чтобы многократно вызывать команду
print
для печати звёздочек по одной штуке за раз,
сформируем строку, состоящую из $n
звёздочек, и выведем её
одной единственной командой print
. Удобный оператор
репликации x к нашим услугам: выражение 'abc' x 4
, к примеру, даёт строку
abcabcabcabc
. Чтобы построить строку
из $n
звёздочек, напишем выражение '*'
x $n
:
Perlfor(1..$n) { print '*' x $n; print "\n"; }
Можно пойти ещё дальше, если вспомнить, что команда print
способна выводить на экран несколько значений за раз — нужно лишь перечислить
выводимые значения через запятую. Это позволит нам обойтись одним вызовом
print
для печати и звёздочек, и знака конца строки:
Perlfor(1..$n) { print '*' x $n, "\n"; }
Наконец, заметим, что тем же способом можно устранить и внешний цикл: все
символы, которыми рисуется квадрат (включая и звёздочки и знаки конца строки),
можно объединить в одну строку всё тем же оператором репликации: (('*' x $n)."\n") x $n
. При $n
,
равном трём, такое выражение даёт строку
"***\n***\n***\n"
. Что ж, попробуем:
Perlprint (('*' x $n)."\n") x $n;
Но что это? При запуске программы на экран выводится следующее:
print (...) interpreted as function at ./asterisks-solid-square-bad.pl line 4.
Useless use of repeat (x) in void context at ./asterisks-solid-square-bad.pl line 4.
***
Будем разбираться. Параметры команды print
, как и других
встроенных команд, можно не заключать в скобки. Эта вольность во многих случаях
создаёт удобства, но в некоторых вызывает недоразумения. Именно это
и случилось. Грамматический анализатор Perl, увидев после
print
открывающую скобку, понимает, что между этой скобкой
и парной к ней закрывающей находится список параметров команды
print
. Поэтому выражение print (('*'
x $n)."\n") x $n
воспринимается как (print(('*' x
$n)."\n")) x $n
. Чтобы вычислить его значение, нужно вычислить значение
print(('*' x $n)."\n")
, и повторить его как строку
$n
раз. Команда print
возвращает
значение, хотя оно обычно никак не используется: это количество успешно
выведенных символов. К примеру, при $n
, равном трём, это
значение равно четырём (выведенная строка состоит из четырёх символов — три
звёздочки и конец строки). Все выражение (print(('*' x
$n)."\n")) x $n;
даёт, таким образом, строку '4' x
$n
, то есть '444'
. Это значение вычисляется, но
никак не используется, что заставляет Perl подозревать ошибку: сообщение
Useless use of repeat (x) in void context… означает
Бесполезная репликация (x) в пустом контексте…. Странно
вычислить выражение '4' x $n
, чтобы не
использовать его (использовать в пустом контексте), и Perl намекает нам на это.
Если, конечно, мы включили режим вывода предупреждений. Побочным итогом
вычисления выражения будет вывод сиротливых $n
звёздочек, но
не всего квадрата.
И как же быть? Конечно, использовать дополнительную пару скобок:
Perlprint((('*' x $n)."\n") x $n);
Невнимательный читатель может и не заметить небольшое отличие этого кода
с окончательной
версией программы: вместо точки, оператора конкатенации строк ('*' x $n)."\n"
, там стоит запятая. Тем не менее всё
будет работать. Поясним, в чём разница, опять взяв для определённости
$n
, равное трём. Тогда команда
Perlprint((('*' x $n)."\n") x $n);
равносильна команде
Perlprint "***\n***\n***\n";
В то же время
Perlprint(('*' x $n, "\n") x $n);
равносильно
Perlprint '***', "\n", '***', "\n", '***', "\n";
Дело в том, что оператор репликации x может применяться не
только к строкам, но и к спискам значений. В последнем случае строится список,
включающий несколько экземпляров исходного списка. Например, выражение (1, 2, 3) x 4
даёт (1, 2, 3, 1, 2,
3, 1, 2, 3, 1, 2, 3)
.
Как и в первой версии программы, рисующей сплошной квадрат, организуем двойной
цикл: во внешнем печатается строка, а во внутреннем — клетки шахматной доски
(звёздочки или пробелы). В обоих циклах перебираются значения счётчиков
$j
(номер строки) и $i
(номер столбца).
Но как по номерам строки и столбца определить, какой из значков выводить?
Заметим, что для клетки чётность суммы номеров строки и столбца отличается от той же величины, вычисленной для клеток, соседних с ней (справа, сверху, слева и снизу). ведь у таких соседей меняется чётность одного из слагаемых, а значит, и всей суммы. Если мы договорились, что в левом верхнем углу доски будет чёрная клетка, тогда чёрными должны быть все клетки с чётной суммой, а белыми — с нечётными.
Это наблюдение приводит нас к такому коду:
Perlfor my $j(0..$n-1) { for my $i(0..$n-1) { if(($i+$j)%2==1) { print ' '; } else { print '*'; } } print "\n"; }
(кстати, заголовок условной конструкции if(($i+$j)%2==1)
можно заменить на if(($i+$j)%2)
.
Поскольку и в блоке if, и в блоке else вызывается
print
(с различными параметрами), можно вообще обойтись
без условной конструкции. Вместо неё напишем условный оператор, возложив на
него задачу выбора или звёздочки, или пробела:
Perlfor my $j(0..$n-1) { for my $i(0..$n-1) { print(($i+$j)%2? ' ': '*'); } }
Программу,
рисующую пустой квадрат, оставим без подробных комментариев. В ней будут
отдельно выводиться первая, последняя строки, и все промежуточные. Следует
особо позаботиться о случаях, когда $n
меньше двух.
Эта задача будет посложней предыдущей. Постараемся сформулировать правило, определяющее, будет ли в -й строке и -м столбце звёздочка или пробел.
Звёздочки будут располагаться на границе квадрата (в строках или столбцах с номерами от до ). Затем, по мере удаления от границы квадрата, будут чередоваться слои, заполненные пробелами или звёздочками. Теперь ясно, что наличие звёздочки или пробела зависит от расстояния до границы: если оно чётно, ставим звёздочку, в противном случае — пробел.
Осталось лишь вычислить расстояние до границы квадрата. Это наименьшее из расстояний до всех четырёх сторон. Расстояние до верхней стороны равно , до левой — , до нижней — , до правой — . Таким образом,
В приведённом ниже фрагменте программы четыре присваивания переменной
$d
посвящены как раз вычислению этого минимума:
Perlmy $d; for my $j(0..$n-1) { for my $i(0..$n-1) { $d=$j; $d=$i if $i<$d; $d=$n-$j-1 if $n-$j-1<$d; $d=$n-$i-1 if $n-$i-1<$d; print($d%2? ' ': '*'); } print "\n"; }