Procedury i funkcje


Procedury i funkcje w Pascalu mają składnię zbliżoną do składni programu.

W najprostszej postaci procedury i funkcje wykorzystuje się je w celu zmniejszenia wielkości kodu programu. Załóżmy, że w kilku miejscach programu chcialibyśmy wykonać pewien ciąg instrukcji. Zamiast powielać ten sam ciąg instrukcji po wielekroć możemy w programie zadeklarować procedurę lub funkcję, której ciało zawiara ten ciąg. Następnie, zamiast tego ciągu w programie umieścić należy wywołania tak zadeklarowanej procedury czy fumkcji.

Zacznijmy od składni procedur.
<dekl_Procedury> ::= 
      procedure <Ident> [(<param_Formalne>)];
      <dekl_Stałych> 
      <dekl_Typów> 
      <dekl_Zmiennych> 
      <dekl_Procedur_lub_Funkcji> 
      begin
        <Ciąg_Instrukcji>
      end;

Powyższe należy traktować jak przepis definiowania poprawnie zbudowanych procedur. I tak, deklaracja procedury ma postać podobną do programu pascalowego, z tym że zamiest słowa kluczowego program występuje słowo kluczowe procedure. Nastęnie musi pojawić się identyfikator będący nazwą procedury. W nagłówku procedury, który kończy znak ;, mogą pojawić się jeszcze parametry formalne. Po nagłówku jest miejsce na deklaracje lokalne definiowanej procedury. Po deklaracjach zaś, pomiędzy słowami kluczowymi begin i end, podaje się ciąg instrukcji nazywany ciałem procedury.

Procedury bezparametrowe i bez zmiennych lokalnych

W istocie procedury i funkcje mogą spełniać rolę mechanizmu ułatwiającego konstrukcję algorytmów. Częstokroć w problemie, który mamy zalgorytmizować, daje się wyodrębnić prostsze podproblemy, których rozwiązanie pozwala podać rozwiązanie całego problemu.

Na przykład rozważmy takie zadanie:

Napisz program, który wczytuje z wejścia standardowego dwie liczby, powiedzmy w i k, a następnie drukuje prostokąt złożony ze znaków '@' o wymiarach w na k, to znaczy w-wierszy, po k-znaków '@' w każdym wierszu.

Jeśli założymy, że mamy daną metodę drukowania jednego wiersza, to rozwiązywać powyższe zadanie jest nietrudno: wystarczy w-krotnie zastosować tę metodę. Program realizujący powyższy pomysł może więc wyglądać następująco:

program druk_prost;
var i, w, k : integer;
procedure druk_wiersza;
   begin
      (* druk jednego wiersza *)
   end { druk_wiersza };
begin
   readln(w,k);
   for i := 1 to w do
      druk_wiersza
end. 
Powyższe rozwiązanie ma charakter hipotetyczny. Zakładając, że znajdziemy rozwiązanie prostszego problemu - drukowania jednego wiersza - znaleźliśmy rozwiązanie głównego problemu.

Możemy skoncentrować się teraz na podbroblemie. Po to, by wydrukować wiersz długości k należy k-krotnie wydrukować w danej linii znak '@', a następnie zmienić linię. Efekt ten można osiągnąć przez wykonanie ciągu dwóch instrukcji

   for j := 1 to k do
      write('@');
   writeln
gdzie j : integer. Możemy zatem wstawić powyższe jako ciało procedury druk_wiersza otrzymując poniższy program.
program druk_prost;
var i, j, w, k : integer;
procedure druk_wiersza;
   begin
      for j := 1 to k do
         write('@');
      writeln
   end { druk_wiersza };
begin
   readln(w,k);
   for i := 1 to w do
      druk_wiersza
end. 
Należy zwrócić uwagę, że w programie trzeba było dodać również stosowną deklarację globalnej zmiennej j.

Semantyka (znaczenie) procedur bezparametrowych, bez zmiennych lokalnych

W tej najprostszej postaci procedury traktować należy jedynie jak skróty notacyjne. Znaczy to, że wywołanie bezparametrowej procedury bez zmiennych lokalnych ma to samo znaczenie, co wstawienie w jej miejsce ciała tejże procedury. W naszym konkretnym przypadku oznacza to, ze powyższy program możnaby zastąpić równoważnym mu programem:
program druk_prost;
var i, j, w, k : integer;
begin
   readln(w,k);
   for i := 1 to w do
   begin
      for j := 1 to k do
         write('@');
      writeln
   end 
