« poprzedni punkt 

4. Funkcje

Idea podziału programu na fragmenty, które mogą być opracowywane odrębnie od siebie, do kodu włączone jednokrotnie a wywoływane wielokrotnie jest zrealizowana (w taki czy inny sposób) chyba we wszystkich językach programowania.
Takie wyodrębnione fragmenty nazywa się podprogramami, procedurami lub funkcjami.
Różnice pojęciowe są tu raczej rozmyte. Niekiedy mówi się ogólnie o procedurach lub podprogramach (procedure, subroutine), funkcjami ( function) nazywając specjalne ich przypadki, które zwracają wartości.
Języki C i C++ operują wyłącznie pojęciem funkcji, nazywając tak oba rodzaje procedur.
Teraz zastosujemy własnie tę terminologię.
Należy dodać, że w innych niż C++ językach obiektowych (w tym w Javie) odpowiednikiem pojęcia funkcji jest pojęcie metody. O różnicy między funkcjami i metodami dowiemy się przy okazji omawiania podstaw programowania obiektowego.

Funkcja - to wyodrębniony opis czynności (zestaw instrukcji) zapisany we fragmencie kodu, który może być jednokrotnie połączony (przez wstawienie albo w fazie kompilacji lub wykonania) z programem źródłowym i wielokrotnie użyty za pomocą odwołań z innych fragmentów programu

Funkcje definiujemy w programie za pomocą dostarczenia:

  • nagłówka funkcji (opisującego m.in. nazwę funkcji oraz informacje, które mogą być funkcji przekazane),
  • < b>ciała funkcji (samego kodu, opisującego czynności wykonywane przez funkcję).

Uwaga. W klasycznym REXXie występuje pojęcie procedury, a składnia jest zupełnie inna.

W różnych językach w różny sposób definiuje się nagłówki i ogranicza ciało funkcji.

Umówmy się, że w naszym uproszczonym, zmodyfikowanym REXXie nagłówki funkcji będziemy oznaczać słowem function, występującym jako pierwsze słowo w wierszu, po którym następuje nazwa funkcji i w nawiasach okrągłych lista parametrów (opis informacji, przekazywanej do funkcji) a kod funkcji będzie się zawsze kończył instrukcją return, która zwróci sterowanie do miejsca wywołania funkcji, ew. zwracając też wynik funkcji jako wartość wyrażenia podanego w instrukcji return.


        function nazwa_funkcji ( lista_parametrów )
                ins1
                ins2
                ...
                insN
                return [ wyrażenie ]

Uwagi:
  • nawiasy kwadratowe oznaczają, że wyrażenie w instrukcji return (zwrot wymiku) jest opcjonalne,
  • lista parametrów stanowi listę zmiennych, rozdzielonych przecinkami; lista może być pusta.

Cóż to jest lista parametrów?

Parametry są zmiennymi za pomocą których w ciele funkcji uzyskujemy dostęp do informacji przekazanych przy wywołaniu funkcji.

Zatem, żeby zrozumieć budowę funkcji musimy też wiedzieć w jaki sposób funkcję się wywołuje.

Wywołanie funkcji jest wyrażeniem o następującej postaci:
   
            nazwa_funkcji(lista_argumentów)

Przy czym:
  • wyrażenie to ma wartość wyniku funkcji (wyrażenia w instrukcji return), jeśli funkcja zwraca wynik (jeśli jakieś wyrażenie w return jest podane);
  • lista argumentów jest listą wyrażeń, rozdzielonych przecinkami. Wyrażenia są opracowywane, a   ich wartości przekazywane funkcji i dostępne wewnętrz funkcji jako wartości parametrów znajdujących się na pozycjach odpowiadających pozycjom argumentów;
  • nawias otwierający listę argumentów musi następować zaraz po nazwie funkcji (bez rozdzielającej spacji). Uwaga. Konieczność spełnienia tego wymagania zależy od języka programowania.

Np. możemy zdefiniować funkcję, której zadaniem będzie porównanie dwóch wartości. Każde wywołanie tej funkcji będzie jej przekazywać dwa argumenty - wartości do porównania. Będą one dostępne w funkcji poprzez nazwy odpowiednich parametrów. Po porównaniu funkcja zwróci wynik do miejsca wywołania (0 - wartości rózne, 1, - pierwszy argument większy od drugiego, -1 - pierwszy mniejszy od drugiego).

