WDP Gramatyki – definiowanie składni i semantyki wyrażeń: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
m
Linia 2: Linia 2:
 
==Gramatyki – definiowanie składni i semantyki wyrażeń==
 
==Gramatyki – definiowanie składni i semantyki wyrażeń==
  
Język naturalny jest siłą rzeczy niejednoznaczny. Wiele pozostawia interpretacji, wyczuciu. Często słyszymy, że dwie osoby rozmawiając, nie mogą się nawzajem zrozumieć. Czasem sami poniewczasie dostrzegamy, że inaczej coś zrozumieliśmy, niż miał to na myśli nasz rozmówca.  
+
Język naturalny jest siłą rzeczy niejednoznaczny. Wiele pozostawia interpretacji, wyczuciu. Często słyszymy, że dwie rozmawiające osoby nie mogą się nawzajem zrozumieć. Czasem sami poniewczasie dostrzegamy, że inaczej coś zrozumieliśmy, niż miał to na myśli nasz rozmówca.  
  
Komunikacja z komputerem jest o tyle trudniejsza, że nie pozostawia miejsca na domysły czy interpretacje. Musimy niezwykle ściśle wyartykułować nasze żądania wobec komputera, aby ten bez wahania wykonał to, o co nam rzeczywiście chodziło. Ponadto, chodzi o to, żeby nasze programy były uniwersalne, przynajmniej w części dotyczącej algorytmiki, a ich wykonanie na każdym komputerze wyglądało tak samo, albo różniło się co najwyżej nieznaczącymi szczegółami, wynikającymi ze specyfiki architektury komputera, a nie z istoty zapisu algorytmu.  
+
Komunikacja z komputerem jest o tyle trudniejsza, że nie pozostawia miejsca na domysły czy interpretacje. Musimy niezwykle ściśle wyartykułować nasze żądania wobec komputera, aby ten bez wahania wykonał to, o co nam rzeczywiście chodziło. Ponadto chodzi o to, żeby nasze programy były uniwersalne, przynajmniej w części dotyczącej algorytmiki, a ich wykonanie na każdym komputerze wyglądało tak samo albo różniło się co najwyżej nieznaczącymi szczegółami, wynikającymi ze specyfiki architektury komputera, a nie z istoty zapisu algorytmu.  
 
[[Image:chomsky.jpg||thumb|right|200px|Avram Noam Chomsky (1928- )]]
 
[[Image:chomsky.jpg||thumb|right|200px|Avram Noam Chomsky (1928- )]]
 
Różnica między językiem programowania a językiem naturalnym polega na tym, że język naturalny jest zastany, podczas gdy język programowania możemy stworzyć od zera według naszego pomysłu. Użyjemy do tego metody gramatyk formalnych, które zostały wynalezione przez znanego językoznawcę Noama Chomsky'ego. Chomsky, jeden z największych językoznawców – postać niezwykle barwna i kontrowersyjna – przez większość swojego życia próbował usystematyzować gramatykę języka angielskiego. Mechanizm gramatyk zaproponowany przez niego co prawda nie wystarczył do tego, żeby oddać bogactwo języka naturalnego, ale okazał się niezastąpiony przy definiowaniu sztucznych języków programowania. Oto pokrótce część jego teorii dotycząca tzw. ''gramatyk bezkontekstowych'', z których będziemy korzystali.  
 
Różnica między językiem programowania a językiem naturalnym polega na tym, że język naturalny jest zastany, podczas gdy język programowania możemy stworzyć od zera według naszego pomysłu. Użyjemy do tego metody gramatyk formalnych, które zostały wynalezione przez znanego językoznawcę Noama Chomsky'ego. Chomsky, jeden z największych językoznawców – postać niezwykle barwna i kontrowersyjna – przez większość swojego życia próbował usystematyzować gramatykę języka angielskiego. Mechanizm gramatyk zaproponowany przez niego co prawda nie wystarczył do tego, żeby oddać bogactwo języka naturalnego, ale okazał się niezastąpiony przy definiowaniu sztucznych języków programowania. Oto pokrótce część jego teorii dotycząca tzw. ''gramatyk bezkontekstowych'', z których będziemy korzystali.  
Linia 23: Linia 23:
 
\langle N,T,P,S \rangle,</math></center>
 
\langle N,T,P,S \rangle,</math></center>
  
gdzie <math>N</math> oraz <math>T</math> to skończone i rozłączne zbiory, odpowiednio symboli '' pomocniczych'' (zwanych czasem nieterminalnymi lub nieterminalami) i ''końcowych'' (zwanych czasem terminalnymi lub terminalami), <math>P</math> jest zbiorem produkcji, a  <math>S\in N</math> jest wyróżnionym symbolem pomocniczym zwanym ''aksjomatem'' gramatyki. Produkcje wchodzące w skład zbioru <math>P</math> są postaci <math>X ::= w</math>, gdzie <math>X\in N</math>, a <math>w\in (N\cup T)^*</math>. Pojedynczą taką produkcję interpretujemy jako możliwość zastąpienia symbolu pomocniczego <math>X</math> przez słowo, w skład którego mogą wchodzić zarówno symbole pomocnicze, jak i końcowe. Proces wyprowadzania słów języka zdefiniowanego przez gramatykę odbywa się w taki sposób, że zaczynamy od symbolu <math>S</math> i do poszczególnych symboli pomocniczych stosujemy którąś z produkcji ze zbioru <math>P</math> tak długo, aż wszystkie symbole pomocnicze znikną. To, co zostanie, czyli słowo złożone z samych symboli końcowych, nazywamy słowem wyprowadzalnym z gramatyki. Zbiór wszystkich słów wyprowadzalnych z danej gramatyki nazywa się ''językiem generowanym przez gramatykę''.
+
gdzie <math>N</math> oraz <math>T</math> to skończone i rozłączne zbiory, odpowiednio symboli ''pomocniczych'' (zwanych czasem nieterminalnymi lub nieterminalami) i ''końcowych'' (zwanych czasem terminalnymi lub terminalami), <math>P</math> jest zbiorem produkcji, a  <math>S\in N</math> jest wyróżnionym symbolem pomocniczym zwanym ''aksjomatem'' gramatyki. Produkcje wchodzące w skład zbioru <math>P</math> są postaci <math>X ::= w</math>, gdzie <math>X\in N</math>, a <math>w\in (N\cup T)^*</math>. Pojedynczą taką produkcję interpretujemy jako możliwość zastąpienia symbolu pomocniczego <math>X</math> przez słowo, w skład którego mogą wchodzić zarówno symbole pomocnicze, jak i końcowe. Proces wyprowadzania słów języka zdefiniowanego przez gramatykę odbywa się w taki sposób, że zaczynamy od symbolu <math>S</math> i do poszczególnych symboli pomocniczych stosujemy którąś z produkcji ze zbioru <math>P</math> tak długo, aż wszystkie symbole pomocnicze znikną. To, co zostanie, czyli słowo złożone z samych symboli końcowych, nazywamy słowem wyprowadzalnym z gramatyki. Zbiór wszystkich słów wyprowadzalnych z danej gramatyki nazywa się ''językiem generowanym przez gramatykę''.
  
 
Zwróćmy uwagę na to, że jeśli w trakcie wywodu słowa pojawia się kilka identycznych symboli pomocniczych, to każdy z nich może samodzielnie decydować o tym, co z siebie wyprodukować.  
 