end. 

Procedury ze zmiennymi lokalnymi

Naszkicowany powyżej sposób postępowanie można z powodzeniem wykorzystywać w produkcji większych systemów oprogramowania. Przy większych projektach jest znaczną niedogodnością konieczność ograniczania inwencji nazewniczej programisty, by uniknąć niezręcznych sytuacji, gdy ta sama zmienna wykorzystywana jest w kilku różnych rolach. W powyższym przykładzie moglibyśmy przecież, myśląc o rozwiązaniu podproblemu, zaproponować jako rozwiązanie

   for i := 1 to k do
      write('@');
   writeln
gdzie i : integer jest użytą już wcześniej zmienną sterującą liczbą drukowanych wierszy. Zgodnie z przedstawioną interpretacją otrzymany wówczas program
program druk_prost;
var i, w, k : integer;
procedure druk_wiersza;
   begin
      for i := 1 to k do
         write('@');
      writeln
   end { druk_wiersza };
begin
   readln(w,k);
   for i := 1 to w do
      druk_wiersza
end. 
równoważny byłby następującemu programowi bez procedur:
program druk_prost;
var i, w, k : integer;
begin
   readln(w,k);
   for i := 1 to w do
   begin
      for i := 1 to k do
         write('@');
      writeln
   end 
end. 
Jak widać w pętli wewnętrznej wartość zmiennej regulującej liczbę wykonań pętli zewnętrznej ulega zmianie. W efekcie, jeśli wczytamy wartości k i w większe niż 0, zostanie wydrukowany jeden wiersz, o ile k jest nie mniejsze niż w. W przeciwnym wypadku program wpadnie w ślepą pętlę.

By zapobiec tego typu sytuacjom, jak również po to by uwolnic programistów od kłopotliwego pamiętania o tym jakie nazwy zostały już użyte, wprowadzono mechanizm deklaracji lokalnych. W odniesieniu do naszego przykładu, myśląc o podprogramie druku wiersza moglibyśmy, tylko na potrzebywykonania ciała procedury, wprowadzić zmienną lokalną do kontroli liczby wykonań wydruku znaku '@'. Na przykład następująco:

program druk_prost;
var i, w, k : integer;
procedure druk_wiersza;
var i : integer;
   begin
      for i := 1 to k do
         write('@');
      writeln
   end { druk_wiersza };
begin
   readln(w,k);
   for i := 1 to w do
      druk_wiersza
end. 

Semantyka procedur ze zmiennymi lokalnymi

Ponieważ zmienna lokalna i ma z założenia tymczasową rolę do odegrania, przyjmuje się że lokalna deklaracja na chwilę zmienia ewentualne deklaracje przypisane tej nazwie. Znaczenie procedury z deklaracją lokalną możnaby wyjaśnić następująco. Program z wywołaniem takiej procedury równoważny jest programowi w którym w miejsce każdego wywołania wpisano zmodyfikowane ciało procedury. Modyfikacja ta sprowadzać się powinna do zastąpienia każdego wystąpienia zmiennej lokalnej jakąś nową nazwą, która nie pojawiła się jeszcze w programie. w naszym przykładzie możnaby to uzyskać na przykład następująco.
program druk_prost;
var i, w, k : integer;
begin
   readln(w,k);
   for i := 1 to w do
   begin
   var i_z_druk_wiersza : integer;
      for i_z_druk_wiersza := 1 to k do
         write('@');
      writeln
   end 
end. 
UWAGA! Powyżej, celem wyjaśnienia idei, wprowadzono notację rodem jeszcze z języka Algol60, pozwalającą wprowadzać deklaracje lokalne w każdym bloku. Nie jest to konwencja dopuszczalna w języku Pascal.

Procedury z parametrami wołanymi przez wartość

Jak na powyższym przykładzie widać, idea procedury daje się łatwo wyjaśnić poprzez odwołanie się do pojęcia zestąpienia tekstu wywołania procedury innym tekstem - zmodyfikowanym lub nie tekstem ciała tej procedury. Dodatkowym mechanizmem zwiększającym praktyczną przydatność procedur jest uzależnienia spobu działania procedury w momencie każdego wywołania od wartości pewnych parametrów.

