W10

Procedury
=========


Deklarowanie typów
------------------

Tak jak można w Pascalu nazywać (deklarować) stałe, tak samo
można nazywać (deklarować) typy. Nazywanie typów daje dwie
korzyści:
 - przyjemniej (i czytelniej) jest napisać:
     var w: wektor;
   zamiast
     var w: array[1..n] of integer;
 - przy  deklarowaniu parametrów procedur i funkcji (o czym
   dalej) trzeba podawać nazwę typu parametru, a nie sam typ.

Deklaracje  typów zaczynamy słowem kluczowym  type, po którym
następuje ciąg deklaracji poszczególnych typów, każda postaci:

   <nazwa typu> = <typ>;
   
Oto przykład deklaracji kilku typów (wraz z już wspomnianym
typem wektor):

   type
     wektor = array[1..n] of integer;
     cyfry  = '0'..'9';
     indeks = 0..100;

Zadeklarowanych w ten sposób typów używamy tak samo  jak  typów
poznanych poprzednio.

Procedury i funkcje
-------------------

Często  pisząc program zauważamy, że kilkakrotnie  występuje  w
nim  taka  sama  (lub  prawie taka sama) sekwencja  instrukcji.
Popatrzmy  na  przykład.  Załóżmy, że  chcemy  napisać  program
wczytujący dwa wektory i wypisujący trzeci, będący ich sumą:

    {$R+,Q+}
    program tablice1;

    const
      n = 10;  {Rozmiar wektora}
    
    type
      indeks = 1..n;
      wektor = array[indeks] of integer; {Typ wektorów}
    
    var
      i       : indeks;
      a, b, c : wektor;  {a i b wektory zadane, c wektor wynikowy}
    
    begin
    
     {Wczytanie pierwszego wektora}
      for i:=1 to n do
       begin
        writeln('Wczytuję wektor, podaj ', i, ' element (z ', n, '): ');
	readln(a[i]);
       end;
    
     {Wczytanie drugiego wektora}
      for i:=1 to n do
       begin
        writeln('Wczytuję wektor, podaj ', i, ' element (z ', n, '): ');
	readln(b[i]);
       end;
    
     {Obliczenia}
      for i:=1 to n do
        c[i] := a[i] + b[i];
    
     {Wypisanie wyniku}
      for i:=1 to n do
        Writeln('Wynik[', i, ']  =  ', c[i]);
    end.

Jak  widać  dwukrotnie powtarza się niemal identyczny  fragment
programu, służący do wczytania z wejścia wektora. Nie ma  sensu
pisać dwa (lub więcej) razy (prawie) tej samej rzeczy, bo:
 - napisanie czegoś dwa razy wymaga więcej czasu,
 - program staje się dłuższy,
 - jeśli będziemy chcieli coś poprawić (np. usunąć błąd),  to
   będziemy  musieli  to  robić  w kilku  miejscach,  więc  znów
   niepotrzebnie stracimy czas,
 - istnieje  duże  prawdopodobieństwo, że w  kilku  częściach
   programu coś poprawimy, a w jednej zapomnimy - tego typu błędy
   są  szczególnie przykre, gdyż jesteśmy pewni, że poprawiliśmy
   program (bo poprawialiśmy), a program dalej nie działa tak, jak
   byśmy tego chcieli.

Rozwiązaniem   tego  problemu  są  procedury,   czyli   nazwane
fragmenty programu. Deklaracja procedury ma następującą  postać
(na razie):

   procedure <nazwa>;
     begin
      <ciąg instrukcji>
     end;

Po zadeklarowaniu procedury można ją wielokrotnie wywoływać
pisząc jej nazwę. Wywołanie procedury spowoduje wykonanie jedna
po drugiej zawartych w niej instrukcji. Popatrzmy znów na
przykład:

    {$R+,Q+}
    program tablice2;
    
    const
      n = 10;  {Rozmiar wektora}
    
    type
      indeks = 1..n;
      wektor = array[indeks] of integer; {Typ wektorów}
    
    var
      x       : wektor;
      i       : indeks;
      a, b, c : wektor;  {a i b wektory zadane, c wektor wynikowy}
    
    procedure wczytaj_wektor_na_x;
    begin
      for i:=1 to n do
       begin
        writeln('Wczytuję wektor, podaj ', i, ' element (z ', n, '): ');
	readln(x[i]);
       end;
    end;
    
    begin
    
     {Wczytanie pierwszego wektora}
      wczytaj_wektor_na_x;
      a := x;
    
     {Wczytanie drugiego wektora}
      wczytaj_wektor_na_x;
      b := x;
    
     {Obliczenia}
      for i:=1 to n do
        c[i] := a[i] + b[i];
    
     {Wypisanie wyniku}
      for i:=1 to n do
        Writeln('Wynik[', i, ']  =  ', c[i]);
    end.