Funkcja Zobaczmy.
Nazwa funkcji: compare
Parametry: val1, val2
Wywołana najpierw z argumentami a i b (ich wartości są przekazywane funkcji i wchodzą "na miejsce" parametrów val1 i val2).
Kod funkcji stwierdza, że val1 (czyli wartość a) jest mniejsze od val2 (czyli wartości b) i stosownie do tego ustala wartość zmiennej r. Wartość ta jest zwracana do miejsca wywołania przez instrukcję return, a instrukcja przypisania podstawia ją na zmienną wynik.
Zauważmy dalej, że  funkcje tę:

  • możemy z kodu programu wywoływać wielokrotnie (ostatnia instrukcja na rysunku - drugie wywołanie)
  • jako argumenty możemy podawać dowolne wyrażenia (np. y+1 lub wartość zwróconą przez inną funkcję)


W innych językach będzie inaczej: np. w C  program składa się wyłącznie z definicji funkcji (oraz ew. z deklaracji tzw zmiennych globalnych), wśród których jedna - o nazwie main - jest wyróżniona i stanowi odpowiednik naszego "głównego programu". Java stanowi nieco inny przypadek, bowiem jest językiem obiektowym i operuje wyłącznie klasami.

Definiując funkcje musimy wiedzieć nie tylko jak sformułować jej definicję, ale również w jaki sposób umieścić ją w strukturze całego programu. I znowu, różne języki stosują tutaj różne reguły.

W naszym uproszczonym REXXie będziemy rozróżniać główny program i funkcje.
Główny program zaczynać się będzie w pierwszym wierszu pliku źródłowego i musi kończyć się instrukcją exit (koniec wykonania programu). Dopiero potem mogą następować definicje funkcji.

Zobaczmy przykład. Program korzystający ze zdefiniowanej funkcji compare i porównujący wartości wprowadzane przez użytkownika z konsoli. Przy okazji na tym przykładzie warto zaobserwować, że funkcja może być wywoływana z innej funkcji.

/* Test funkcji  */

do forever
  v1 = linein();
  if (v1 = '') then exit;
  v2 = linein();
  if (v2 = '') then exit;
  showComparison( v1, v2);
end
exit


function compare ( val1, val2 )

   if ( val1 > val2 ) then return 1;
   else if (val1 < val2) then return -1;
   else return 0;


function showComparison ( val1, val2)

   msg[1] = "większa od";
   msg[0] = "rowna";
   i = -1;
   msg[i] = "mniejsza od";

   r = compare( val1, val2 );
   say "Wartość " val1 " jest " msg[r] "wartości " val2
   return

Pętla do forever (słowo kluczowe języka) wykonywana jest w nieskończoność. W petli tej użytkownik wprowadza z konsoli po kolei dwie wartości (liczby lub napisy!). Wciśnięcie "pustego" ENTER przerywa działanie pętli (bowiem kończy program - instrukcja exit). Po wprowadzeniu wartości wołana jest funkcja showComparison, która sama z kolei woła funkcję compare i wyprowadza wyniki porównania. Wyprowadzane napisy ("mniejsze od", "równe", "większe od") umieściliśmy w tablicy, której indeksy odpowiadają wynikom porównania zwracanym przez funkcję compare (w ten sposób unikamy instrukcji if, które - jeśli są rozbudowane - stanowią częste źródło błędów logicznych w programie). Na marginesie: REXX dopuszcza dowolne wartości jako indeksy tablicy, ale wartości, które nie są nieujemnymi liczbami całkowitymi powinny być podawane jako zmienne, dlatego użyliśmy zmiennej i z nadaną wartością -1 do indeksowania elementu-napisu "mniejsze od".

Zwróćmy też uwagę na to, że funkcja showComparison nie zwraca żadnych wartości. Kończy jej działanie instrukcja return bez podanego wyrażenia "do zwrotu".

Proszę zapisać i uruchomić omawiany wyżej program na własnym komputerze.

Funkcje nie muszą mieć parametrów.

Jeżeli funkcja nie ma żadnych parametrów to w jej definicji  i tak trzeba podać po nazwie nawiasy okrągłe ().
Przy wywołaniu funkcji bez podawania jakichkolwiek argumentów - po nazwie funkcji również podajemy nawiasy okrągłe ()

