Podobnie jak poprzednio, stan powinien opisywać wartości przypisane zmiennym.
Tym razem jednak uwzględnimy niezainicjowane zmienne, czyli zmienne bez żadnej wartości.
Przyjmijmy zatem, że stan to skończona funkcja częściowa z do .
Oznaczmy symbolem zbiór wszystkich takich funkcji:
.
Naturalnym stanem początkowym jest stan "pusty", tzn. pusta funkcja częściowa, który będziemy oznaczać symbolem .
Wartość wyrażenia w stanie początkowym wynosi , o ile zachodzi:
Będziemy potrzebowac tranzycji dwóch postaci, podobnie jak poprzednio, ale pierwsza postać będzie nieco ogólniejsza:
Tranzycja ta oznacza mały krok w trakcie obliczania wyrażenia w stanie , w wyniku którego wyewoluowało do , a nowym stanem jest .
Stan może się teraz zmienić na skutek deklaracji zmiennych.
Spróbujmy rozszerzyć semantykę z poprzedniego zadania.
Ponieważ stan jest funkcją częściową, musimy zmienić niektóre reguły, np.
Następnie dodajemy reguły dla wyrażenia .
Gdy jest już obliczone, wystarczy reguła:
Notacja oznacza stan , który zmodyfikowano przypisując zmiennej wartość , niezależnie od tego, czy było określone, czy nie, i pozostawiając niezmienione wartości dla pozostałych zmiennych.
Formalnie
W szczególności dla , jest określone wtedy i tylko wtedy, gdy jest określone.
Natomiast aby obliczyc , potrzebujemy reguły:
Zwróćmy uwagę, że stan może być różny od , np. dlatego, że wewnątrz znajduje się podwyrażenie .
Pytanie: czy taka semantyka jest poprawna?
Niestety nie, gdyż nie uwzględniamy ograniczonego zasięgu zmiennej.
Rzućmy okiem na przykład:
Według naszych intencji to wyrażenie nie ma wartości, gdyż ostatnie odwołanie do jest błędne.
Natomiast według powyższych reguł mamy
Nasz błąd polega na tym, że po zakończeniu obliczania podwyrażenia "zapominamy" przywrócić zmiennej poprzednią wartość (a właściwie brak wartości w przykładzie powyżej).
Przedyskutujmy kilka wariantów.
Wariant 1
Wygodne i eleganckie rozwiązanie tego problemu jest możliwe, jeśli rozszerzymy składnię naszego języka.
Intuicyjnie, reguła
powinna zostać zastąpiona przez
Parser nie mógł rozpoznać (błąd składni): {\displaystyle \mathbf{let}\, x = n \,\mathbf{in}\, e_2, s \,\Longrightarrow\, e_2 \,\mathbf{then}\, \mbox{"przywróć wartość zmiennej x"}, s[x \mapsto n]. }
czyli potrzebujemy konstrukcji składniowej, która polega na obliczeniu wyrażenia , a następnie na przypisaniu zmiennej danej wartości.
Rozszerzmy zatem składnię następujaco:
Wyrażenie jest w pewnym sensie dualne do , gdyż jedyna (choć niewątpliwie istotna) różnica między nimi to kolejność obliczenia i przypisania wartości na zmienną .
Oto nowa reguła
Pewna trudność pojawia się w sytuacji, gdy jest nieokreślone, czyli gdy zmienna jest niezainicjowana -- reguła powyższa nie obejmuje wogóle takiej sytuacji.
Najprostszym sposobem rozwiązania tej trudności jest rozszerzenie konstrukcji :
gdzie symbol oznacza brak wartości.
Dodajemy również regułę:
Rozwiązanie to jest odrobinę nieeleganckie, gdyż prawie identyczne reguły musimy napisać dwukrotnie.
Widać to np. w poniższych regułach, "scalających" semantykę dla z semantyką pozostałych wyrażeń:
Wariant 2
Zanim przejdziemy do kolejnego wariantu, zastanówmy się, czy istnieje inny sposób rozwiązania trudności związanej z , który pozwalałby uniknąć wprowadzania dodatkowej konstrukcji
.
Pomysł może polegać na rozszerzeniu zbioru o dodatkowy element :
Wtedy nie musimy pisać dwóch bardzo podobnych wariantów reguł.
Dodatkowo, w tym rozwiązaniu warto poczynić umowę, że reprezentuje brak wartości zmiennej .
Wtedy stany są funkcjami całkowitymi z w , przyjmującymi wartość różną od tylko dla skończenie wielu elementów.
Pewnym mankamentem jest to, że teraz może pojawiać się w wyrażeniach podobnie jak stałe.
Tym niemniej nie musimy adaptować reguł dla stałych tak, aby radziły one sobie z , ponieważ wyrażenia zawierające możemy również uważać za roszerzenie składni.
Jeśli jednak dopuścimy symbol w wyrażeniach, to możemy elegancko wybrnąć z sytuacji, rozszerzając operacje arytmetyczne na zbiór tak, aby zachowywały one nieokreśloność:
Trzeba jednak w takim razie zadbać o to, aby wyrażenie obliczało się normalnie tylko wtedy, gdy wartość wyrażenia jest różna od .
,
Wariant 3
Zrewidujmy teraz podstawowe założenia, które dotychczas poczyniliśmy.
Jednym z nich było przyjęcie ogólnej postaci tranzycji:
pozwalającej na zmianę stanu podczas obliczania wyrażenia.
Czy faktycznie był to dobry pomysł? Czy moglibyśmy poradzić sobie przy pomocy tranzycji postaci
Spróbujmy! Oto nowa wersja jednej z reguł dla , dotycząca kroku wewnątrz :
Dotychczas nie ma problemu: podwyrażenie jest prawidłowo obliczane w stanie . Trudność pojawi się, gdy
zakończymy obliczanie i przejdziemy do .
Oto możliwa reguła:
Okazuje się, że wszystko jest w porządku. Wyrażenie obliczamy w prawidłowym stanie, tzn. z wartością przypisaną zmiennej .
Mały krok w daje przyczynek do małego kroku w całym wyrażeniu, a przy tym stan pozostaje niezmieniony.
Przy tym wogóle nie potrzebujemy przywracać poprzedniej wartości zmiennej , ponieważ zyskuje nową wartość "tylko" na potrzeby obliczania podwyrażenia !
Można na to również spojrzeć inaczej: informacja o nowej wartości dla zmiennej nie jest jawnie dodawana do stanu , ale jest przechowywana w składni wyrażenia jako deklaracja .
Na końcu musimy oczywiście pozbyć się tej deklaracji za pomocą następującej tranzycji:
Podsumujmy. Okazuje się, że rozwiązanie nie było wcale łatwe, nawet dla tak prościutkiego języka. W przyszłości przekonamy się, że łatwiej jest poradzić sobie z zagadnieniem wiązania identyfikatorów w semantyce naturalnej (duże kroki).
W wariancie 1 i 2 wprowadziliśmy do języka dodatkowe elementy, tak by łatwiej było pisać reguły. W przyszłości będziemy czasem stosować takie podejście.
Niekiedy jednak rozszerzanie języka będzie zabronione.