Semantyka i weryfikacja programów/Ćwiczenia 4
Zawartość
Napiszemy semantykę naturalną języka wyrażeń (z ), rozważymy strategię gorliwą (jak na wcześniejszych zajęciach, w semantyce małych kroków) i leniwą. Rozważymy i statyczne i dynamiczne wiązanie identyfikatorów. Następnie rozszerzymy ten język o lambda-abstrakcję i aplikację, otrzymując prosty język funkcyjny.
Różne semantyki wyrażeń
Zadanie 1 (semantyka gorliwa)
Napisz semantykę dużych kroków dla języka wyrażeń, którego semantykę mało-krokową napisaliśmy na jednych z poprzednich ćwiczeń:
Rozwiązanie
Przypomnijmy, że zbiór stanów to
Nasze tranzycje będą postaci , gdzie . Oto reguły semantyki naturalnej.
Zwróćmy uwagę na fakt, że prawidłowe odwzorowanie zasięgu deklaracji nie predstawia w semantyce naturalnej żadnych trudności, w przeciwieństwie do semantyki małych kroków.
Zadanie 2 (semantyka leniwa)
Zmodyfikuj semantykę z poprzedniego zadania, aby uzyskać leniwą ewaluację wyrażeń, zgodnie z dyrektywą: nie obliczaj wyrażenia o ile jego wynik nie jest potrzebny (albo: obliczaj wartość wyrażenia dopiero wtedy, gdy jego wynik jest naprawdę potrzebny). Spójrzmy na przykład:
Według semantyki z poprzedniego zadania wyrażnie to nie ma wartości, bo w deklaracji jest odwołanie do niezainicjowanej zmiennej. Natomiast w semantyce leniwej wyrażenie to obliczy się do wartości , gdyż wyrażenie nie będzie wogóle obliczane. Będzie tak dlatego, że w wyrażeniu nie ma odwołań do zmiennej .
Rozwiązanie
Semantyka leniwa będzie bardzo podobna do tej z poprzedniego zadania. Zasadnicza różnica dotyczy informacji przechowywanej w stanie. Dotychczas nalażał do zbioru , gdyż podwyrażenie w obliczało sie natychmiast. Jeśli chcemy opóżnic obliczenie tego podwyrażenia, to w powinniśmy zapamiętać całe (nieobliczone) wyrażenie wraz ze stanem bieżącym. Czyli
Np. odpowiednia reguła dla wyrażenia w semantyce małych kroków mogłaby wyglądać następująco:
Czyli stan zawiera, dla każdej zmiennej, parę (wyrażenie definiujące, stan w momencie deklaracji).
Uważnego czytelnika zapewne zaniepokoił fakt, że stoi zarówno po lewej jak i po prawej stronie równania Również zapis może wzbudzić niepokój, gdyż sugeruje on, iż zawiera, jako jeden z elementów pary, obiekt tego samego typu co . Formalnego rozwiązania tego typu dylematów dostarcza teoria dziedzin. Natomiast na użytek semantyki operacyjnej wystarczy, jeśli uznamy, iż równanie stanowi skrótowy zapis następującej definicji. Zdefiniujmy następująco:
i przyjmijmy, że
Parser nie mógł rozpoznać (nieznana funkcja „\nat”): {\displaystyle \mathbf{State} = \bigcup_{n \in \nat} \mathbf{State}_{n} }
Tranzycje będą znów postaci: Podamy tylko reguły dla wystąpienia zmiennej i dla wyrażenia . Reguły dla pozostałych konstrukcji języka pozostają praktycznie bez zmian.
Zadanie 3 (semantyka dynamiczna)
Rozważmy teraz zupełnie inny mechanizm wiązania identyfikatorów, zwany wiązaniem dynamicznym. Dla odróżnienia, dotychczasowy sposób wiązania (widoczności) identyfikatorów będziemy nazywać wiązaniem statycznym. Oto przykładowe wyrażenie:
które nie ma wartości w pustym stanie początkowym, według semantyk z poprzednich zadań, ponieważ odwołanie do zmiennej w deklaracji jest niepoprawne. Tak samo jest nawet w semantyce leniwej, gdyż wartość zmiennej będzie w końcu policzona, i będzie wymagała odwołania do w stanie pustym.
Natomiast wyobrażmy sobie, że zmieniamy semantykę leniwą następująco: odwołanie do zmiennej podczas obliczania wartości będzie odnosiło się nie do stanu w momencie deklaracji , ale do stanu w momencie odwołania do . Jest to dość rewolucyjna zmiana, zapewne sprzeczna z intuicjami programisty (statyczne reguły widoczności zamieniamy na dynamiczne). W szczególności powyższe wyrażenie policzy się w semantyce dynamicznej do wartości 11, ponieważ stan w momencie odwołania do zmiennej przypisuje zmiennej wartość 10 !
Napisz semantykę naturalną dla wiązania dynamicznego.
Rozwiązanie
Teraz w stanie wystarczy przechowywać wyrażenie definiujące wartość danej zmiennej:
Nie potrzebujemy zapamiętywać stanu, w którym byliśmy w momencie deklaracji. Do obliczenia zapamiętanego wyrażenia użyjemy stanu, w którym będziemy w momencie odwołania do danej zmiennej. Znów podajemy tylko reguły dla wystąpienia zmiennej i dla wyrażenia , gdyż pozostałe reguły pozostają bez zmian
Pytanie: czy oblicza się do jakiejś wartości w stanie ?
Prosty język funkcyjny
Zadanie 1 (przekazywanie parametru przez wartość)
Rozważmy prosty język funkcyjny rozszerzający język wyrażeń z poprzednich zadań następująco:
Lambda-abstrakcja reprezentuje anonimową (nienazwaną) funkcję jednoargumentową, natomiast wyrażenie to aplikacja do (wyrażenie powinno zatem obliczać się do funkcji). Np.
Przyjmijmy statyczną widoczność identyfikatorów. Możliwe są różne mechanizmy przekazywania parametrów. Na razie wybierzmy mechanizm przekazywania przez wartość, zapewne doskonale znany Czytelnikowi: wyrażenie będące parametrem aktualnym jest obliczane przed wywołaniem funkcji, czyli w stanie, w którym jesteśmy z momencie wywołania funkcji.
Zaproponuj semantykę naturalną dla tego języka dla obydwu mechanizmów przekazywania parametrów.
Rozwiązanie
Zauważmy, że oprócz deklaracji zmiennych, również machanizm przekazywania parametru do funkcji wymaga zmiany stanu.
Tranzycje będą postaci
gdzie reprezentuje wartość, do której oblicza się program . Z tym, że wartościami będą nie tylko wartości liczbowe. Na przykład oblicza się również do pewnej wartość, która w tym przypadku powinna reprezentować jakoś funkcję iudentycznościową. Wystarczającą reprezentacją funkcji będzie trójka:
,
gdzie jest nazwą parametru formalnego, jest ciałem funkcji a jest stanem, w którym należy obliczać wartość funkcji po zaaplikowaniu do jakiegoś parametru aktualnego. Oto prosty przykład pokazujący, dlaczego powinniśmy pamiętać stan:
Funkcja zwiększa parametr aktualny o . Program oblicza się do wartości , gdyż wystąpienie zmiennej w ciele funkcji wiąże statycznie a zatem odnosi się zawsze do deklaracji , mimo tego, że w momewncie wywołania tej funkcji wartość zmiennej wynosi .
Oto jedyna reguła, jakiej będziemy potrzebować dla lambda-abstrakcji:
Nie potrafimy zrobić z funkcją nic innego jak zapamiętać informację niezbędną do obliczania jej wartości w przyszlości.
Zatem zbiór wartości bedzie następujący: a zbiór stanów określony jest następującym równaniem:
Parser nie mógł rozpoznać (błąd składni): {\displaystyle \mathbf{State} = \\mathbf{Var} \to_{\mathrm{fin}} \mathbf{Values} } . </math>
Zacznijmy od stałych, zmiennych i lambda-abstrakcji:
Parser nie mógł rozpoznać (nieznana funkcja „\n”): {\displaystyle \n, s \,\longrightarrow\, n \quad \quad x, s \,\longrightarrow\, v \quad \mbox{ o ile } s(x) = v \quad \quad \lambda x.e, s \,\longrightarrow\, \langle x, e, s \rangle }
W regule dla zmiennej, oznacza albo wartość liczbową albo funkcyjną. Pomijamy regułę dla dodawania, bo jest ona identyczna jak dla gorliwej semantyki wyrażeń w jednym z poprzednich zadań. Reguła dla będzie prawie taka sama jak dla wyrażeń, z tą różnicą, że wyrażenie definiujące wartość zmiennej może się teraz obliczać do wartości funkcyjnej, np. .
Pozostała nam już tylko reguła dla aplikacji: najpierw oblicz funkcję; następnie oblicz wartość parametru aktualnego; wreszcie przekaż ją do ciała funkcji (czyli oblicz ciało funkcji w zmodyfikowanym stanie):
Zwróćmy uwagę na wymóg, że oblicza się do wartości funkcyjnej . W szczególności np. wyrażenie jest niepoprawne. Natomiast parametr aktualny nie musi być liczbą, może być funkcją, np. w programie:
który oblicza się do wartości .
Zadanie 2 (przekazywanie parametru przez nazwę)
Zaproponyj leniwą semantykę języka z mechnizmem przekazywanie parametru przez nazwę. Mechanizm ten stanowi leniwy odpowiednik przekazywania przez wartość: nie obliczamy wyrażenia będącego parametrem aktualnym, a zamiast jego wartości przekazujemy do funkcji to wyrażenie wraz ze stanem z miejsca wywołania funkcji. To ten stan bedzie brany pod uwagę, gdy obliczana będzie wartość parametru, tzn. przy odwołaniu w ciele funkcji do parametru formalnego. Oto przykład programu:
który w stanie pustym (wszystkie zmienne nieokreślone) nie ma wartości przy przekazywaniu parametru przez wartość (bo odwołanie do zmiennej jest niepoprawne) a oblicza się do wartości jeśli wybierzemy mechanizm przekazywania przez nazwę.
Rozwiązanie
Zbiór wartości stojących po prawej stronie symbolu będzie taki sam jak w poprzednim zadaniu. Natomiast zbiór stanów taki sam jak w semantyce leniwej w jednym z poprzedniich zadań:
Parser nie mógł rozpoznać (błąd składni): {\displaystyle \mathbf{State} = \mathbf{Var} \to_{\mathrm{fin}} \mathbf{Exp} \times \\mathbf{State}. }
Podamy tylko trzy reguły: dla wystąpienie zmiennej, deklaracji i aplikacji -- wszystkie pozostałe reguły pozostają właściwie takie same jak w poprzednim zadaniu.
Podstawowa różnica w ostatnej regule w porównaniu do poprzedniego zadania to brak ewaluacji parametru aktualnego . Zwróćmy też uwagę na wyrażenie , w którym . stany, których potrzebowaliśmy dotychczas, miały zawsze postać .
Zadania domowe
Zadanie 1
Podaj przykład wyrażenia takiego, które:
- ma wartość w semantyce statycznej i dynamicznej, ale w każdej inną
- ma wartość w semantyce leniwej a nie ma w dynamicznej
- ma wartość w semantyce dynamicznej a nie ma w leniwej
Zadanie 2
W semantyce leniwej wyrażeń, jeśli jest wiele odwołań do jakiejś zmiennej, to obliczenie wartości tej zmiennej nastąpi za każdym razem od nowa. Zmoddyfikuj tę semantykę tak, aby po pierwszym odwołaniu do zmiennej, obliczona wartość tej zmiennej była umieszczana w stanie, zastępując parę (wyrażenie, stan).
Zadanie 3
Zaproponuj dynamiczne odpowiedniki obydwu statycznych semantyk dla języka funkcyjnego . Czyli zakładamy, że widoczność identyfikatorów, m.in. w ciele funkcji, jest dynamiczna. Oto przykład programu, który w semantyce statycznej oblicza się do wartości , a w dynamicznej do wartości (parametr przekazywany przez wartość):
Rozważ dwa mechanizmy przekazywania parametrów:
- przez wartość
- przez nazwę
Ten drugi mechanizm rozumiemy teraz następująco. Parametr aktualny nie jest obliczany w momencie zaaplikowania do niego funkcji, a do ciała funkcji przekazuje się wyrażenie będące parametrem aktualnym. W momencie odwołania do parametru formalnego w ciele funkcji, wyrażenie będące parametrem aktualnym jest obliczane w bieżącym stanie (a nie w stanie z miejsca wywołania funkcji). Jako przykład pozważmy program:
Przy przekazywaniu przez wartość, w stanie pustym program się nie obliczy, ponieważ nie da się obliczyć parametru aktualnego . Natomiast przy przekazywaniu przez nazwę, parametr aktualny będzie obliczany dopiero w momencie odwołania do parametru formalnego , czyli w momencie obliczania wartości wyrażenia . W stanie tym zmienna ma już wartość, a zatem wartością całego programu będzie 21.