Możemy teraz stwierdzić, że w wykorzystywanych przez nas wcześniej funkcjach linein(), time(), right() nie ma nic tajemniczego. Po prostu, niektóre użyteczne funkcje już zostały napisane i są udostępnione jako tzw. funkcje wbudowane (wbudowane w REXX). W innych językach mamy tzw. biblioteki funkcji standardowych, które też już są gotowe do wykorzystania. Pakiety Javy (choć mają nieco inne znaczenie) możemy kojarzyć pod względem funkcjonalności z bibliotekami funkcji standardowych.

Funkcje wbudowane lub standardowe - to skompilowane, gotowe do wykorzystania w programach funkcje, spełniające wiele użytecznych zadań np. przetwarzanie łańcuchów znakowych, wejście-wyjście, funkcje matematyczne

Przez definicję zmiennej w REXXie będziemy rozumieli pierwsze jej użycie (np. przypisanie) jej wartości). W innych językach rozróżnia się deklarację i definicję zmiennej; o deklaracjach będziemy mówić ucząc się Javy

Tworząc i używając funkcji musimy zdawać sobie sprawę z dwóch ważnych problemów:
  • po pierwsze, jakie są relacje pomiędzy zmiennymi definiowanymi w danej funkcji, a zmiennymi występującymi w programie poza tą funkcją (w "programie glównym", w innych funkcjach),
  • po drugie, w jaki sposób przekazywane są funkcji argumenty.


W większości języków programowania zmienne definiowane w kodzie funkcji są tak zwanymi zmiennymi lokalnymi.

W naszym uproszczonym REXXie mamy do czynienia tylko z lokalnymi blokami wprowadzanymi przez definicję funkcji. W innych językach możemy mieć zagnieżdżone bloki lokalne wprowadzane przez instrukcję grupującą, np. w C czy Javie - nawiasy klamrowe



Ciało funkcji  jest szczególnym przypadkiem tzw. bloku lokalnego. Istotnie, definicja funkcji grupuje kod (w REXXie między nagłowkiem funkcji  a słowem return włącznie)  - stąd nazwa "blok", natomiast słowo "lokalny" wiąże się z właściwościami tego bloku, m.in. z tym, że każda zmienna zdefiniowana w tym bloku ma charakter lokalny) czyli:


Zmienna lokalna jest zmienną zdefiniowaną w bloku lokalnym, widzianą (przez kompilator czy interpreter) i istniejącą tylko w tym bloku, od początku definicji zmiennej do końca bloku

Zobaczmy przykład:

a = 3;
b = 5;
func1();
func2();
say "main a =" a "b =" b;
exit;

function func1()
  a = 7;
  b = 10;
  c = 12;
  func2();
  say "func1 a =" a "b = " b "c = " c;
  return;

function func2()
  a = 100;
  b = 101;
  c = 101;
  say "func2 a =" a "b = " b "c = " c;
  return;

Wydruk z programu
func2 a = 100 b =  101 c =  101
func1 a = 7 b =  10 c =  12
func2 a = 100 b =  101 c =  101
main a = 3 b = 5


Ważne obserwacje:
  • mimo tych samych nazw zmienne a i b w funkcji func1 są innymi zmiennymi niż a i b w funkcji func2 oraz a i b w programie głównym
  • zmienna c jest znana w func1 i jest znana w func2 (ale tu mimo tej samej nazwy jest to inna zmienna), natomiast nie możemy odwołać się do c w programie głównym, bo tu ta zmienna jest nieznana (ściślej - jej wartość będzie nieokreślona)

Widać tu wyraźnie,  że zmienne a, b, c są lokalne w funkcjach func1 i func2 (a zatem za każdym razem istnieją tylko w ramach bloku danej funkcji). I co potwierdza  wydruk programu każda funkcja operuje na swoich lokalnych zmiennych a, b, c. Dlatego - mimo wywołania func2 z func1 - wartości zmiennych a, b, c na końcu func1 równe są 7, 10, 12 (a nie 100, 101,101). Tak samo a i b na końcu działania programu (już po wywołaniu func1) mają wartości nadane w dwóch pierwszych instrukcjach programu głównego.

Czasami jednak - oprócz dostępu do informacji przekazanej jako argumenty wywołania - funkcja winna mieć dostęp do zmiennych zdefiniowanych poza jej kodem.