Co  zyskaliśmy? Jeśli będziemy chcieli zmodyfikować wczytywanie
wektora, to wystarczy to teraz zrobić tylko w jednym miejscu  -
w treści procedury. Jednak nie to jest najważniejszą korzyścią.
To co jest najważniejsze, to zwiększenie czytelności programu:
 - po  pierwsze, wyraźnie wskazaliśmy ten fragment  programu,
   który wczytuje wektor, osoba czytająca program zyskuje dzięki
   temu dodatkowe informacje,
 - po  drugie, jawnie zaznaczyliśmy, że wczytanie  wektora  a
   jest  taką  samą operacją jak wczytanie wektora b. Poprzednio
   żeby to zauważyć, trzeba było wnikliwie porównać oba fragmenty
   programu wczytujące wektory.

Skoro uznaliśmy, że najważniejszą korzyścią jaką dają procedury
jest  zwiększenie  czytelności programu, nie  może  dziwić,  że
procedury  będziemy  tworzyć nawet wtedy, gdy  będą  wywoływane
tylko  jeden  raz.  W  naszym  przykładzie  możemy  dodać  dwie
procedury, dodającą i wypisującą:

    {$R+,Q+}
    program tablice3;
 
    const
      n = 10;  {Rozmiar wektora}
    
    type
      indeks = 1..n;
      wektor = array[indeks] of integer; {Typ wektorów}
    
    var
      x       : wektor;
      i       : indeks;
      a, b, c : wektor;  {a i b wektory zadane, c wektor wynikowy}
    
    procedure wczytaj_wektor_na_x;
    begin
      for i:=1 to n do
       begin
        writeln('Wczytuję wektor, podaj ', i, ' element (z ', n, '): ');
	readln(x[i]);
       end;
    end;
    
    procedure dodaj_a_do_b_i_zapisz_wynik_na_c;
    begin
      for i:=1 to n do
        c[i] := a[i] + b[i];
    end;
    
    procedure wypisz_c;
    begin
      for i:=1 to n do
        Writeln('Wynik[', i, ']  =  ', c[i]);
    end;
    
    begin
     {Wczytanie pierwszego wektora}
      wczytaj_wektor_na_x;
      a := x;
    
     {Wczytanie drugiego wektora}
      wczytaj_wektor_na_x;
      b := x;
    
     {Obliczenia}
      dodaj_a_do_b_i_zapisz_wynik_na_c;
    
     {Wypisanie wyniku}
      wypisz_c;
    
    end.

Zauważmy, że wprawdzie nasz program się wydłużył, ale stał  się
za to czytelniejszy.

Parametry
---------

Korzystanie  z  procedur  można znacznie  uprościć  używając  z
parametrów. Parametry służą do komunikacji treści procedury  ze
środowiskiem,  w  którym ją wywołano. Mogą  służyć  zarówno  do
przekazywania informacji z procedury jak i do procedury. Z tego
powodu w Pascalu mamy dwa rodzaje parametrów:
 - parametry przekazywane przez wartość (inf. przekazywane do
   procedury),
 - parametry przekazywane przez zmienną (inf. przekazywane  z
   procedury i do procedury).
W niektórych implementacjach Pasala (np. Free Pascal, Delphi),
wprowadzono  jeszcze dodatkowe  tryby  przekazywania parametrów,
lecz (poza otwartymi tablicami) nie są  one  warte uwagi.