Wracając do przykładu - poddano już krytyce wykorzystanie zmiennej globalnej do realizacji pewnych zadań lokalnych - w przykładzie chodziło o odliczanie liczby wydruków znaku '@'. Inną zmienną globalną. która pojawia sie w ciele procedury druk_wiersza jest zmienna k. Jej rola jest zasadniczo różna od pomocniczej roli zmiennej lokalnej. To wartość zmiennej k decyduje o liczbie wydrukowanych znaków. Nie jest dobrym styl programowania, który wykorzystuje zmienne globalne do parametryzacji sposobu działania procedury. Powoduje to między innymi zmniejszenie czytelności kodu. W analizowanym już programie:

program druk_prost;
var i, w, k : integer;
procedure druk_wiersza;
var i : integer;
   begin
      for i := 1 to k do
         write('@');
      writeln
   end { druk_wiersza };
begin
   readln(w,k);
   for i := 1 to w do
      druk_wiersza
end. 
z treści wywołania procedury druk_wiersza w programie głównym nie widać, by program ten odnosił się w jakikolwiek sposób do zmiennej k.

By uczytelnić programowanie i umożliwić takie uzależnienie działania procedur od wartości pewnych wyrażeń, wprowadzono mechanizm parametrów wołanych przez wartość. W naszym przypadku należałoby zadeklarować procedurę druk_wiersza jako uzależnioną od wartości parametru określającego liczbę znaków do wydrukowania w wierszu.

program druk_prost;
var i, w, k : integer;
procedure druk_wiersza (ile : integer) ;
var i : integer;
   begin
      for i := 1 to ile do
         write('@');
      writeln
   end { druk_wiersza };
begin
   readln(w,k);
   for i := 1 to w do
      druk_wiersza(k)
end. 

Postępowanie taki może okazać się przydatne, jeśli na przykład zechcemy zmodyfikować zadanie programistyczny i myśleć o drukowaniu bardziej wymyślnych kształtów. Na przykład program

program druk_prost;
var i, n : integer;
procedure druk_wiersza (ile : integer) ;
var i : integer;
   begin
      for i := 1 to ile do
         write('@');
      writeln
   end { druk_wiersza };
begin
   readln(n);
   for i := 1 to n do
      druk_wiersza(i)
end. 
drukował będzie trójkąt. Dla n = 7 otrzymamy na przykład taki wydruk:
@
@@
@@@
@@@@
@@@@@
@@@@@@
@@@@@@@

Semantyka procedur z parametrami wołanymi przez wartość

Parametry wołane przez wartość są bardzo podobne w opisie semantycznym do zmiennych lokalnych. Podobnie do tych ostatnich mają one z założenia tymczasową rolę do odegrania. Przyjmuje się więc, że lokalna deklaracja na chwilę zmienia dotychczasowe deklaracje przypisane nazwie parametru. Zasadnicza różnica między nimi sprowadza się do tego, parametry służą do przekazania do ciała procedury wartości parametru aktualnego wyliczonej w momencie wywołania procedury.

Znaczenie procedury z deklaracją lokalną możnaby wyjaśnić następująco. Program z wywołaniem takiej procedury równoważny jest programowi w którym w miejsce każdego wywołania wpisano zmodyfikowane ciało procedury. Modyfikacja ta sprowadzać się powinna do zastąpienia każdego wystąpienia parametru formalnego wołanego przez wartość jakąś nową nazwą, która nie pojawiła się jeszcze w programie. w naszym przykładzie możnaby to uzyskać na przykład następująco.

program druk_prost;
var i, w, k : integer;
begin
   readln(w,k);
   for i := 1 to w do
   begin
   var i_z_druk_wiersza : integer;
   var ile_z_druk_wiersza : integer;
      ile_z_druk_wiersza := k;
      for i_z_druk_wiersza := 1 to ile_z_druk_wiersza do
         write('@');
      writeln
   end 
end. 
Jak widać różnica w traktowaniu zmiennych lokalnych i parametrów wołanych przez wartość sprowadza się do przekazania wartości parametrowi w momencie wywołania.

UWAGA! Jak poprzednio, celem przekazania idei, użyto notacji deklaracji lokalnej w bloku, która nie jest dopuszczalna w języku Pascal.

Procedury z parametrami wołanymi przez nazwę