W języku C podobny efekt uzyskuje się za pomocą tzw. zmiennych globalnych, deklarowanych poza ciałem jakiejkolwiek funkcji. W Javie pewnym odpowiednikiem zmiennych globalnych są pola klasy.

W REXXie mechanizm "dzielenia" zmiennych przez główny program i funkcje jest bardzo elastyczny. Dla każdej funkcji - indywidualnie - możemy w jej nagłówku podać jakie zmienne (w tym tablice) definiowane poza funkcją będą dostępne w jej ciele. Służy temu klauzula expose.
 

            function (lista_parametrów) expose lista_zmiennych

    gdzie:
  • lista_zmiennych  - lista nazw zmiennych (rozdzielonych spacjami)  zdefiniowanych poza funkcją, a do których funkcja będzie miała dostęp,
  • Uwaga. Zmienne oznaczające tablice zapisujemy podając bezpośrednio po nazwie zmiennej nawiasy klamrowe [ ]

Drugi, istotny, wspomniany wcześniej, problem dotyczy sposobu przekazywania argumentów przy wywołaniu funkcji.

Generalnie możemy mieć dwa sposoby przekazywania argumentów: przez wartość i przez adres.

Mówimy, że argument przekazywany jest przez wartość, jeśli przy wywołaniu funkcji wartość argumentu kopiowana jest do zmiennej-parametru występującej w definicji funkcji na liście parametrów

Ma to poważne konsekwencje. Otóż parametry funkcji są zmiennymi lokalnymi. Zatem - przy przekazywaniu argumentów przez wartość - w funkcji mamy dostęp do wartości przekazanego argumentu (poprzez zmienną-parametr), ale zmiany tej wartości będą dotyczyć tylko zmiennej-parametru, a nie zmiennej - przekazanego argumentu.

Zobaczmy:

a = 3;
tryChange(a);
say a;
exit;

function tryChange(a)
   a = a + 1;
   return;

Funkcja otrzymuje - jako parametr a - wartość zmiennej a zdefiniowanej w pierwszym wierszu.
Ale mamy tu przekazanie przez wartość, zatem wartość zmiennej a (3) jest kopiowana do zmiennej lokalnej - parametru a (przypadkowo nazywa się tak samo, ale jak już wiemy jest całkiem inną zmienną). Zatem wszystko co robimy w funkcji ze zmienną a ma lokalny charakter i nie dotyczy zmiennej a z programu głównego.

Modyfikacje argumentów przekazanych przez wartość są nieskuteczne


Ze względu na implementację tablic w C, C++ i Javie elementy przekazanych funkcji tablic mogą być modyfikowane (ale nie oznacza to, że argumenty są przekazywane przez adres). W naszym uproszczonym REXXie nie możemy przekazywac tablic jako argumentów, zamiast tego - dla udostępniania tablic funkcjom -  będziemy stosować klauzulę expose

W niektórych językach programowania możliwe jest przekazywania argumentów przez adres (wtedy funkcja otrzymuje nie wartość, ale adres argumentu). Mając adres możemy oczywiście nie tylko pobrać dane spod tego adresu, ale i zapisać tam jakąś nową wartość. Ten sposób przekazywania argumentów nie jest jednak możliwy w Javie ani też w naszym uproszczonym REXXie, zatem pozostawimy go na boku.


Po tej dawce dość skomplikowanych rozważań warto spojrzeć na omawiane zagadnienie bardziej ogólnie. 
Łatwo możemy sobie wyobrazić, że wprowadzenie funkcji do programu oszczędza nam pisaniny. Oto jakiś wielokrotnie powtarzany kod wyodrębniamy w funkcję i zapisujemy tylko raz, a wszędzie tam, gdzie poprzednio ten kod występował umieszczamy wywołanie tej funkcji.
Daje to nie tylko większą efektywność programowania, ale i zwiększa jego elastyczność. Przy modyfikacjach kodu odpowiedzialnego za jakieś zadanie trzeba zrobić to tylko raz - w funkcji, która go zawiera.
Ale nie tylko to sprawia, że stosowanie funkcji w programowaniu jest korzystne.
Przecież istotą tworzenia i użycia funkcji jest wyodrębnienie w ramach dużego problemu, który jest rozwiązywany przez nasz program - mniejszych podproblemów i zajęcie się każdym z nich niejako osobno, skupiając się wyłącznie na konkretnej specyfice danego zadania.
 