Popatrzmy    na    procedury   z    naszego    przykładu.    We
wczytaj_wektor_na_x chcemy przypisać na wektor dane wczytane  z
wejścia,  chcemy więc by procedura przekazała jakieś informacje
(tu: wektor) do środowiska w jakim ją wywołano. Zastosujemy  tu
przekazywanie przez zmienną.

W wypisz_wektor_c jest inaczej, procedura pobiera pewną wartość
ze środowiska, w którym ją wywołaliśmy (tu: wektor) i coś z nią
robi  (wypisuje).  W  tym  przypadku zastosujemy  przekazywanie
przez wartość.

Wreszcie procedura dodaj_a_do_b_i_zapisz_wynik_na_c pobiera  ze
środowiska wartości wektorów a i b, zaś jako wynik daje  wektor
c. Tu a i b przekażemy przez wartość, zaś c przez zmienną.

Składnia:  parametry  procedury  (wraz  z  nazwami  ich  typów)
piszemy   w   nagłówku  procedury  (po  słowie   procedure,   w
nawiasach).  Oddzielamy  je od siebie  średnikami,  nazwę  typu
piszemy   po  dwukropku.  Jeśli  parametr  przekazujemy   przez
wartość,  to nic już nie musimy pisać. Jeśli przez zmienną,  to
piszemy jeszcze var przed nazwą parametru.

Semantyka: argumenty przekazywane przez wartość są kopiowane na
pomocnicze  zmienne  lokalne procedury, która  działa  na  tych
kopiach.  Jeśli  natomiast  argument  jest  przekazywany  przez
zmienną, to procedura działa bezpośrednio na tym argumencie.

Stosowanie: Jeśli celem działania procedury/funkcji jest zmiana
wartości  argumentu,  to  parametr  trzeba  przekazywać   przez
zmienną.  W  przeciwnym przypadku parametr  przekazujemy  przez
wartość.

Spójrzmy jeszcze raz na nasz przykład:

    {$R+,Q+}
    program tablice4;
    
    const
      n = 10;  {Rozmiar wektora}
    
    type
      indeks = 1..n;
      wektor = array[indeks] of integer; {Typ wektorów}
    
    var
      a,  b,  c  :  wektor;  {a i b wektory zadane, c wektor wynikowy}
      i       : indeks;
    
    procedure wczytaj_wektor(var x: wektor);
    begin
      for i:=1 to n do
       begin
        writeln('Wczytuję wektor, podaj ', i, ' element (z ', n, '): ');
	readln(x[i]);
       end;
    end;
    
    procedure dodaj(a,b: wektor; var c: wektor);
    begin
      for i:=1 to n do
        c[i] := a[i] + b[i];
    end;
    
    procedure wypisz(x: wektor);
    begin
      for i:=1 to n do
        Writeln('Wynik[', i, ']  =  ', c[i]);
    end;
      
    begin
    
     {Wczytanie pierwszego wektora}
      wczytaj_wektor(a);
    
     {Wczytanie drugiego wektora}
      wczytaj_wektor(b);
    
     {Obliczenia}
      dodaj(a,b,c);
    
     {Wypisanie wyniku}
      wypisz(c);

    end.

Uwaga, zapis:

   procedure dodaj(a,b: wektor; var c: wektor);
   
jest skrótem zapisu:

   procedure dodaj(a: wektor; b: wektor; var c: wektor);

Dodatkowe uwagi:

 - Kolejność parametrów na liście jest bardzo ważna: pierwszy
   parametr odpowiada pierwszemu argumentowi z wywołania,  drugi
   drugiemu itd. (Czasem zamiast nazw parametr/argument używa się
   określeń parametr formalny/parametr aktualny).
 - Argumentem odpowiadającym parametrowi przekazywanemu przez
   wartość  może  być  dowolne wyrażenie typu zgodnego  z  typem
   parametru.
 - Argumentem odpowiadającym parametrowi przekazywanemu przez
   zmienną może być tylko coś, co może wystąpić po lewej stronie
   instrukcji przypisania, i znów typ argumentu musi być zgodny z
   typem parametru.

(Na potrzeby tego wykładu przyjmiemy, że zgodność typów
oznacza, że typy muszą być identyczne.)
Oto przykład:

   var 
     i: integer;

   procedure Test1(var x: integer); 
     begin  
       x := 3;
     end; {Test1}

   begin
     Test1(i); {OK}
     Test1(7); {Źle, 7 nie może wystąpić po lewej stronie przypisania}
   end.