Zwróćmy uwagę na to, że jeśli w trakcie wywodu słowa pojawia się kilka identycznych symboli pomocniczych, to każdy z nich może samodzielnie decydować o tym, co z siebie wyprodukować.  
Linia 51: Linia 51:
 
  czyli takie słowo <math>a_1,\ldots,a_n</math>, że dla każdego <math>k\in\{1,...,n\}</math> zachodzi  
 
  czyli takie słowo <math>a_1,\ldots,a_n</math>, że dla każdego <math>k\in\{1,...,n\}</math> zachodzi  
 
  <math>a_k=a_{n-k+1}</math>.  
 
  <math>a_k=a_{n-k+1}</math>.  
  Parzysty palindrom, to palindrom o parzystej długości.</div>
+
  Parzysty palindrom to palindrom o parzystej długości.</div>
 
</div>
 
</div>
  
Linia 119: Linia 119:
 
Podamy teraz reguły semantyczne związane z produkcjami, które pozwolą na określenie wartości liczby na podstawie jej drzewa wywodu. Reguły te będą związane z produkcjami gramatyki. Musimy dokonać tu pewnego rozróżnienia, często niezauważonego. Gdy piszemy na przykład <math>11</math>, to myślimy o tym napisie, jako o liczbie "jedenaście". Jednak na dobrą sprawę są to po prostu dwie jedynki napisane na klawiaturze, które można zinterpretować też na przykład jako trójkę w systemie dwójkowym. Gramatyka służy do tego, aby produkować napisy. Zbiór napisów wyprowadzalnych z gramatyki jest językiem. Nas jednak przede wszystkim będzie interesowało znaczenie tych napisów w pewnej dziedzinie. Umówmy się więc, że aby zrobić rozróżnienie między napisem <math>11</math>, a liczbą <math>11</math>, tę drugą będziemy oznaczali kolorem niebieskim. Zatem <math>11</math> oznacza napis złożony z dwóch znaków, a <math>\textcolor{blue}{11}</math> będzie oznaczało liczbę jedenaście.  
 
Podamy teraz reguły semantyczne związane z produkcjami, które pozwolą na określenie wartości liczby na podstawie jej drzewa wywodu. Reguły te będą związane z produkcjami gramatyki. Musimy dokonać tu pewnego rozróżnienia, często niezauważonego. Gdy piszemy na przykład <math>11</math>, to myślimy o tym napisie, jako o liczbie "jedenaście". Jednak na dobrą sprawę są to po prostu dwie jedynki napisane na klawiaturze, które można zinterpretować też na przykład jako trójkę w systemie dwójkowym. Gramatyka służy do tego, aby produkować napisy. Zbiór napisów wyprowadzalnych z gramatyki jest językiem. Nas jednak przede wszystkim będzie interesowało znaczenie tych napisów w pewnej dziedzinie. Umówmy się więc, że aby zrobić rozróżnienie między napisem <math>11</math>, a liczbą <math>11</math>, tę drugą będziemy oznaczali kolorem niebieskim. Zatem <math>11</math> oznacza napis złożony z dwóch znaków, a <math>\textcolor{blue}{11}</math> będzie oznaczało liczbę jedenaście.  
  
Teraz możemy już ustalić znaczenie poszczególnych produkcji gramatyki. Będziemy wiązali z każdą produkcją regułę semantyczną. Węzły drzewa będą interpretowane w wybranej przez nas dziedzinie. Określenie wartości węzła będzie zależało od wartości podwieszonych pod nim węzłów, czyli jego synów. Reguła semantyczna będzie określała, jak wartość węzła zależy od wartości synów. Oznaczmy wartość węzła <math>v</math> w drzewie przez <math>I(v)</math>. Zatem <math>I:V\rightarrow Z</math> jest funkcją, która każdemu węzłowi przypisze liczbę całkowitą.
+
Teraz możemy już ustalić znaczenie poszczególnych produkcji gramatyki. Będziemy wiązali z każdą produkcją regułę semantyczną. Węzły drzewa będą interpretowane w wybranej przez nas dziedzinie. Określenie wartości węzła będzie zależało od wartości podwieszonych pod nim węzłów, czyli jego synów. Reguła semantyczna będzie określała jak wartość węzła zależy od wartości synów. Oznaczmy wartość węzła <math>v</math> w drzewie przez <math>I(v)</math>. Zatem <math>I:V\rightarrow Z</math> jest funkcją, która każdemu węzłowi przypisze liczbę całkowitą.
  
 
Zacznijmy od tego, że określimy znaczenie węzłów końcowych – liści drzewa. Są one cyframi -  symbolami końcowymi. Ustalamy zatem, że:
 