Parametryzacja procedur opisana powyżej ma tę istotną wadę, że nie pozwala na eksport wyników obliczeń przeprowadzonych w ciele procedury na zewnątrz, poza jej ciało. Można to, oczywiście, osiągnąć korzystając ze zmiennych globalnych. Utrudniać to będzie jednak analizowanie tekstów długich programów, bo wymagałoby od każdego z programistów pamiętania tego, która procedura może zmieniać jakie zmienne globalne.

By unikąć tego dylematu w Pascalu można wołać parametry przez nazwę lub lokację. Idea jest taka sama jak w przypadku procedur wejścia/wyjścia. Przy drukowaniu parametry wołane są przez wartość: write(2*7+1) może być użyte z dowolnym wyrażeniem typu prostego jako parametrem aktualnym. Przy czytaniu parametrami są zmienne, lub ogólniej - lokacje, gdyż intencją jest nadanie im nowych wartości zgodnie z tym co pobrane zostanie z wejścia.

W sposób naturalny procedury z parametrami wołanymi przez zmienną stosuje się do obróbki złożonych struktur danych: tablic, rekordów, a zwłaszcza struktur dynamicznych (listy, drzewa, ...). Idea jest zawsze taka sama - chodzi o zmianę stanu jakieś zmiennej.

program tablice;
const  n = 5;
type tab = array [1..n] of real;
var t1, t2, t3 : tab;
procedure generuj (var t : tab) ;
var i : integer;
   begin
      randomize;
      for i := 1 to n do
         t[i] := random
   end { generuj };
procedure drukuj (t : tab) ;
var i : integer;
   begin
      for i := 1 to n do
         write(t[i]:8:5)
   end { drukuj };
begin
   generuj(t1);  generuj(t2);  generuj(t3);
   writeln('tablica 1'); drukuj(t1);  
   writeln('tablica 2'); drukuj(t2);  
   writeln('tablica 3'); drukuj(t3);  
end. 
W obu procedurach wykorzystano stałą globalną n - nie jest to tak niebezpieczne jak użycie zmiennych globalnych, gdyż wartości stałych w programie nie można zmieniać.

Wykonanie powyższego programu dać może następujące rezultaty:

sh-2.05b$ ./tablice
tablica 1
 0.41835 0.89078 0.54621 0.61766 0.71612
tablica 2
 0.32527 0.35698 0.49459 0.63709 0.50140
tablica 3
 0.30091 0.66060 0.05772 0.36933 0.44402
sh-2.05b$
Jak widać z powyższego, do drukowania wystarczyło przekazanie tablic jako parametrów przez watrość - procedura drukuj nie ma intencji zmiany stanu swego argumentu, a jedynie odczyt zapisanych w tablicy wartości. Intencją procedura generuj jest jednak zmiana stanu tablicy - w przykładzie chodzi o wypełnienie jej komórek liczbami pseudolosowymi. Dla prawidłowego działania generuj wołanie przez nazwę parametru ma znaczenie podstawowe. Gdyby zastąpić je wołaniem przez wartość otrzymalibyśmy (w przypadku freePascala) następujący rezultat:
sh-2.05b$ fpc tablice.pas
Free Pascal Compiler version 1.9.4 [2004/05/30] for i386
Copyright (c) 1993-2004 by Florian Klaempfl
Target OS: Linux for i386
Compiling tablice.pas
tablice.pas(20,4) Warning: Variable "t1" does not seem to be initialized
tablice.pas(21,4) Warning: Variable "t2" does not seem to be initialized
tablice.pas(22,4) Warning: Variable "t3" does not seem to be initialized
Linking tablice
26 Lines compiled, 0.1 sec
sh-2.05b$ ./tablice
tablica 1
 0.00000 0.00000 0.00000 0.00000 0.00000
tablica 2
 0.00000 0.00000 0.00000 0.00000 0.00000
tablica 3
 0.00000 0.00000 0.00000 0.00000 0.00000
sh-2.05b$

UWAGA! W Pascalu parametry wołane przez zmienną można w pewnych sytuacjach zastąpić funkcjami zwracającymi wartości odpowiedniego typu. W zasadzie ogranicza się to jednak tylko do jednej wartości i to wartości typu prostego. To ostatnie zależne bywa od implementacji.

Semantyka procedur z parametrami wołanymi przez nazwę