I jeszcze jeden przykład: procedura zamieniająca wartości
argumentów:

   procedure zamień_zła(x,y: integer);
    var
     pom: integer;
    begin
     pom := x;
     x   := y;
     y   := pom;
    end; {zamień_zła}

Co trzeba w niej zmienić?

Zmienne lokalne
---------------

Wewnątrz  procedur  można  deklarować  zmienne  lokalne,  czyli
takie, które są tworzone w momencie wywołania procedury i  giną
wraz z zakończeniem jej działania. Tak jak wszystkie zmienne  w
Pascalu mają one na początku nieokreśloną wartość (więc  trzeba
je  inicjalizować). Zmienne lokalne deklarujemy  dokładnie  tak
samo  jak  zmienne  z  programu  głównego  (nazywane  zmiennymi
globalnymi).  Zmienne lokalne są wygodne, gdyż pozwalają  ukryć
wewnątrz  procedur  te  zmienne,  które  są  potrzebne  jedynie
lokalnie, na potrzeby danego wywołania procedury.

W  naszym przykładzie taką zmienną jest zmienna i: nie  używamy
jej  nigdzie w programie głównym, a jedynie w procedurach.  Oto
uwzględniająca tę zmianę wersja naszego przykładu:

   {$R+,Q+}
   program tablice5;
   
   const
     n = 10;  {Rozmiar wektora}
   
   type
     indeks = 1..n;
     wektor = array[indeks] of integer; {Typ wektorów}
   
   var
     a, b, c : wektor;  {a i b wektory zadane, c wektor wynikowy}

   procedure wczytaj_wektor(var x: wektor);
    var
     i       : indeks;
   begin
     for i:=1 to n do
      begin
       writeln('Wczytuję wektor, podaj ', i, ' element (z ', n, '): ');
       readln(x[i]);
      end;
   end;
   
   procedure dodaj(a,b: wektor; var c: wektor);
    var
     i       : indeks;
   begin
     for i:=1 to n do
       c[i] := a[i] + b[i];
   end;
   
   procedure wypisz(x: wektor);
    var
     i       : indeks;
   begin
     for i:=1 to n do
        Writeln('Wynik[', i, ']  =  ', x[i]);
   end;
    
   begin
   
    {Wczytanie pierwszego wektora}
     wczytaj_wektor(a);
   
    {Wczytanie drugiego wektora}
     wczytaj_wektor(b);
   
    {Obliczenia}
     dodaj(a,b,c);
   
    {Wypisanie wyniku}
     wypisz(c);
   
   end.
   
Wprawdzie program się wydłużył, ale oczyściliśmy program główny
ze  zbędnej tam zmiennej i, rozpraszając ewentualne wątpliwości
czytelnika tego programu co do roli tej zmiennej - teraz widać,
że jej znaczenie jest tylko lokalne.

Mając  zmienne  lokalne, możemy inaczej spojrzeć  na  parametry
przekazywane  przez wartość: są to po prostu  zmienne  lokalne,
tyle że inicjalizowane (wartością argumentu).

Deklaracje lokalne

W Pascalu można deklarować:
 - stałe,
 - typy,
 - zmienne,
 - procedury i funkcje.
(I  nic  innego!) Kolejność jest dowolna, o ile  zawsze  każda
rzecz  jest najpierw (tekstowo) zadeklarowana, a dopiero  potem
używana.  Najsensowniejsza kolejność deklaracji jest  taka  jak
podano powyżej (standard Pascala wymaga dokładnie takiej 
kolejności).

W  procedurze  można deklarować wszystko to co  i  w  programie
głównym  (a  więc  także można deklarować  lokalne  procedury).
Zatem pełna składnia procedury jest następująca:

   procedure   <nazwa>  (  <lista  parametrów>  ); 
     <deklaracje lokalne> 
   begin
     <ciąg instrukcji> 
   end;

Przesłanianie nazw,  zasięg lokalny i globalny
----------------------------------------------