Zacznijmy od tego, że określimy znaczenie węzłów końcowych – liści drzewa. Są one cyframi -  symbolami końcowymi. Ustalamy zatem, że:
Linia 171: Linia 171:
 
}}  
 
}}  
  
Zmienne, których będziemy używali w wyrażeniach, będą oznaczane za pomocą identyfikatorów. Identyfikatory będą ciągami liter lub cyfr zaczynającymi się od litery. Zatem gramatyka dla identyfikatora, to  
+
Zmienne, których będziemy używali w wyrażeniach, będą oznaczane za pomocą identyfikatorów. Identyfikatory będą ciągami liter lub cyfr zaczynającymi się od litery. Zatem gramatyka dla identyfikatora to  
 
<span id=""/>  
 
<span id=""/>  
 
<center><math>  
 
<center><math>  
Linia 179: Linia 179:
 
W gramatyce tej <math>L</math> oznacza pojedynczą literę, <math>C</math> oznacza pojedynczą cyfrę, <math>Z</math> oznacza ciąg złożony z liter lub cyfr. Zgodnie z tradycją programistyczną, poza literami alfebetu łacińskiego dopuszczamy jeszcze jako literę symbol podkreślenia.  
 
W gramatyce tej <math>L</math> oznacza pojedynczą literę, <math>C</math> oznacza pojedynczą cyfrę, <math>Z</math> oznacza ciąg złożony z liter lub cyfr. Zgodnie z tradycją programistyczną, poza literami alfebetu łacińskiego dopuszczamy jeszcze jako literę symbol podkreślenia.  
  
Zbliżyliśmy się do momentu, kiedy może nam braknąć oznaczeń na symbole pomocnicze. W rzeczywistości w językach programowania gramatyki mają sto kilkadziesiąt symboli pomocniczych. Żeby się nam to wszystko nie pomyliło przyjmijmy konwencję, w myśl której symbole pomocnicze piszemy w nawiasach rombowych, a oznaczamy je znaczącym tekstem, np. <tt><cyfra>, <litera>, <ciąg liter lub cyfr></tt> itp.  
+
Zbliżyliśmy się do momentu, kiedy może nam braknąć oznaczeń na symbole pomocnicze. W rzeczywistości w językach programowania gramatyki mają sto kilkadziesiąt symboli pomocniczych. Żeby się nam to wszystko nie pomyliło, przyjmijmy konwencję, w myśl której symbole pomocnicze piszemy w nawiasach rombowych, a oznaczamy je znaczącym tekstem, np. <tt><cyfra>, <litera>, <ciąg liter lub cyfr></tt> itp.  
  
 
Jak teraz w analogiczny sposób określić gramatykę wyrażeń całkowitoliczbowych? Zauważmy, że w wyrażeniach są zmienne, stałe, operatory <math>+,-,*,\div,\bmod</math> oraz nawiasy. Ogólnie rzecz biorąc można powiedzieć, że wyrażeniem jest stała, zmienna, oraz że jeśli <math>U,V</math> są wyrażeniami, to wyrażeniem też jest <math>-U,U+V,U-V,U*V,U\div V, U \bmod V, (U)</math>. Daje to niezwykle prostą gramatykę:  
 
Jak teraz w analogiczny sposób określić gramatykę wyrażeń całkowitoliczbowych? Zauważmy, że w wyrażeniach są zmienne, stałe, operatory <math>+,-,*,\div,\bmod</math> oraz nawiasy. Ogólnie rzecz biorąc można powiedzieć, że wyrażeniem jest stała, zmienna, oraz że jeśli <math>U,V</math> są wyrażeniami, to wyrażeniem też jest <math>-U,U+V,U-V,U*V,U\div V, U \bmod V, (U)</math>. Daje to niezwykle prostą gramatykę:  
Linia 223: Linia 223:
 
  <zmienna> ::= <identyfikator></tt>
 
  <zmienna> ::= <identyfikator></tt>
  
Symbol pomocniczy (liczba całkowita bez znaku) jest prostym uproszczeniem gramatyki liczb polegającym na usunięciu znaku.  
+
Symbol pomocniczy (liczba całkowita bez znaku) jest uproszczeniem gramatyki liczb polegającym na usunięciu znaku.  
  
 
<span id="Stałe bez znaku" \>  
 
<span id="Stałe bez znaku" \>  
Linia 249: Linia 249:
 
Zbadajmy teraz, jak wygląda drzewo wywodu wyrażenia <math>a+b*4</math>. Łatwo się można przekonać, że pierwszą produkcją musi być  <tt><wyrażenie Z> ::= <wyrażenie Z> + <składnik></tt>. Żadna inna produkcja nie wchodzi w grę: dwie z nich wprowadzają znak minus, którego byśmy się już nie pozbyli, a trzecia <tt><wyrażenie Z> ::= <składnik></tt> przenosi nas do składnika, z którego nie daje się już uzyskać plusa bez wprowadzenia nawiasów.  
 
Zbadajmy teraz, jak wygląda drzewo wywodu wyrażenia <math>a+b*4</math>. Łatwo się można przekonać, że pierwszą produkcją musi być  <tt><wyrażenie Z> ::= <wyrażenie Z> + <składnik></tt>. Żadna inna produkcja nie wchodzi w grę: dwie z nich wprowadzają znak minus, którego byśmy się już nie pozbyli, a trzecia <tt><wyrażenie Z> ::= <składnik></tt> przenosi nas do składnika, z którego nie daje się już uzyskać plusa bez wprowadzenia nawiasów.  
  
Skoro tak, to dalej dostajemy już w jednoznaczny sposób następujące drzewo wywodu.  
+
Skoro tak, dalej dostajemy już w jednoznaczny sposób następujące drzewo wywodu.  
  
 
\begin{rysunek}[Drzewo wywodu dla wyrażenia <math>a+b*4</math>]
 