Tak jak i poprzednio, semantykę wywołania procedury z parametrem wołanym przez nazwę przedstawimy na przykładzie, wyjaśniając ją poprzez mechanizm zastępowania wywołania przez odpowiednio zmodyfikowane ciało procedury. Modyfikacje te sprowadzają się do zastąpienia w ciele procedury parametru formalnego wołanego przez nazwę przez parametr aktualny, który musi być lokacją. W przypadku programu tablice dałoby to następujący rezultat:
program tablice;
const  n = 5;
type tab = array [1..n] of real;
var t1, t2, t3 : tab;
begin
   begin
   var i_z_generuj : integer;
      randomize;
      for i_z_generuj := 1 to n do
         t1[i] := random
   end { generuj };
   begin
   var i_z_generuj : integer;
      randomize;
      for i_z_generuj := 1 to n do
         t2[i] := random
   end { generuj };
   begin
   var i_z_generuj : integer;
      randomize;
      for i_z_generuj := 1 to n do
         t3[i] := random
   end { generuj };

   writeln('tablica 1'); 
   begin
   var i_z_drukuj : integer;
   var t_z_drukuj : tab;
      t_z_drukuj :=  t1;
      for i_z_generuj := 1 to n do
         write(t_z_drukuj[i]:8:5);
      writeln
   end { drukuj };

   writeln('tablica 2'); 
   begin
   var i_z_drukuj : integer;
   var t_z_drukuj : tab;
      t_z_drukuj :=  t2;
      for i_z_generuj := 1 to n do
         write(t_z_drukuj[i]:8:5);
      writeln
   end { drukuj };
   writeln('tablica 3'); 
   begin
   var i_z_drukuj : integer;
   var t_z_drukuj : tab;
      t_z_drukuj :=  t3;
      for i_z_generuj := 1 to n do
         write(t_z_drukuj[i]:8:5);
      writeln
   end { drukuj };
end. 
Powyższy przykład demonstruje jednocześnie oba sposoby wołania parametrów. W ogólnym przypadku procedura może mieć wiele parametrów i/lub zmiennych lokalnych. Wszystkie one obsługiwane są według przedstawionych tu zasad.

Funkcje

Składnia funkcji jest bardzo zbliżona do składni procedur.
<dekl_Procedury> ::= 
      function <Ident> [(<param_Formalne>)] : <typ_prosty>;
      <dekl_Stałych> 
      <dekl_Typów> 
      <dekl_Zmiennych> 
      <dekl_Procedur_lub_Funkcji> 
      begin
        <Ciąg_Instrukcji>
      end;
Idea jest taka, że ciąg instrukcji w ciele funkcji służyć ma obliczeniu wartości, którą funkcja miałaby zwracać. W przekazaiu wartości służy instrukcja przypisania pod nazwę funkcji tej wyliczonej wartości. W związku z tym wywołanie funkcji typu, na przykład integer, ma prawo pojawić się w dowolnym miejscu programu, w którym może pojawić się dowolne wyrażenie typu integer.

Dla przykładu, w poniższym programie zadeklarowano rekursywną funkcję NWD wyliczającą największy wspólny dzielnik dwóch liczb - podawanych jako parametry funkcji.

program Euklid;
var k, l : integer;
function NWD(m,n : integer) : integer ;
   begin
      if m = n then NWD := m
	  else if m > n then NWD := NWD(m-n,n)
	       else NWD := NWD(m,n-m)
   end { NWD };
begin
   readln(k,l);
   writeln('NWD(', k, ',', l, ')= ', NWD(k,l))
end. 
Kolorem podkreślono te instrukcje przypisania, które nadają nazwie funkcji wartość zwracaną w momencie jej wywołania.

Jak działał będzie ten program dla danych k = 24 oraz l = 18 ?

A teraz to samo - zrealizowane bez rekursji. Tym razem przekazanie wyniku na zewnątrz następuje w jednym miejscu.
program Euklid;
var k, l, wynik : integer;
function NWD(m,n : integer) : integer ;
   begin
      while m <> n do 
        if m > n then m := m-n
        else n := n-m;
      NWD := m
   end { NWD };
begin
   readln(k,l); 
   wynik := NWD(k,l);
   writeln('NWD(', k, ',', l, ')= ', wynik)
end. 

Jak działał będzie ten program dla danych k = 24 oraz l = 18 gdy w deklaracji funkcji parametry wołane będą przez nazwę ? Nagłówek funkcji będzie miał wtedy postać:

function NWD(var m,n : integer) : integer ;