Zwróćmy uwagę, że 
w   procedurach  wczytaj_wektor  i  wypisz_wektor   użyto   dla
parametru  nowej nazwy (x), zaś w procedurze dodaj  użyto  nazw
zmiennych  występujących w programie głównym. Jest  to  możliwe
dlatego,  że każda procedura wprowadza nowy zasięg widoczności.
Jeśli występują w niej nazwy występujące także w jej otoczeniu,
to  nazwy  lokalne  przesłonią takie same  nazwy  z  otoczenia.
Oznacza  to,  że  w  procedurze  wczytaj_wektor  można   używać
zmiennej  globalnej  a,  ale w procedurze  dodaj  już  nie  (bo
przesłoniła ją nazwa parametru a).

Podsumowując:
 - z  programem  głównym  i z każdą procedurą  związany  jest
   zasięg widoczności nazw,
 - w  jednym  zasięgu nie może być dwu rzeczy o takiej  samej
   nazwie,
 - w  różnych zasięgach mogą być różne rzeczy o takich samych
   nazwach,
 - jeśli  jeden zasięg jest zanurzony w innym, to w nim  jego
   nazwy  przesłaniają  takie same nazwy z otaczającego  zasięgu
   widoczności.

Oto przykłady:
1.
   const x = 5;
   var x: Integer; {Źle! x jest już zadeklarowane w tym zasięgu.}

2.
   const x = 5;
   
   procedure Proc(x: real);  {Poprawne, to x jest w innym zasięgu}
     {...}

3.
   procedure Proc(x: real);
    var x: real;  {Źle! x już jest w tym zasięgu!}


Funkcje
-------

Funkcje  są specjalną formą procedur. Często piszemy procedurę,
która ma obliczyć jedną wartość, np.

   procedure kwadrat(x: integer; var wynik: integer);
     begin
       wynik := x * x;
     end; {kwadrat}

Użycie takiej procedury jest czasem niewygodne:

   var i, pom: integer:
    {...}
    begin
      {...}
      kwadrat(i, pom);
      Writeln('Kwadratem ', i, ' jest ', pom);
    End;

Możemy w takiej sytuacji użyć funkcji:

   function kwadrat(x: integer): integer;
     begin
       kwadrat := x * x;
     end; {kwadrat}

której się dużo wygodniej używa:

   var i: integer:
    {...}
    begin
      {...}
      Writeln('Kwadratem ', i, ' jest ', kwadrat(i));
    end.

Składnia deklaracji funkcji:

   function <nazwa> ( <lista parametrów> ): <identyfikator_typu>;
     <deklaracje lokalne>
   begin
     <ciąg instrukcji>
   end;

Dodatkowe zasady deklarowania funkcji:
 - wewnątrz funkcji, przy każdym jej wywołaniu, musi nastąpić
   co najmniej jedno przypisanie na jej nazwę,
 - nie można odczytać wartości przypisanej na nazwę funkcji,
 - wartość funkcji musi być typu prostego lub String.

Uwaga (FreePascal, Delphi):

zamiast  na  nazwę funkcji, można przypisywać na predefiniowaną
(lokalnie w każdej funkcji) zmienną result. Wartości przypisane
na  tę zmienną można odczytywać. (Można też mieszać przypisania
na  nazwę  funkcji  i  użycia  zmiennej  result,  ale  jest  to
nieczytelne).

Programowanie bez procedur i funkcji  jest  bardzo
niewygodne,  nie  da  się  bez nich  sensownie  budować  dużych
programów.  Kiedy używać procedur i funkcji? Zawsze wtedy,  gdy
mamy   fragment  programu  o  jasno  określonym   zadaniu.   Co
zyskujemy?  Czytelność i łatwość modyfikacji.  Zauważmy  o  ile
bardziej  czytelna  jest  ostatnia  wersja  programu  głównego.
Zamiast  długiego ciągu instrukcji mamy kilka poleceń o  jasnym
(bo ściśle związanym z zastosowaniem) znaczeniu. Tworzymy w ten
sposób   maszynę  wirtualną.  Jednocześnie  o  wiele   prościej
analizuje się treść procedur - czytając je myślimy tylko o  ich
(jasno  określonym)  celu,  np. czytając  procedurę  wczytującą
wektory nie musimy myśleć o ich dodawaniu, czy wypisywaniu.