Wróćmy do problemu wyliczenia ceny procesora wg podanych przez użytkownika informacji.
Jak pamiętamy, zarysowując w pierwszym wykladzie różne możliwe algorytmy rozwiązania tego problemu, zwracaliśmy uwagę na potrzebę sprawdzania poprawności danych (czy wprowadzona cena jest liczbą). Uwzględnimy teraz ten warunek. W programie będziemy również chcieli dać użytkownikowi możliwość policzenia udziału w sumarycznej cenie ceny wybranego komponentu oraz zmiany ceny wybranego komponentu.
Nasz problem rozbijemy na następujące podproblemy:

  • wprowadzenie ceny i-tego komponentu komputera wraz ze sprawdzeniem jej poprawności
  • wyliczenie sumarycznej ceny komputera
  • wybór numeru komponentu z listy
  • policzenie udziału ceny komponentu
  • zmiany ceny komponentu.

Trzy pierwsze zadania zrealizujemy jako funkcje (wprowadzenie ceny komponentu - inputData(..), obliczenie ceny komputera - sumPrices(), wybór numeru komponentu z listy - choose()). Pozostałe dwa, wraz z organizacją danych i kolejności wywołań funkcji będą stanowić treść programu głównego.

Program główny będzie miał następującą postać:

/* Tablica nazw składników */

elt[0] = 6;  /* liczba skladnikow - w elemencie z indeksem 0 */
elt[] = { "Procesor", "Płyta", "Pamięć", "Dysk", "Monitor", "Inne" };

cena[0] = elt[0]  /* tyle samo cen ile skladnikow */

do i = 1 to cena[0]             /* pobieranie cen i umieszczanie w tablicy cena */
   cena[i] = inputData(elt[i]);
end

more = 1;                             /* czy powtarzać obliczenia i zmiany ? */
do while (more = 1)
   cenaOg = sumPrices();              /* obliczenie ceny komputera */
   say "Cena komputera wynosi :" cenaOg;

   nrSkl = choose("Wybierz składnik, którego udział w cenie chcesz policzyć");
   if (nrSkl \= '') then do      /* jeżeli wybrano składnik  - licz udział */
     udzial = cena[nrSkl]/cenaOg;
     say "Udzial ceny skladnika" elt[nrSkl] "wynosi: " udzial;
   end

   nrSkl = choose("Wybierz skladnik, ktorego cene chcesz zmienic");
   if (nrSkl = "") then more = 0; /* jeżeli zrezygnowano z wyboru - koniec działania*/
   else cena[nrSkl] = inputData( elt[nrSkl] );

end
exit;

A funkcje:

function inputData(nazwaSkl)
   say "Wprowadz cene dla: " nazwaSkl;
   trzebaPobracDane = 1;
   do while (trzebaPobracDane)
      cena = linein()
      if datatype(cena) = "NUM" then trzebaPobracDane = 0;
   end
   return cena;

function sumPrices() expose cena[]
  sum = 0;
  do i = 1 to cena[0]      /* umowa: cena[0] zawiera liczbę elementów tablicy cena */
    sum = sum + cena[i];
  end
  return sum;

function choose(msg) expose elt[]
  say msg;
  do i = 1 to elt[0]         /* umowa: elt[0] zawiera liczbę elementów tablicy elt */
    say i '-' elt[i];
  end
  do forever
    say "Podaj wybrany numer lub samo ENTER by zrezygnowac:"
    nr = linein();
    if nr = "" then leave;
    if (nr < 1 | nr > elt[0]) then say "Zły wybór.";
    else leave;
  end
  return nr;

Uwagi:

  1. w funkcji inputData sprawdzamy czy wprowadzona cena jest liczbą, jeśli nie - ponawiamy wprowadzania danych. Wykorzystujemy przy tym wbudowaną funkcję datatype(), która ma różne formy, a w zastosowanej tutaj (datatype(arg)) zwraca napis "NUM", jeśli argument jest liczbą,
  2. w funkcji choose zwracamy pusty łańcuch znakowy ("") gdy użytkownik zrezygnował z wyboru wciskając tylko ENTER.

 Proszę zapisać i uruchomić omawiany wyżej program na własnym komputerze.


« poprzedni punkt