\begin{rysunek}[Drzewo wywodu dla wyrażenia <math>a+b*4</math>]
 
\end{rysunek}
 
\end{rysunek}
  
Jakie reguły semantyczne należy dobrać do naszych produkcji? Dość naturalne. Dla stałych nie ma problemu: już je mamy. Ze zmiennymi jest jednak mały kłopot: jaką wartość powinny mieć zmienne? Musimy tu zrobić dygresję na temat implementacji. Wszystkie zmienne w programie w momencie, w którym się z nich korzysta, mają swoje miejsce w pamięci. W czasie kompilacji i konsolidacji programu ustala się adres, pod którym można je znaleźć i kiedy odwołujemy się do nich, ich wartość uzyskujemy zaglądając do tego miejsca w pamięci. Przyjmijmy, że pamięć w naszym ujęciu modeluje funkcja <math>V:ID\rightarrow Z_?</math>, która zbiór wszystkich identyfikatorów <math>ID</math> przeprowadza w zbiór nieco rozszerzony liczb całkowitych <math>Z_?=Z\cup \{?\}</math>,gdzie symbol <math>?</math> oznacza wartość ''nieokreśloną''. Ta wartość jest nam potrzebna, gdyż wszystkie zmienne w chwili, gdy zaczynamy wykonywać program, mają wartości nieokreślone i dopiero w efekcie wykonywania instrukcji, konkretne wartości będą im sukcesywnie nadawane (i zmieniane). Przy okazji mała dygresja. W odróżnieniu od całej matematyki, zmienne naprawdę zmieniają swoje wartości. Gdy w matematyce zaczynamy wywód mówiąc np. "Niech <math>k</math> będzie liczbą przekątnych w wielokącie", wówczas do końca wywodu <math>k</math> nie może oznaczać niczego innego. W informatyce, jeśli napiszemy niech <math>k</math> stanie się równe <math>5</math>, to za chwilę wartość zmiennej <math>k</math> może ulec zmianie, jeśli sobie tego zażyczymy.  
+
Jakie reguły semantyczne należy dobrać do naszych produkcji? Dość naturalne. Dla stałych nie ma problemu: już je mamy. Ze zmiennymi jest jednak mały kłopot: jaką wartość powinny mieć zmienne? Musimy tu zrobić dygresję na temat implementacji. Wszystkie zmienne w programie w momencie, w którym się z nich korzysta, mają swoje miejsce w pamięci. W czasie kompilacji i konsolidacji programu ustala się adres, pod którym można je znaleźć i kiedy odwołujemy się do nich, ich wartość uzyskujemy zaglądając do tego miejsca w pamięci. Przyjmijmy, że pamięć w naszym ujęciu modeluje funkcja <math>V:ID\rightarrow Z_?</math>, która zbiór wszystkich identyfikatorów <math>ID</math> przeprowadza w zbiór nieco rozszerzony liczb całkowitych <math>Z_?=Z\cup \{?\}</math>, gdzie symbol <math>?</math> oznacza wartość ''nieokreśloną''. Ta wartość jest nam potrzebna, gdyż wszystkie zmienne w chwili, gdy zaczynamy wykonywać program, mają wartości nieokreślone i dopiero w efekcie wykonywania instrukcji konkretne wartości będą im sukcesywnie nadawane (i zmieniane). Przy okazji mała dygresja. W odróżnieniu od całej matematyki, zmienne naprawdę zmieniają swoje wartości. Gdy w matematyce zaczynamy wywód mówiąc np. „Niech <math>k</math> będzie liczbą przekątnych w wielokącie”, wówczas do końca wywodu <math>k</math> nie może oznaczać niczego innego. W informatyce, jeśli napiszemy „niech <math>k</math> stanie się równe <math>5</math>, to za chwilę wartość zmiennej <math>k</math> może ulec zmianie, jeśli sobie tego zażyczymy.  
  
 
Zatem przez <math>V(z)</math> będziemy rozumieli wartość zmiennej <math>z</math>, a wartość wyrażenia, w którym zmienne występują, będzie zależała od tego wartościowania. Interpretację wyrażenia musimy zatem uzależnić od wartościowania <math>V</math>. Podamy teraz reguły wartościowania wyrażeń. Tym razem funkcję interpretacji oznaczymy przez <math>I_V</math>, żeby podkreślić zależność interpretacji wyrażenia od wartościowania zmiennych. Musimy jeszcze zauważyć, że jeśli w wyrażeniu występuje dzielenie (niezależnie od tego czy div czy mod), to wynik dzielenia będzie błędny, jeśli dzielnik będzie zerem. Komputer, zmuszony do wykonania takiego dzielenia, odmawia współpracy i generuje błąd. Wygodnie jest, określając semantykę, tę sytuację uwzględnić w dziedzinie semantycznej, dodając wartość error do zestawu wartości całkowitych. Zatem ostatecznie nasza dziedzina liczb całkowitych rozrasta się nam o dwie wartości: <math>error</math> i <math>?</math>. Rozszerzmy jeszcze działania na te wartości. Przyjmujemy więc, że dla każdej operacji dwuargumentowej <math>\oplus</math>
 
Zatem przez <math>V(z)</math> będziemy rozumieli wartość zmiennej <math>z</math>, a wartość wyrażenia, w którym zmienne występują, będzie zależała od tego wartościowania. Interpretację wyrażenia musimy zatem uzależnić od wartościowania <math>V</math>. Podamy teraz reguły wartościowania wyrażeń. Tym razem funkcję interpretacji oznaczymy przez <math>I_V</math>, żeby podkreślić zależność interpretacji wyrażenia od wartościowania zmiennych. Musimy jeszcze zauważyć, że jeśli w wyrażeniu występuje dzielenie (niezależnie od tego czy div czy mod), to wynik dzielenia będzie błędny, jeśli dzielnik będzie zerem. Komputer, zmuszony do wykonania takiego dzielenia, odmawia współpracy i generuje błąd. Wygodnie jest, określając semantykę, tę sytuację uwzględnić w dziedzinie semantycznej, dodając wartość error do zestawu wartości całkowitych. Zatem ostatecznie nasza dziedzina liczb całkowitych rozrasta się nam o dwie wartości: <math>error</math> i <math>?</math>. Rozszerzmy jeszcze działania na te wartości. Przyjmujemy więc, że dla każdej operacji dwuargumentowej <math>\oplus</math>

