от Курс за ССОК
Увод в програмирането на Perl
Лекция #6 Подпрограми
Доказан начин да напишем голяма програма е да я конструираме от по-малки парчета код или подпрограми. Потребителските подпрограми в Perl (или още subroutines) могат да се дефинират веднъж и да бъдат ползвани на много места, където са ни необходими. Естествено, където е възможно, е добре да се ползват многобройните вградени функции (built-in functions). Добре е да се разгледа perlfunc на вашата дистрибуция. В допълнение на вградените функции има и доста много пакети и модули, лесни за сваляне и готови за употреба (макар някои да са alpha версия) na www.cpan.org, така че не преоткривайте колелото, освен ако наистина не се налага ;)
Подпрограмите могат да се извикват като подадем името на подпрограмата и аргументите (ако има такива). Извикващата част няма нужна да знае детайлите на имплементацията на съответната подпрограма. Добър подход е главната програма да се разделя на подпрограми, т.е. да се разбива на функционални части. По този начин е по-лесна за промяна и развиване на по-късен етап. Също така се получава преизползване на съществуващите, вече написани подпрограми (software reuse) като блокове за следващи програми, които изискват подобна фукнционалност. Друг важен фактор е намаляване на повтарящия се код. Добре е всяка подпрограма, която се пише да извършва единична, добре дефинирана задача, която еднозначно се подразбира от името й (ако не може да изберете име за подпрограмата си, обикновено означава, че се опитва да направи повече неща, отколкото е необходимо).
В Perl има доста удобни вградени функции, които извършват математически операции. Примерно sqrt(9) връща стойност 3, като фукнцията sqrt приема скалар за аргумент и връща такъв. Аргументи на подпрограмите могат да бъдат масиви, хешове, препратки, скалари, както и изрази. Някои от математическите функции са:
- cos($x) : косинус от х (х в радиани)
- sin($x) : синус от х (х в радиани)
- exp($x) : е на степен х (ех)
- abs($x) : абсолютната стойност на х
- log($x) : натурален логаритъм от х
- sqrt($x): корен квадратен от х
Вградените математически функции ползват $_ като аргумент по подразбиране:
#!/usr/bin/perl
use warnings;
$_ = -7;
print “Absolute value of 5 is: “, abs(5), “\n”,
“Absolute value of $_ is: “, abs, “\n”,
“exp($_) is: “, exp, “\n\n”;
Следващият пример илюстрира извикването на две съвсем опростени подпрограми:
#!/usr/bin/perl
use warnings;
sub1();
sub2();
sub sub1 {
print “hi from sub1!\n”;
}
sub sub2 {
print “hi from sub2!\n”;
}
Когато се извиква подпрограма, контрола се прехвърля в началото на дефиницията на извикваната подпрограма, като след това се изпълнява тялото й, след което изпълнението завършва и контрола се връща точно след извикването й. Форматът на една подпрограма е (sub e ключова дума):
sub subName {
some statements
}
Списъкът с аргументи, подаван на една функция се пази в специалната променлива масив @_:
#!/usr/bin/perl
use warnings;
showArgs(“Ivo”, “Petko”, 10, 8.6, 3, 7.77);
sub showArgs {
print “The arguments list is: @_\n\n“;
print “$_\t” foreach (@_) {
print “\n”;
}
Специалната променлива @_ “сплесква” (flattening) всички подадени й масиви и хешове. Ако подадем масив и скалар, то @_ ще съдържа един списък от скалари. Ако подадем хеш, то той ще придобие вида (ключ1, стойност1, ключ2, стойност2, ...). Сплескването (flattening) може да се предотврати чрез препратки (по късно за това).
Когато една подпрограма завърши изпълнението си, ползваме ключовата дума return, за да се върнем към извикващата точка:
#!/usr/bin/perl
use warnings;
print square($_), “ ” foreach (1..10);
print “\n”;
sub square {
$value = shift;
return $value ** 2;
}
Забелязва се, че когато ползваме shift без аргументи, функцията работи върху @_. Подпрограмата терминира веднага след изпълнението на return. Ако няма return, то тогава подпрограмата връща стойността на последния изпълнен statement.
В Perl, подпрограмата може да върне скалар или списък. Дори, тя може да върне скалар ИЛИ списък в зависимост от контекста, в който е извикана. Функцията wantarray, извикана в тялото на подпрограма, връща истина ако подпрограмата е викната в списъчен контекст и лъжа ако е извикана в скаларен контекст:
#!/usr/bin/perl
use warnings;
@array = scalarOrList();
$” = “\n”;
print “Returned:\n@array\n”;
print “Returned: ” . scalarOrList() . “\n\n”;
sub scalarOrList {
wantarray ?
return 'list', 'context' :
return 'scalar', 'context'; #return values?
}
Има няколко начина за извикване на една подпрограма. Perl ползва типови идентификатори, за да различава типовете. За подпрограми типовият идентификатор е &, т.е. &sub1; ще извика подпрограмата sub1. Можем да ползваме този синтаксис (без скоби) за извикване на подпрограма без аргументи или за подпрограма, която ще получи @_ на повиквача (caller) като аргумент по подразбиране. Скобите са необходими ако подаваме явно аргументи към подпрограма. Друг начин за извикване на дадена подпрограма е да ползваме само името й (bareword). Ако подпрограмата е дефинирана преди този bareword, то тя ще бъде повикана. Ако обаче този bareword се появява преди дефиницията на подпрограмата, то Perl ще интерпретира съответния bareword като низ и няма да извика подпрограмата (ползването на barewords не е добра практика, защото може да доведе до трудни за улавяне логически грешки; хубаво е да се ползват скоби в повикване към съответната подпрограма):
#!/usr/bin/perl
use warnings;
sub definedBeforeNoArgs {
print “definedBeforeNoArgs\n”;
}
sub definedBeforeWithArgs {
print “definedBeforeWithArgs: @_\n”;
}
#calling those defined before use
print “\nUsing & and ():\n”;
&definedBeforeNoArgs();
&definedBeforeWithArgs(1, 2, 3);
print “\nUsing only ():\n”;
definedBeforeNoArgs();
definedBeforeWithArgs(1, 2, 3);
print “\nUsing only &:\n”;
&definedBeforeNoArgs;
print “\”&definedBeforeWithArgs 1, 2, 3\””,
“ generates a syntax error!\n”;
print “\nUsing bareword:\n”;
definedBeforeNoArgs;
definedBeforeWithArgs 1, 2, 3;
#calling those defined after use
print “\n\n\n\nUsing & and ():\n”;
&definedAfterNoArgs();
&definedAfterWithArgs(1, 2, 3);
print “\nUsing only ():\n”;
definedAfterNoArgs();
definedAfterWithArgs(1, 2, 3);
print “\nUsing only &:\n”;
&definedAfterNoArgs;
print “\”&definedAfterWithArgs 1, 2, 3\””,
“ generates a syntax error!\n”;
print “\nUsing bareword:\n”;
definedAfterNoArgs;
print “\”definedAfterWithArgs 1, 2, 3\””,
“ generates a syntax error!\n”;
sub definedAfterNoArgs {
print “definedAfterNoArgs\n”;
}
sub definedAfterWithArgs {
print “definedAfterWithArgs: @_\n”;
}
Забелязва се, че когато викаме подпрограма с &, без (), и подаваме аргументи явно, се генерира синтактична грешка.
Сега преминаваме към една по-отдалечена (и по-интересна ;)) тема, а именно генерация на случайни числа. Функцията rand генерира скалар (floating-point) по-голям или равен на 0 и по-малък от 1 (0 <= $х < 1). Всяко число в този диапазон има еднаква вероятност да се падне при извикване на rand. Можем да специфицираме диапазона на rand, като му подадем числов аргумент (тогава rand ще върне стойност >= 0 и по-малка от аргумента). За да получим цяло число, можем да ползваме int, която взема цялата част на аргумента ( int(4.677) e 4 ):
#!/usr/bin/perl
use warnings;
print “\nRandoms produced by rand():\n”;
print “ ”, rand(), “\n” foreach (1..3);
print “\nRandoms produced by rand(100):\n”;
print “ ”, rand(100), “\n” foreach (1..3);
print “\nRandoms produced by 1 + int( rand(6) ):\n”;
print “ ”, 1+ int( rand(6) ), “\n” foreach (1..3);
Функцията rand всъщност генерира псевдослучайни числа. Случайните числа на rand са базирани на алгоритъм, който използва предишното случайно число и сийд (seed). Ако знаем алгоритъма и съответния seed можем да намерим следващото случайно число. Може да се ползва функцията srand за указване на seed:
#!/usr/bin/perl
use warnings;
print “\n\nSetting seed to 1\n” foreach (1..3);
srand(1);
print “ “, 1 + int( rand(6) ) foreach (1..3);
print “\n\nReset seed\n”;
srand();
print “\n\nAfter seed has been reset\n” foreach(1..3);
print “ “, 1 + int( rand(6) ) foreach (1..3);
print “\n”;
Можем да ползваме srand когато се дебъгват програми със случайни числа. Ползване на rand без да се указва seed позволява на този seed да се вземе от системното време.
За някои програми се налага да ползваме рекурсия, т.е. подпрограма викаща себе си, директно или индиректно (чрез друга подпрограма). Рекурсивната подпрограма знае да решава само най-простите случаи (наричани още base cases). Когато подпрограмата е извикана с този (или тези) най-прости случаи, тя просто връща стойност. Ако съответната подпрограма се вика с по-сложен случай, то подпрограмата разделя проблема на две части: част, която знае как да реши и такава, която не знае как да реши, като частта, която не знае как да реши трябва да наподобява първоначалния проблем, но да е по-проста версия на него. Факториел на дадено число може да бъде намерен итеративно по следния начин:
$factorial = 1;
foreach (1..$number) {
$factorial *= $_;
}
Забелязва се, че n! = n . (n-1)!. С рекурсия решението е следното:
#!/usr/bin/perl
#find factorials from 1 to 10
use warnings;
foreach (0..10) {
print “$_! = “ . factorial($_) . “\n”;
}
sub factorial {
my $num = shift;
if ($num <= 1) { #base case
return 1;
} else {
return $num * factorial($num – 1);
}
}
Рекурсията е тежка операция, тъй като включва многобройни повиквания към подпрограми. Това може да е скъпо от гледна точка на процесорно време и памет. Всяко рекурсивно повикване причинява още едно повикване към подпрограмата. Всички задачи, които могат да се решат рекурсивно, имат и итеративно решение, но рекурсията е избирана заради по-естественото отразяване на проблема, по-лесен дебъг и подръжка (често итеративното решение не се вижда лесно).
Видимостта на променливите е важен аспект от всяка програма. В Perl има три видими области (scopes): глобална, лексикална и динамична. Променливите, ползвани дотук, бяха глобални. Глобалните променливи могат да бъдат манипулирани навсякъде из програмата. Променливи, дефинирани без ключови думи, са автоматично глобални променливи. Лексикална променлива е такава, която съществува само в блока, в който е дефинирана. Тези променливи са достъпни за блока, в който са дефинирани. Динамичните променливи са видими в блока, в който са дефинирани и са достъпни за подпрограмите, които се викат от този блок. За дефиниране на видимост се ползват ключовите думи my, our, и local. Думата our явно дефинира една променлива като глобална. Думата my дефинира лексикална променлива. Думата local дефинира динамична променлива:
#!/usr/bin/perl
use warnings;
print “Without globals: \n”;
sub1();
our $x = 7;
our $y = 17;
print “\nWith globals: ”;
print “\nglobal \$x: $x”;
print “\nglobal \$y: $y\n”;
sub1();
print “\nglobal \$x: $x”;
print “\nglobal \$y: $y\n”;
sub sub1 {
my $x = 10;
local $y = 5;
print “\$x in sub1: $x\t(lexical to sub1)\n”;
print “\$y in sub1: $y\t(dynamic to sub1)\n”;
sub2();
}
sub sub2 {
print “\$x in sub2: $x\t(global)\n”;
print “\$y in sub2: $y\t(dynamic to sub1)\n”;
}
Забелязва се, че лексикалните и динамични променливи в една подпрограма скриват глобалните такива (не ги унищожават). Ключовата дума local, приложена за $у, всъщност създава временно копие на глобалната промнелива. Старата стойност е запазена, докато има нова стойност в $у. Когато $у изчезне, се възстановява старата стойност. Не е добра практика да се ползват едни и същи имена, които скриват съответните глобални променливи. В общия случай се препоръчва глобалните променливи да се избягват. Също така, по-добре е една променлива да се подаде на подпрограма, отколкото да се използва local.
Perl използва пакети (packages или namespaces), за да определи достъпността на променливите и подпрограмните идентификатори. Пакетите могат да бъдат използвани за достъп до идентификатори, дефинирани в други файлове, наречени модули. Повечето от правилата за видимост произлизат от концепцията за пакет (package). Всеки пакет си има своя символна таблица, която пази всички променливи и имена на подпрограми на пакета. По подразбиране, глобалните идентификатори в един Perl сорс файл (глобалните променливи и имената на подпрограмите) са част от символната таблица на main пакета. Всъщност, Perl няма истински глобални променливи, а по-скоро пакетни глобални променливи или пакетни променливи. Лексикалните променливи не се поставят в символната таблица на пакета. Всеки блок, в който има някакви локални променливи, си има свой собствен временен “склад” (scratchpad). Символната таблица всъщност е един хеш със идентификатори за ключове и местоположения в паметта за стойности (ключовете на хеша трябва да са уникални, т.е. и променливите трябва да имат уникални имена; @somevar и $somevar са позволени в един пакет, защото имат различни типове).
Досега сме работили само с пакета main (това е името на пакета по подразбиране, ако не сме указали такова). Текущият пакет може да се промени чрез package. Създавайки нов пакет, Perl създава и нова символна таблица (празна). Няколко пакета в една програма могат да имат променливи със същите имена без да има конфликт:
#!/usr/bin/perl
#in one file
use warnings;
require FirstPackage;
our $var = “happy”;
print “From main:\n”;
print “\$var = $var\n”;
print “\$main::var = $main::var\n”;
print “\nFrom FirstPackage:\n”;
print “\$FirstPackage::var = $FirstPackage::var\n”;
print “\$FirstPackage::another = $FirstPackage::another\n”;
FirstPackage::displayFirstPackageVars();
FirstPackage.pm:
#!/usr/bin/perl
use warnings;
package FirstPackage;
our $var = “birthday”;
my $another = “new year”;
sub displayFirstPackageVars {
print “\$var in displayFirstPackageVars = ”, $var, “\n”;
print “\$another in displayFirstPackageVars = ”,
$another, “\n”;
}
Ключовата дума require указва на Perl да намери FirstPackage.pm и да го добави към програмата, а Perl търси в текущата директория. Ако файла не може да бъде намерен там, то Perl търси в масива @INC. Тази вградена специална променлива - масив указва къде къде се намират вградените Perl библиотеки на съответната машина. Ключовата дума require предполага разширение .pm при търсене, т.е. няма нужда да указваме .pm в сорс файла. Пълното име (fully qualified name) на една променлива се дава, като се указва първо пакета и след него името на променливата, разделени от :: ($packageName::varName). Taka Perl знае в коя символна таблица да търси съответната променлива.
Модулът е просто пакет, който позволява повече контрол относно как ползвателя на съответния модул може да реферира идентификаторите в пакета на този модул. Плюс на модула е, че разрешава на програмиста да указва идентификаторите да бъдат винаги достъпни до клиентския модул все едно тези идентификатори са оригинално дефинирани в програмата на ползвателя. По този начин идентификаторите могат да бъдат ползвани без да има нужда от пълното им име. Също така, ако искаме да импортваме модули и пакети по време на “компилация”, можем да ползваме use вместо require, който прави това по време на изпълнение. Ползвайки use, Perl може да осигури присъствието на пакета преди изпълнение (иначе може да не се забележи че един пакет липсва докато не се реферира по време на изпълнение):
#!/usr/bin/perl
#program to use our module
use FirstModule;
print “Using automatically imported names:\n”;
print “\@array contains: @array\n”;
greetings();
FirstModule.pm:
#!/usr/bin/perl
package FirstModule;
use Exporter;
our @ISA = qw (Exporter);
our @EXPORT = qw (@array &greetings);
our @array = (1, 2, 3);
sub greetings {
print “Modules are nice! :~)”;
}
return 1; #indicates successful import of module
Модулът има няколко допълнителни изисквания освен тези на пакета. Той трябва да има разширение .pm. Ключовата дума require позволява да се дават други разширения (.pl), но use не позволява това. Perl позволява на модулния програмист да експортира идентификатори от един модул за ползване от namespace на друг файл. За да се получи това, трябва съответния модул да е Exporter (той дава функционалността за export на идентификатори за употреба в други файлове). Със израза our @ISA = qw (Exporter); индикираме, че специалната вградена променлива – масив @ISA съдържа Exporter или иначе казано текущия модул е Exporter. Това е пример за наследяване (повече за това по-напред в лекциите). Другият важен момент е този statement: our @EXPORT = qw (@array &greetings); Тук указваме идентификаторите, които нашия модул може да експортира, като добавим идентификаторните елементи към специалната вградена променлива – масив @EXPORT. Така всяка програма, която ползва този модул ще има директен достъп до идентификаторите в този масив без да има нужда от пълното име. return 1; е задължително за модули, които са импортнати със use. Достигането до този код гарантира, че модулът е успешно импортнат, като този statement трябва да връща истина, в противен случай създаваме фатална грешка по време на “компилация”.
Със use може да правим и други интересни неща, като например да указваме версията на Perl, която ни е нужна:
use v5.6.0;
Perl сравнява тази версия със версията на Perl, инсталирана на ползваната машина. Ако версията на машината е по-ниска, генерира се фатална грешка програмата терминира. Аналогично можем да указваме и версията на определен модул, който ползваме:
use someModule 2.0;
Версията на модула пък можем да укажем чрез:
our $VERSION = 2.0;
Освен че може да се импортва цял модул, с Perl можем и да специфицираме части от модул, които да се вкарат:
use FirstModule qw(@array); #imports only @array
Добра практика е да се импортват само тези идентификатори, който трябват, защото в противен случай можем да “замърсим” съответния namespace. Също така, ако не искаме да импортнем нито един идентификатор можем да използваме празен списък (идентификаторите ще са достъпни чрез пълните им имена).
Ключовата дума use си има и противоположност, а именно no. С no можем да “отпратим” указани идентификатори от съответния namespace. По този начин могат да се дефинират подпрограми, който иначе биха били импортвани от друг модул.
Не на последно място по важност са прагмите в Perl. С тях може да се укаже на “компилатора” да ползва разни възможности, които иначе не би ползвал. Две от най-важните прагми са use strict; и use warnings; , като режима strict задължава програмиста да декларира всички променливи като променливи на пакета или като лексикални променливи. Другите задължения са да се ползват кавички около всеки низ и да се вика всяка подпрограма явно. На strict могат да се подават тагове, като например 'vars' и 'subs'. use strict 'vars', поставено в началото на дадена програма проверява дали всички пакетни променливи са указани с пакетното си име (примерно ако имаме една променлива $var, то тя трябва да е $main::var).
Има 4 начина една променлива да е валидна за strict. Първият начин е лексикални променливи (дефинирани с my) да бъдат реферирани със краткото си име в блока, в който са дефинирани. Вторият е пакетни променливи в символната таблица (дефинирани с our) да са достъпни до пакета,в който са дефинирани с краткото си име, а във външни пакети да са достъпни със пълното си име. Третият е променливи да бъдат достъпни чрез пълното си име при всички случаи (независимо в или извън техния пакет). Накрая, една програма може да може да укаже use vars; последван от списък с имена на променливи:
use vars qw(var1 var2 var3);
За всяко име се създава глобална пакетна променлива, която е достъпна с краткото си име в текущия пакет.
Ако пък укажем use strict 'subs'; , то Perl отказва да се викат подпрограми с bareword.
Другата важна прагма, use warnings; (от 5.6 насам), предупреждава за грешки при писане, неинициализирани стойности, както и разни други потенциални проблеми, като нито един от тях не е фатален.
Ако пък искаме в част от код да изключим strict, то можем да ползваме no strict; в този блок.
Със use също можем да създаваме константи, например:
use constant PI => 3.14159;
След това в кода можем да ползваме PI, вместо числената стойност.
Друга интересна прагма е use diagnostics; , която дава по-детайлни съобщения за грешки. Тази прагма може да бъде включвана и изключвана по време на изпълнение на програмата със enable() и disable().
Интерес представлява и прагмата use integer;. Тя указва на “компилатора” да извършва всички аритметични операции като целочислени такива. Това може да се ползва ако имаме дълъг блок от код само със цели числа, където не искаме пред всяка операция да слагаме оператора int.
Задачи
Задача 1
Да се симулира хвърляне на зар 6000 пъти и да се изведе в табуларен формат броя
на попаденията на всяка страна на зара (приема се, че зара има 6 страни >
стойност от 1 до 6).
Задача 2
Да се напише програма, която симулира играта “Craps” със следните правила:
Един играч хвърля 2 зара (старндартни, като в предната задача). Сборът от двата
хвърлени зара се намира и ако сумата е 7 или 11 на първото хвърляне, то играчът
печели; ако сумата е 2, 3 или 12 на първото хвърляне, то играчът губи. Ако сумата е
4, 5, 6, 8, 9, 10 на първото хвърляне, то тази стойност става “точка” на играча. За да
спечели, трябва да продължи да хвърля зарчетата докато не направи “точката” си.
Играчът губи, ако хвърли 7 преди да направи точката си.
Задача 3
Всяко следващо число в редицата на Фибоначи представлява сбор на предишните
две числа, като първите две са 0 и 1:
0, 1, 1, 2, 3, 5, 8, 13, 21, ....
Да се напише програма, която изчислява рекурсивно и итеративно стойността на
съответния елемент за реда на Фибоначи за произволно въведено число (> 0), което
потребителя въвежда. Примерен изход:
Enter a number ( > 0): 6
fibonacci(6) = 8
Enter a number ( > 0): 10
fibonacci(10) = 55
Enter a number ( > 0 ): 35
fibonacci(35) = 9227465
Решения