Parametr proceduralny
W informatyce parametr proceduralny jest parametrem procedury , która sama jest procedurą.
Ta koncepcja jest niezwykle potężnym i wszechstronnym narzędziem programistycznym , ponieważ pozwala programistom modyfikować pewne kroki procedury bibliotecznej w dowolnie skomplikowany sposób, bez konieczności rozumienia lub modyfikowania kodu tej procedury.
To narzędzie jest szczególnie skuteczne i wygodne w językach obsługujących lokalne definicje funkcji , takich jak Pascal i współczesny dialekt GNU języka C. Jest jeszcze bardziej, gdy dostępne są domknięcia funkcji . Tę samą funkcjonalność (i więcej) zapewniają obiekty w obiektowych językach programowania , ale za znacznie wyższą cenę.
Parametry proceduralne są w pewnym stopniu związane z koncepcjami funkcji pierwszej klasy i funkcji anonimowej , ale różnią się od nich. Te dwie koncepcje mają więcej wspólnego ze sposobem definiowania funkcji niż z tym, jak są używane.
Podstawowy pomysł
W większości języków, które zapewniają tę funkcję, parametr proceduralny f podprogramu P można wywołać wewnątrz ciała P , tak jakby to była zwykła procedura:
procedura P ( f ): powrót f (6,3) * f (2,1)
Wywołując podprogram P , należy podać mu jeden argument, którym musi być jakaś wcześniej zdefiniowana funkcja zgodna ze sposobem, w jaki P używa swojego parametru f . Na przykład, jeśli zdefiniujemy
procedura plus ( x , y ): powrót x + y
wtedy możemy nazwać P ( plus ), a wynikiem będzie plus (6,3) * plus (2,1) = (6 + 3)*(2 + 1) = 27. Z drugiej strony, jeśli zdefiniujemy
procedura quot ( u , v ): powrót u / v
wtedy wywołanie P ( quot ) zwróci quot (6,3)* quot (2,1) = (6/3)*(2/1) = 4. Wreszcie, jeśli zdefiniujemy
procedura zło ( z ) powrót z + 100
wówczas wywołanie P ( zły ) nie będzie miało większego sensu i może zostać oznaczone jako błąd.
Szczegóły składniowe
Niektóre języki programowania, które mają tę cechę, mogą zezwalać lub wymagać pełnej deklaracji typu dla każdego parametru proceduralnego f , w tym liczby i typu jego argumentów oraz typu wyniku, jeśli taki istnieje. Na przykład w języku programowania C powyższy przykład można zapisać jako
int P ( int ( * fa ) ( int a , int b )) { return fa ( 6 , 3 ) * fa ( 2 , 1 ); }
W zasadzie rzeczywista funkcja actf , która jest przekazywana jako argument podczas wywoływania P , musi być zgodna typu z zadeklarowanym typem parametru procedury f . Zwykle oznacza to, że actf i f muszą zwracać ten sam typ wyniku, muszą mieć taką samą liczbę argumentów, a odpowiednie argumenty muszą być tego samego typu. Nazwy argumentów nie muszą być jednak takie same, na co wskazują znaki plus i quot przykłady powyżej. Jednak niektóre języki programowania mogą być bardziej restrykcyjne lub bardziej liberalne w tym zakresie.
Określanie zakresu
W językach, które dopuszczają parametry proceduralne, reguły określania zakresu są zwykle definiowane w taki sposób, że parametry proceduralne są wykonywane w ich natywnym zakresie. Dokładniej, załóżmy, że funkcja actf jest przekazywana jako argument do P , jako jej parametr proceduralny f ; i f jest następnie wywoływana z wnętrza ciała P . Podczas wykonywania actf widzi środowisko swojej definicji. [ potrzebny przykład ]
Wdrożenie tych zasad określania zakresu nie jest trywialne. Do czasu ostatecznego wykonania actf rejestry aktywacji , w których znajdują się zmienne środowiskowe, mogą znajdować się dowolnie głęboko w stosie. Jest to tak zwany problem funargu skierowanego w dół .
Przykład: ogólne sortowanie przez wstawianie
Pojęcie parametru proceduralnego najlepiej wyjaśnią przykłady. Typowym zastosowaniem jest następująca ogólna implementacja sortowania przez wstawianie , który przyjmuje dwa parametry całkowite a , b i dwa parametry proceduralne prec , swap :
procedura isort ( a , b , prec , swap ): liczba całkowita i , j ; ja ← za ; podczas gdy ja ≤ b zrobić j ← ja ; podczas gdy j > a i prec ( j , j −1) zamień miejscami ( j , j −1); jot ← jot -1; ja ← ja +1;
Tej procedury można użyć do sortowania elementów od x [ a ] do x [ b ] jakiejś tablicy x dowolnego typu w kolejności określonej przez użytkownika. Parametry prec i swap powinny być dwiema funkcjami zdefiniowanymi przez klienta, obie przyjmujące dwie liczby całkowite r , s z przedziału a i b . Funkcja prec powinna zwracać wartość true wtedy i tylko wtedy, gdy dane przechowywane w x [ r ] powinno poprzedzać dane przechowywane w x [ s ], w kolejności określonej przez klienta. Funkcja zamiany powinna wymieniać zawartość x [ r ] i x [ s ] i nie zwracać żadnego wyniku.
Dzięki odpowiedniemu doborowi funkcji prec i swap ta sama procedura isort może być wykorzystana do zmiany kolejności tablic dowolnego typu, przechowywanych na dowolnym nośniku i zorganizowanych w dowolną strukturę danych, która zapewnia indeksowany dostęp do poszczególnych elementów tablicy. (Należy jednak pamiętać, że istnieją algorytmy sortowania , które są znacznie bardziej wydajne niż sortowanie przez wstawianie w przypadku dużych tablic).
Sortowanie liczb zmiennoprzecinkowych
Na przykład, możemy posortować tablicę z 20 liczb zmiennoprzecinkowych, od z [1] do z [20] w porządku rosnącym, wywołując isort (1, 20, zprec , zswap ), gdzie funkcje zprec i zswap są zdefiniowane jako
procedura zprec ( r , s ): powrót ( z [ r ] < z [ s ]); procedura zswap ( r , s ): float t ; t ← z [ r ]; z [ r ] ← z [ s ]; z [ s ] ← t
Sortowanie wierszy macierzy
Dla innego przykładu, niech M będzie macierzą liczb całkowitych z 10 wierszami i 20 kolumnami, z indeksami zaczynającymi się od 1. Poniższy kod przestawi elementy w każdym wierszu, tak aby wszystkie parzyste wartości były przed wszystkimi nieparzystymi:
liczba całkowita i procedura eoprec ( r , s ): return ( M [ i , r ] mod 2) < ( M [ i , s ] mod 2); procedura eoswap ( r , s ): liczba całkowita t ; t ← M [ ja , r ]; M [ ja , r ] ← M [ ja , s ]; M [ ja , s ] ← t ; dla i od 1 do 10 do isort (1, 20, eoprec, eoswap);
Zauważ, że efekty eoprec i eoswap zależą od numeru wiersza i , ale procedura isort nie musi o tym wiedzieć.
Procedura sortowania wektorów
Poniższy przykład używa isort do zdefiniowania procedury vecsort , która pobiera liczbę całkowitą n i wektor liczb całkowitych v z elementami od v [0] do v [ n −1] i sortuje je w kolejności rosnącej lub malejącej, w zależności od tego, czy trzeci parametr incr jest odpowiednio prawdą lub fałszem :
procedura vecsort ( n , v , incr ): procedura vprec ( r , s ): jeśli incr to zwróć v [ r ] < v [ s ]; w przeciwnym razie zwróć v [ r ] > v [ s ]; procedura vswap ( r , s ): liczba całkowita t ; t ← v [ r ]; v [ r ] ← v [ s ]; v [ s ] ← t issort (0, n −1, vprec , vswap );
Zwróć uwagę na użycie definicji funkcji zagnieżdżonych w celu uzyskania funkcji vprec , której efekt zależy od parametru incr przekazanego do vecsort . W językach, które nie pozwalają na zagnieżdżone definicje funkcji, takich jak standard C, uzyskanie tego efektu wymagałoby raczej skomplikowanego i/lub niebezpiecznego dla wątków kodu.
Przykład: połączenie dwóch sekwencji
Poniższy przykład ilustruje użycie parametrów proceduralnych do przetwarzania abstrakcyjnych struktur danych niezależnie od ich konkretnej implementacji. Problem polega na połączeniu dwóch uporządkowanych ciągów rekordów w jeden posortowany ciąg, gdzie charakter rekordów i kryterium uporządkowania może wybrać klient. Poniższa implementacja zakłada tylko, że do każdego rekordu można się odwoływać za pomocą adresu pamięci i istnieje „adres zerowy” Λ, który nie jest adresem żadnego ważnego rekordu. Klient musi podać adresy A , B pierwszych rekordów w każdej sekwencji oraz funkcje prec , next i append , które zostaną opisane później.
procedura scalania ( A , B , prec , nextA , appendA , nextB , appendB ): adres ini , fin , tini ← Λ; fin ← Λ podczas gdy A ≠ Λ lub B ≠ Λ zrobić , jeśli B = Λ lub ( A ≠ Λ i B ≠ Λ i prec ( A , B )) następnie t ← następny A ( A ) koniec ← dołącz A ( A , koniec ); jeśli ini = Λ to ini ← koniec A ← t inaczej t ← następnyB ( B ) koniec ← dołączB ( B , koniec ); jeśli ini = Λ wtedy ini ← fin B ← t return ini
Funkcja prec powinna przyjąć adresy r , s dwóch rekordów, po jednym z każdej sekwencji, i zwrócić prawdę , jeśli pierwszy rekord powinien wystąpić przed drugim w sekwencji wyjściowej. Funkcja nextA powinna pobrać adres rekordu z pierwszej sekwencji i zwrócić adres następnego rekordu w tej samej sekwencji lub Λ, jeśli go nie ma. Funkcja appendA powinna dołączyć pierwszy rekord z sekwencji A do sekwencji wyjściowej; jego argumentami są adresy A rekordu, który ma zostać dołączony, oraz adres fin ostatniego rekordu listy wyjściowej (lub Λ, jeśli ta lista jest jeszcze pusta). Procedura appendA powinna zwrócić zaktualizowany adres ostatniego elementu listy wyjściowej. Procedury nextB i appendB są analogiczne dla drugiej sekwencji wejściowej.
Scalanie połączonych list
Aby zilustrować użycie ogólnej procedury łączenia, oto kod służący do łączenia dwóch prostych połączonych list , zaczynając od węzłów pod adresami R , S . Tutaj zakładamy, że każdy rekord x zawiera pole liczb całkowitych x . INFO i pole adresowe x . NEXT , który wskazuje następny węzeł; gdzie informacyjne są w porządku rosnącym na każdej liście. Listy wejściowe są demontowane przez scalanie, a ich węzły są używane do budowania listy wyjściowej.
procedura listmerge ( R , S ): procedura prec ( r , s ): powrót r . INFORMACJE < s . Procedura INFO następna ( x ): powrót x . NASTĘPNA procedura dodaj ( x , fin ) jeśli fin ≠ Λ to fin . DALEJ ← x x . NEXT ← Λ return x return scalanie ( R , S , prec , next , append , next , append )
Scalanie wektorów
Poniższy kod ilustruje niezależność ogólnej procedury scalania od rzeczywistej reprezentacji sekwencji. Łączy elementy dwóch zwykłych tablic U [0] do U [ m −1] i od V [0] do V [ n −1] liczb zmiennoprzecinkowych w kolejności malejącej. Tablice wejściowe nie są modyfikowane, a połączona sekwencja wartości jest przechowywana w trzecim wektorze W [0] do W [ m + n −1]. Podobnie jak w języku programowania C, zakładamy, że wyrażenie „& V ” daje adres zmiennej V , „* p ” zwraca zmienną, której adresem jest wartość p , a „&( X [ i ])” to odpowiednik "&( X [0]) + i " dla dowolnej tablicy X i dowolnej liczby całkowitej i .
procedura arraymerge ( U , m , V , n , W ): procedura prec ( r , s ): powrót (* r ) > (* s ) procedura nextU ( x ): if x = &( U [ m −1]) następnie zwróć Λ, w przeciwnym razie zwróć x + 1 procedura nextV ( x ): jeśli x = &( V [ n −1]) to zwróć Λ w przeciwnym razie zwróć x + 1 procedura dołącz ( x , fin ) jeśli fin = Λ to fin ← &( W [0]) ( * płetwa ) ← (* x ) powrót płetwa + 1 jeśli m = 0 to U ← Λ jeśli n = 0 to V ← Λ powrót scalanie ( U , V , prec , nextU , append , nextV , append )
Przykład: Całka oznaczona
Całkowanie po przedziale
Poniższa procedura oblicza przybliżoną całkę fa ( x ) re x danej funkcji o wartościach rzeczywistych f w danym przedziale [ a , b ] z prawdziwa linia . Stosowaną metodą numeryczną jest reguła trapezu z zadaną liczbą n kroków; liczby rzeczywiste są aproksymowane liczbami zmiennoprzecinkowymi.
0 procedura Intg ( f , a , b , n ): float t , x , s ; liczba całkowita i jeśli b = a to zwróć x ← a ; s ← fa ( za ) / 2; dla i od 1 do n −1 do t ← ja / ( n +1); x ← (1− t ) * za + t * b ; s ← s + fa ( x ) s ← fa ( b ) / 2 powrót ( b - za ) * s / n
Całkowanie na dysku
problem całkowania danej funkcji argumentami na dysku z danym środkiem ( ) i danym promieniem . Ten problem można sprowadzić do dwóch zagnieżdżonych całek z pojedynczą zmienną poprzez zmianę zmiennych
Poniższy kod implementuje formułę prawostronną :
procedura DiskIntg ( g , xc , yc , R , n ) procedura gring ( z ): procedura gpolar ( t ): float x , y x ← xc + z * cos ( t ) y ← yc + z * sin ( t ) return g ( x , y ) liczba całkowita m ← zaokrąglenie ( n * z / R ) return z * Intg ( gpolar , 0, 2*π, m ) return Intg ( gring , 0, R , n )
Ten kod wykorzystuje procedurę integracji Intg na dwóch poziomach. Poziom ) Intg całki dla . The inner level (next-to-last line) defines as being the sol okręgu o środku i promieniu ( .
Historia
Parametry proceduralne zostały wynalezione przed erą komputerów elektronicznych przez matematyka Alonzo Churcha jako część jego modelu obliczeniowego rachunku lambda .
Parametry proceduralne jako cecha języka programowania zostały wprowadzone przez ALGOL 60 . W rzeczywistości ALGOL 60 miał potężny mechanizm przekazywania parametrów „ wywołania według nazwy ”, który mógł uprościć niektóre zastosowania parametrów proceduralnych; zobacz Urządzenie Jensena .
Parametry proceduralne były istotną cechą języka programowania LISP , który wprowadził również koncepcję domknięcia funkcji lub funarg . Język programowania C umożliwia przekazywanie wskaźników funkcji jako parametrów, które osiągają ten sam cel i są często używane jako wywołania zwrotne w programowaniu sterowanym zdarzeniami i jako procedury obsługi błędów. Jednak tylko kilka nowoczesnych kompilatorów C pozwala na zagnieżdżone definicje funkcji, więc inne zastosowania są stosunkowo rzadkie. Parametry proceduralne zostały podane również w Pascalu wraz z zagnieżdżonymi definicjami procedur; jednakże, ponieważ standardowy Pascal nie pozwalał na oddzielną kompilację, funkcja ta była również rzadko używana w tym języku.