Wersja z 20:55, 15 lis 2006

<<< Powrót

Gramatyki – definiowanie składni i semantyki wyrażeń

Język naturalny jest siłą rzeczy niejednoznaczny. Wiele pozostawia interpretacji, wyczuciu. Często słyszymy, że dwie rozmawiające osoby nie mogą się nawzajem zrozumieć. Czasem sami poniewczasie dostrzegamy, że inaczej coś zrozumieliśmy, niż miał to na myśli nasz rozmówca.

Komunikacja z komputerem jest o tyle trudniejsza, że nie pozostawia miejsca na domysły czy interpretacje. Musimy niezwykle ściśle wyartykułować nasze żądania wobec komputera, aby ten bez wahania wykonał to, o co nam rzeczywiście chodziło. Ponadto chodzi o to, żeby nasze programy były uniwersalne, przynajmniej w części dotyczącej algorytmiki, a ich wykonanie na każdym komputerze wyglądało tak samo albo różniło się co najwyżej nieznaczącymi szczegółami, wynikającymi ze specyfiki architektury komputera, a nie z istoty zapisu algorytmu.

Avram Noam Chomsky (1928- )

Różnica między językiem programowania a językiem naturalnym polega na tym, że język naturalny jest zastany, podczas gdy język programowania możemy stworzyć od zera według naszego pomysłu. Użyjemy do tego metody gramatyk formalnych, które zostały wynalezione przez znanego językoznawcę Noama Chomsky'ego. Chomsky, jeden z największych językoznawców – postać niezwykle barwna i kontrowersyjna – przez większość swojego życia próbował usystematyzować gramatykę języka angielskiego. Mechanizm gramatyk zaproponowany przez niego co prawda nie wystarczył do tego, żeby oddać bogactwo języka naturalnego, ale okazał się niezastąpiony przy definiowaniu sztucznych języków programowania. Oto pokrótce część jego teorii dotycząca tzw. gramatyk bezkontekstowych, z których będziemy korzystali.

Alfabetem nazywamy dowolny skończony i niepusty zbiór symboli . Słowami nad alfabetem nazwiemy wszystkie skończone ciągi złożone z elementów alfabetu. W szczególności dopuścimy puste słowo – słowo nie mające żadnej litery, które oznaczamy wspólnym symbolem niezależnie od alfabetu nad którym jest utworzone. Zbiór wszystkich słów nad alfabetem oznaczamy przez . Przykładowo jeśli


Parser nie mógł rozpoznać (błąd składni): {\displaystyle A={a}, \text to\ A^*={\varepsilon,a,aa,aaa,\ldots}} .

Jeśli

Parser nie mógł rozpoznać (błąd składni): {\displaystyle A=\{a,b\}, \text to\ A^*=\{\varepsilon,a,b,aa,ab,ba,bb,aaa,aab,\ldots\}} .

Długość słowa, to liczba jego liter. Język nad alfabetem , to dowolny podzbiór zbioru .

Gramatyką (bezkontekstową) nazwiemy uporządkowaną czwórkę

gdzie oraz to skończone i rozłączne zbiory, odpowiednio symboli pomocniczych (zwanych czasem nieterminalnymi lub nieterminalami) i końcowych (zwanych czasem terminalnymi lub terminalami), jest zbiorem produkcji, a jest wyróżnionym symbolem pomocniczym zwanym aksjomatem gramatyki. Produkcje wchodzące w skład zbioru są postaci , gdzie , a . Pojedynczą taką produkcję interpretujemy jako możliwość zastąpienia symbolu pomocniczego przez słowo, w skład którego mogą wchodzić zarówno symbole pomocnicze, jak i końcowe. Proces wyprowadzania słów języka zdefiniowanego przez gramatykę odbywa się w taki sposób, że zaczynamy od symbolu i do poszczególnych symboli pomocniczych stosujemy którąś z produkcji ze zbioru tak długo, aż wszystkie symbole pomocnicze znikną. To, co zostanie, czyli słowo złożone z samych symboli końcowych, nazywamy słowem wyprowadzalnym z gramatyki. Zbiór wszystkich słów wyprowadzalnych z danej gramatyki nazywa się językiem generowanym przez gramatykę.

Zwróćmy uwagę na to, że jeśli w trakcie wywodu słowa pojawia się kilka identycznych symboli pomocniczych, to każdy z nich może samodzielnie decydować o tym, co z siebie wyprodukować.

Przykład 1

Gramatyka generuje wszystkie parzyste słowa złożone z samych liter (uwaga słowo puste jest słowem o parzystej długości!).

Przykład 2

Gramatyka generuje wszystkie parzyste palindromy nad dwuelementowym alfabetem .


[Jeśli nie wiesz co to są palindromy, to kliknij poniżej]

Przyjmijmy konwencję notacyjną, która pozwoli nam uprościć wypisywanie produkcji wywodzących się z tego samego symbolu. Jeśli jest kilka wariantów produkcji z tego samego symbolu, to piszemy je w jednym ciągu oddzielając pionowymi kreskami. W szczególności produkcje gramatyki wyglądają w tej notacji następująco:

.

Drzewa wywodu

Historię wyprowadzeń słowa w danej gramatyce możemy zapisać za pomocą drzewa wywodu. Korzeniem w tym drzewie jest aksjomat gramatyki, a jeśli została do symbolu pomocniczego , który jest węzłem w drzewie, zastosowana produkcja , to synami węzła stają się elementy . Oto przykład wyprowadzenia słowa abba w gramatyce .

\begin{Rysunek}[rys1-abba] \end{Rysunek}

Moglibyśmy to wyprowadzenie przedstawić w wersji liniowej

,

jednak drzewo wywodu daje nam dokładnie taką informację, o jaką nam chodzi, zamazując niepotrzebne szczegóły. Przyjrzyjmy się bliżej różnicom w reprezentacji historii wywodu. Rozważmy gramatykę

.

Za pomocą tej gramatyki możemy wygenerować dowolną naprzemienną sekwencję liter oraz zaczynającą się i kończącą na literę . Na ile sposobów można otrzymać słowo ? W notacji liniowej tych sposobów jest sporo, przykładowów

.

Albo inaczej

.


Ćwiczenie

Na ile sposobów można liniowo zapisać wywód słowa ?

Odpowiedź

{{{2}}}

Ile jest drzew wywodu tego słowa? Okazuje się, że tylko dwa. Poniżej przykład ilustrujący powstawanie wspomnianych drzew. Przykład w powiększeniu.

<flash>file=Dwa-drzewa-babab.swf|width=550|height=350|salign=left</flash>

Można gramatykę tak skonstruować, żeby słowo babab, podobnie jak i każde inne generowane przez powyższą gramatykę, miało tylko jedno drzewo wywodu. Oto ta gramatyka:

Taka gramatyka ma już tylko jedno drzewo wywodu dla słowa babab. Oto ono:

\begin{Rysunek}[rys3-babab-jedno] \end{Rysunek}

Podobnie każde inne słowo wyprowadzalne z tej gramatyki ma jedno drzewo wywodu.

Powiemy, że gramatyka jest jednoznaczna, jeśli każde słowo wyprowadzalne z ma tylko jedno drzewo wywodu. Własność jednoznaczności jest niezwykle ważna – będziemy starali się tak zdefiniować język programowania, aby drzewo wywodu każdego programu było jednoznaczne. To pozwoli nam określić semantykę języka programowania jako funkcję obliczaną na podstawie drzewa wywodu.

Powiemy, że gramatyka jest zgodna z językiem , jeśli każde słowo wyprowadzalne z należy do . Powiemy, że gramatyka jest pełna względem języka , jeśli wszystkie słowa należące do są wyprowadzalne z . Rzecz jasna każda gramatyka G jest zarówno zgodna, jak i pełna względem swojego języka .

Liczby całkowite

Zacznijmy od zdefiniowania liczb całkowitych. Po pierwsze określmy, co będziemy chcieli nazywać liczbami całkowitymi. Przyjmijmy, że poprawnymi liczbami są wszystkie skończone ciągi cyfr dziesiętnych poprzedzone pojedynczym znakiem lub . Dopuścimy zatem jako poprawne napisy , ale jako niepoprawne uznamy np. napisy . Oto propozycja gramatyki:

G=S,Z,C,L,-,+,0,1,2,3,4,5,6,7,8,9,C::=0|1|2|3|4|5|6|7|8|9,S::=L|ZL,Z::=+|-,L::=C|LC,,S

W ten sposób napisana gramatyka daje jednoznaczne wyprowadzenie każdej liczby całkowitej, zgodnej z naszymi założeniami. Zauważmy, że na samym początku wyprowadzania decydujemy, czy liczba ma być ze znakiem, czy bez – o tym decyduje wybór lub spośród dwóch wariantów, które daje nam symbol . Z symbolu pomocniczego możemy wygenerować dowolny niepusty ciąg cyfr, a z symbolu znak. Przykładowe wyprowadzenie liczby -120 wygląda następująco:

\begin{Rysunek}[rys3-120] \end{Rysunek}

Reguły semantyczne

Podamy teraz reguły semantyczne związane z produkcjami, które pozwolą na określenie wartości liczby na podstawie jej drzewa wywodu. Reguły te będą związane z produkcjami gramatyki. Musimy dokonać tu pewnego rozróżnienia, często niezauważonego. Gdy piszemy na przykład , to myślimy o tym napisie, jako o liczbie "jedenaście". Jednak na dobrą sprawę są to po prostu dwie jedynki napisane na klawiaturze, które można zinterpretować też na przykład jako trójkę w systemie dwójkowym. Gramatyka służy do tego, aby produkować napisy. Zbiór napisów wyprowadzalnych z gramatyki jest językiem. Nas jednak przede wszystkim będzie interesowało znaczenie tych napisów w pewnej dziedzinie. Umówmy się więc, że aby zrobić rozróżnienie między napisem , a liczbą , tę drugą będziemy oznaczali kolorem niebieskim. Zatem oznacza napis złożony z dwóch znaków, a Parser nie mógł rozpoznać (nieznana funkcja „\textcolor”): {\displaystyle \textcolor{blue}{11}} będzie oznaczało liczbę jedenaście.

Teraz możemy już ustalić znaczenie poszczególnych produkcji gramatyki. Będziemy wiązali z każdą produkcją regułę semantyczną. Węzły drzewa będą interpretowane w wybranej przez nas dziedzinie. Określenie wartości węzła będzie zależało od wartości podwieszonych pod nim węzłów, czyli jego synów. Reguła semantyczna będzie określała jak wartość węzła zależy od wartości synów. Oznaczmy wartość węzła w drzewie przez . Zatem jest funkcją, która każdemu węzłowi przypisze liczbę całkowitą.

Zacznijmy od tego, że określimy znaczenie węzłów końcowych – liści drzewa. Są one cyframi - symbolami końcowymi. Ustalamy zatem, że:

  Parser nie mógł rozpoznać (nieznana funkcja „\textcolor”): {\displaystyle \textcolor{blue}{0},}     Parser nie mógł rozpoznać (nieznana funkcja „\textcolor”): {\displaystyle \textcolor{blue}{1},}      Parser nie mógł rozpoznać (nieznana funkcja „\textcolor”): {\displaystyle \textcolor{blue}{9},}     Parser nie mógł rozpoznać (nieznana funkcja „\textcolor”): {\displaystyle \textcolor{blue}{-1},}     Parser nie mógł rozpoznać (nieznana funkcja „\textcolor”): {\displaystyle \textcolor{blue}{1}.}

Dalej

  • dla produkcji określamy gdzie Parser nie mógł rozpoznać (nieznana funkcja „\ldtots”): {\displaystyle x\in\{0,1,\ldtots,9\}}
  • dla produkcji określamy gdzie
  • dla produkcji określamy ,

przy czym stosujemy tu konwencję, że jeśli zastosowaliśmy produkcję , to ojciec w tej regule jest oznaczany przez , a syn przez . Dla produkcji musimy się umówić, jak rozróżnimy z lewej i z prawej strony produkcji. Zatem umawiamy się, że jeśli symbol powtarza się z lewej strony i prawej, to ten z prawej wystąpi we wzorze z primem. Kolejne reguły zatem, to

  • Dla produkcji określamy ,
  • Dla produkcji określamy ,
  • Dla produkcji określamy


Oto drzewo semantyczne dla napisu .

\begin{Rysunek}[semantyka-124] \end{Rysunek}

Jak widać, wartość w korzeniu to Parser nie mógł rozpoznać (nieznana funkcja „\textcolor”): {\displaystyle \textcolor{blue}{-124}} i jest to faktycznie liczba, którą chcemy oznaczać za pomocą napisu .

Ćwiczenie

Podaj gramatykę generującą liczby w układzie dwójkowym i określ dla niej reguły semantyczne

Wskazówka

{{{3}}}

Odpowiedź

{{{2}}}

Zmienne, których będziemy używali w wyrażeniach, będą oznaczane za pomocą identyfikatorów. Identyfikatory będą ciągami liter lub cyfr zaczynającymi się od litery. Zatem gramatyka dla identyfikatora to

W gramatyce tej oznacza pojedynczą literę, oznacza pojedynczą cyfrę, oznacza ciąg złożony z liter lub cyfr. Zgodnie z tradycją programistyczną, poza literami alfebetu łacińskiego dopuszczamy jeszcze jako literę symbol podkreślenia.

Zbliżyliśmy się do momentu, kiedy może nam braknąć oznaczeń na symbole pomocnicze. W rzeczywistości w językach programowania gramatyki mają sto kilkadziesiąt symboli pomocniczych. Żeby się nam to wszystko nie pomyliło, przyjmijmy konwencję, w myśl której symbole pomocnicze piszemy w nawiasach rombowych, a oznaczamy je znaczącym tekstem, np. <cyfra>, <litera>, <ciąg liter lub cyfr> itp.

Jak teraz w analogiczny sposób określić gramatykę wyrażeń całkowitoliczbowych? Zauważmy, że w wyrażeniach są zmienne, stałe, operatory Parser nie mógł rozpoznać (błąd składni): {\displaystyle +,-,*,\div,\bmod} oraz nawiasy. Ogólnie rzecz biorąc można powiedzieć, że wyrażeniem jest stała, zmienna, oraz że jeśli są wyrażeniami, to wyrażeniem też jest . Daje to niezwykle prostą gramatykę:


G=W,<zmienna>,<stała>,Parser nie mógł rozpoznać (błąd składni): {\displaystyle \{\} _,a,b,...,z,0,1,...,9,-,+,*,Parser nie mógł rozpoznać (błąd składni): {\displaystyle \div\} ,Parser nie mógł rozpoznać (błąd składni): {\displaystyle \bmod\} ,(,),<zmienna>::=
<identyfikator>,<stała>::=<liczbacałkowita>,W::=<zmienna>|<stała>|-W|W+W|W-W|W*W|WW|(W),W,


gdzie identyfikator to Id z poprzedniej gramatyki, a liczba całkowita, to S z naszej gramatyki definiującej liczby całkowite.

Niestety ta gramatyka ma kilka wad. Po pierwsze, nie jest jednoznaczna. Po drugie, nie można określić dla niej semantyki – ta wada jest konsekwencją niejednoznaczności. Po trzecie, dopuszcza wytworzenie dziwolągów typu czy i w żaden sposób nie oddaje właściwego związku z tradycją matematyczną.

O co nam zatem chodzi? Oto lista naszych wymagań:

1. Gramatyka wyrażeń powinna być jednoznaczna.
2. Wartości wyrażeń powinny być zgodne z tradycją matematyczną, w szczególności należy zachować
   priorytety operatorów oraz wykluczyć napisy typu .
3. Nawiasy powinny wymusić dowolną kolejność, na której nam zależy. 
4. Operacje niełączne, takie jak odejmowanie i dzielenie powinny być lewostronnie łączne, 
   czyli ich wykonywanie musi być od lewej do prawej (w szczególności np.    
   powinno dać wynik , a nie ).

Rozwiązanie jest zaskakująco proste jak na wyrafinowanie naszych wymagań. Oto szukana gramatyka wyrażeń całkowitoliczbowych.

Wyrażenia całkowitoliczbowe


=<wyrażenie Z>,<składnik>,<czynnik>,<stała>,<zmienna>  Parser nie mógł rozpoznać (błąd składni): {\displaystyle \{\}
_,a,b,...,z,0,1,...,9,-,+,Parser nie mógł rozpoznać (błąd składni): {\displaystyle \div\}
,Parser nie mógł rozpoznać (błąd składni): {\displaystyle \bmod\}
,(,), 
P,<wyrażenie Z>

gdzie jest zbiorem następujących produkcji:

<wyrażenie Z> ::= <składnik> | -<składnik> | <wyrażenie Z> + <składnik> | <wyrażenie Z> -<składnik>
<składnik> ::= <czynnik> | <składnik> * <czynnik> | <składnik>div<czynnik> | <składnik>mod<czynnik> 
<czynnik> ::= <stała> | <zmienna> | (<wyrażenie Z>)
<stała>::= <liczba całkowita bez znaku>
<zmienna> ::= <identyfikator>

Symbol pomocniczy (liczba całkowita bez znaku) jest uproszczeniem gramatyki liczb polegającym na usunięciu znaku.

Ćwiczenie

Podaj gramatykę dla liczb całkowitych bez znaku

Wskazówka

{{{3}}}

Odpowiedź

{{{2}}}

Zbadajmy teraz, jak wygląda drzewo wywodu wyrażenia . Łatwo się można przekonać, że pierwszą produkcją musi być <wyrażenie Z> ::= <wyrażenie Z> + <składnik>. Żadna inna produkcja nie wchodzi w grę: dwie z nich wprowadzają znak minus, którego byśmy się już nie pozbyli, a trzecia <wyrażenie Z> ::= <składnik> przenosi nas do składnika, z którego nie daje się już uzyskać plusa bez wprowadzenia nawiasów.

Skoro tak, dalej dostajemy już w jednoznaczny sposób następujące drzewo wywodu.

\begin{rysunek}[Drzewo wywodu dla wyrażenia ] \end{rysunek}

Jakie reguły semantyczne należy dobrać do naszych produkcji? Dość naturalne. Dla stałych nie ma problemu: już je mamy. Ze zmiennymi jest jednak mały kłopot: jaką wartość powinny mieć zmienne? Musimy tu zrobić dygresję na temat implementacji. Wszystkie zmienne w programie w momencie, w którym się z nich korzysta, mają swoje miejsce w pamięci. W czasie kompilacji i konsolidacji programu ustala się adres, pod którym można je znaleźć i kiedy odwołujemy się do nich, ich wartość uzyskujemy zaglądając do tego miejsca w pamięci. Przyjmijmy, że pamięć w naszym ujęciu modeluje funkcja , która zbiór wszystkich identyfikatorów przeprowadza w zbiór nieco rozszerzony liczb całkowitych , gdzie symbol oznacza wartość nieokreśloną. Ta wartość jest nam potrzebna, gdyż wszystkie zmienne w chwili, gdy zaczynamy wykonywać program, mają wartości nieokreślone i dopiero w efekcie wykonywania instrukcji konkretne wartości będą im sukcesywnie nadawane (i zmieniane). Przy okazji mała dygresja. W odróżnieniu od całej matematyki, zmienne naprawdę zmieniają swoje wartości. Gdy w matematyce zaczynamy wywód mówiąc np. „Niech będzie liczbą przekątnych w wielokącie”, wówczas do końca wywodu nie może oznaczać niczego innego. W informatyce, jeśli napiszemy „niech stanie się równe ”, to za chwilę wartość zmiennej może ulec zmianie, jeśli sobie tego zażyczymy.

Zatem przez będziemy rozumieli wartość zmiennej , a wartość wyrażenia, w którym zmienne występują, będzie zależała od tego wartościowania. Interpretację wyrażenia musimy zatem uzależnić od wartościowania . Podamy teraz reguły wartościowania wyrażeń. Tym razem funkcję interpretacji oznaczymy przez , żeby podkreślić zależność interpretacji wyrażenia od wartościowania zmiennych. Musimy jeszcze zauważyć, że jeśli w wyrażeniu występuje dzielenie (niezależnie od tego czy div czy mod), to wynik dzielenia będzie błędny, jeśli dzielnik będzie zerem. Komputer, zmuszony do wykonania takiego dzielenia, odmawia współpracy i generuje błąd. Wygodnie jest, określając semantykę, tę sytuację uwzględnić w dziedzinie semantycznej, dodając wartość error do zestawu wartości całkowitych. Zatem ostatecznie nasza dziedzina liczb całkowitych rozrasta się nam o dwie wartości: i . Rozszerzmy jeszcze działania na te wartości. Przyjmujemy więc, że dla każdej operacji dwuargumentowej

  • dla każdego
  • dla każdego

i analogicznie dla operacji jednoargumentowych.

Reguły semantyczne

  • dla symboli końcowych określamy (jak w ćwiczeniu stałe bez znaku)
  • dla produkcji określamy
  • dla produkcji określamy
  • dla produkcji określamy
  • dla produkcji określamy
  • dla produkcji określamy
  • dla produkcji określamy
  • dla produkcji określamy
Parser nie mógł rozpoznać (nieznana funkcja „\textcolor”): {\displaystyle I_V(<skladnik>) = \begin{cases} I_V(<skladnik'>\textcolor{blue}{\div} I_V(<czynnik>) & \mbox{jeśli }I_V(<czynnik>)\neq0 \\ error & \mbox{jeśli }I_V(<czynnik>)=0 \end{cases}}
  • dla produkcji określamy
Parser nie mógł rozpoznać (nieznana funkcja „\textcolor”): {\displaystyle I_V(<skladnik>) = \begin{cases} I_V(<skladnik'> \textcolor{blue}{\bmod} I_V(<czynnik>) & \mbox{jeśli }I_V(<czynnik>)\neq0 \\ error & \mbox{jeśli }I_V(<czynnik>)=0 \end{cases}}


  • dla produkcji określamy
  • dla produkcji określamy Parser nie mógł rozpoznać (nieznana funkcja „\textcolor”): {\displaystyle I_V(<wyrazenie Z>)=\textcolor{blue}{-}I_V(<skladnik>)}
  • dla produkcji określamy Parser nie mógł rozpoznać (nieznana funkcja „\textcolor”): {\displaystyle I_V(<wyrazenie Z>)=I_V(<wyrazenie Z'>)\textcolor{blue}{+} I_V(<skladnik>)}
  • dla produkcji określamy Parser nie mógł rozpoznać (nieznana funkcja „\textcolor”): {\displaystyle I_V(<wyrazenie Z>)=I_V(<wyrazenie Z'>)\textcolor{blue}{-} I_V(<skladnik>)}

Ćwiczenie

Uff! Wiemy już więc, jak interpretować wyrażenia arytmetyczne. A teraz parę pytań sprawdzających.

Ćwiczenie

Ile to jest dla

Odpowiedź

{{{2}}}

Odpowiedź

{{{2}}}

Odpowiedź

{{{2}}}

Odpowiedź

{{{2}}}