Struktury danych i obliczenia w Prologu. Wykonali: Dzięgielewski Patryk Golacik Tomasz Rzepecki Michał Zawadzki Marcin

Podobne dokumenty
Prolog struktury danych oraz obliczenia. 1. Arytmetyka?- Y is 2+2. Y = 4. ?- 5 is 3+3. false. ?- Z is (3.9 / 2.1). Z =

Prolog 2 (Filip Wroński, Łukasz Betkowski, Paweł Świerblewski, Konrad Kosmatka)

Programowanie w logice

Programowanie strukturalne. Opis ogólny programu w Turbo Pascalu

Programowanie w logice

Definicje wyższego poziomu

Programowanie w Logice

Podstawy programowania 2. Temat: Funkcje i procedury rekurencyjne. Przygotował: mgr inż. Tomasz Michno

PROLOG INNE PRZYKŁADY MACIEJ KELM

Pascal - wprowadzenie

I. Podstawy języka C powtórka

4. Funkcje. Przykłady

REKURENCJA W JĘZYKU HASKELL. Autor: Walczak Michał

Cw.12 JAVAScript w dokumentach HTML

JAVAScript w dokumentach HTML (1) JavaScript jest to interpretowany, zorientowany obiektowo, skryptowy język programowania.

Widoczność zmiennych Czy wartości każdej zmiennej można zmieniać w dowolnym miejscu kodu? Czy można zadeklarować dwie zmienne o takich samych nazwach?

Luty 2001 Algorytmy (7) 2000/2001

znajdowały się różne instrukcje) to tak naprawdę definicja funkcji main.

Wskaźniki a tablice Wskaźniki i tablice są ze sobą w języku C++ ściśle związane. Aby się o tym przekonać wykonajmy cwiczenie.

Arytmetyka liczb binarnych

Języki programowania deklaratywnego

Algorytmy i struktury danych. Wykład 4

Programowanie w logice Prolog 2

1. Wypisywanie danych

JAVAScript w dokumentach HTML (1)

Instrukcja do ćwiczeń nr 4 typy i rodzaje zmiennych w języku C dla AVR, oraz ich deklarowanie, oraz podstawowe operatory

Algorytm. a programowanie -

Projekt współfinansowany przez Unię Europejską w ramach Europejskiego Funduszu Społecznego

PASCAL. Etapy pisania programu. Analiza potrzeb i wymagań (treści zadania) Opracowanie algorytmu Kodowanie Kompilacja Testowanie Stosowanie

Liczby losowe i pętla while w języku Python

Instrukcje warunkowe i skoku. Spotkanie 2. Wyrażenia i operatory logiczne. Instrukcje warunkowe: if else, switch.

Języki i techniki programowania Ćwiczenia 2

lekcja 8a Gry komputerowe MasterMind

Po uruchomieniu programu nasza litera zostanie wyświetlona na ekranie

Pętla for. Wynik działania programu:

DZIAŁANIA NA UŁAMKACH DZIESIĘTNYCH.

Zmienne, stałe i operatory

Pascal typy danych. Typy pascalowe. Zmienna i typ. Podział typów danych:

Programowanie komputerowe. Zajęcia 1

1. Operacje logiczne A B A OR B

Podstawy programowania. Wykład Funkcje. Krzysztof Banaś Podstawy programowania 1

Projekt 4: Programowanie w logice

do instrukcja while (wyrażenie);

12.Rozwiązywanie równań i nierówności liniowych oraz ich układów.

Operatory AND, OR, NOT, XOR Opracował: Andrzej Nowak Bibliografia:

Podstawy programowania. Wykład: 4. Instrukcje sterujące, operatory. dr Artur Bartoszewski -Podstawy programowania, sem 1 - WYKŁAD

Definicje. Algorytm to:

Programowanie. programowania. Klasa 3 Lekcja 9 PASCAL & C++

Informatyka I. Typy danych. Operacje arytmetyczne. Konwersje typów. Zmienne. Wczytywanie danych z klawiatury. dr hab. inż. Andrzej Czerepicki

Wykład 11a. Składnia języka Klasycznego Rachunku Predykatów. Języki pierwszego rzędu.

Zadanie 1. Potęgi (14 pkt)

Samodzielnie wykonaj następujące operacje: 13 / 2 = 30 / 5 = 73 / 15 = 15 / 23 = 13 % 2 = 30 % 5 = 73 % 15 = 15 % 23 =

Podstawy Programowania

wagi cyfry pozycje

Niezwykłe tablice Poznane typy danych pozwalają przechowywać pojedyncze liczby. Dzięki tablicom zgromadzimy wiele wartości w jednym miejscu.

Informacja o języku. Osadzanie skryptów. Instrukcje, komentarze, zmienne, typy, stałe. Operatory. Struktury kontrolne. Tablice.

Podstawowe operacje arytmetyczne i logiczne dla liczb binarnych

Arytmetyka komputera. Na podstawie podręcznika Urządzenia techniki komputerowej Tomasza Marciniuka. Opracował: Kamil Kowalski klasa III TI

Kurs ZDAJ MATURĘ Z MATEMATYKI MODUŁ 2 Teoria liczby rzeczywiste cz.2

Programowanie w logice

Wstęp do Informatyki zadania ze złożoności obliczeniowej z rozwiązaniami

Programowanie w Logice Struktury danych (Lista 2)

Wstęp do programowania. Drzewa podstawowe techniki. Piotr Chrząstowski-Wachtel

Nazwa implementacji: Nauka języka Python wyrażenia warunkowe. Autor: Piotr Fiorek. Opis implementacji: Poznanie wyrażeń warunkowych if elif - else.

1 Podstawy c++ w pigułce.

Programowanie w Baltie klasa VII

WHILE (wyrażenie) instrukcja;

3. Macierze i Układy Równań Liniowych

Nazwa implementacji: Nauka języka Python pętla for. Autor: Piotr Fiorek

Schematy blokowe I. 1. Dostępne bloki: 2. Prosty program drukujący tekst.

METODY KOMPUTEROWE W OBLICZENIACH INŻYNIERSKICH

Elżbieta Kula - wprowadzenie do Turbo Pascala i algorytmiki

Laboratorium Wstawianie skryptu na stroną: 2. Komentarze: 3. Deklaracja zmiennych

Wstęp do informatyki- wykład 2

JAVAScript w dokumentach HTML - przypomnienie

Języki programowania zasady ich tworzenia

WYRAŻENIA ALGEBRAICZNE

Metody numeryczne Laboratorium 2

Temat 1: Podstawowe pojęcia: program, kompilacja, kod

Celem ćwiczenia jest zapoznanie się z podstawowymi możliwościami języka Prolog w zakresie definiowania faktów i reguł oraz wykonywania zapytań.

Pliki. Operacje na plikach w Pascalu

PoniŜej znajdują się pytania z egzaminów zawodowych teoretycznych. Jest to materiał poglądowy.

Lista 0. Kamil Matuszewski 1 marca 2016

B.B. 2. Sumowanie rozpoczynamy od ostatniej kolumny. Sumujemy cyfry w kolumnie zgodnie z podaną tabelką zapisując wynik pod kreską:

Wstęp do programowania. Różne różności

Kod U2 Opracował: Andrzej Nowak

Powtórka algorytmów. Wprowadzenie do języka Java.

Wstęp do programowania INP003203L rok akademicki 2018/19 semestr zimowy. Laboratorium 2. Karol Tarnowski A-1 p.

Myśl w języku Python! : nauka programowania / Allen B. Downey. Gliwice, cop Spis treści

Programowanie w języku Python. Grażyna Koba

Operacje wykonywane są na operandach (argumentach operatorów). Przy operacji dodawania: argumentami operatora dodawania + są dwa operandy 2 i 5.

W dowolnym momencie można zmienić typ wskaźnika.

Obiekt klasy jest definiowany poprzez jej składniki. Składnikami są różne zmienne oraz funkcje. Składniki opisują rzeczywisty stan obiektu.

Podstawy programowania. 1. Operacje arytmetyczne Operacja arytmetyczna jest opisywana za pomocą znaku operacji i jednego lub dwóch wyrażeń.

Podstawy programowania skrót z wykładów:

x 2 = a RÓWNANIA KWADRATOWE 1. Wprowadzenie do równań kwadratowych 2. Proste równania kwadratowe Równanie kwadratowe typu:

Bash - wprowadzenie. Bash - wprowadzenie 1/39

Zapisywanie algorytmów w języku programowania

Transkrypt:

Struktury danych i obliczenia w Prologu Wykonali: Dzięgielewski Patryk Golacik Tomasz Rzepecki Michał Zawadzki Marcin

Arytmetyka Przykłady obliczeń w Prologu:?- Y is 2+2. Y=4?- 5 is 3-3. false?- Z is 4.5 + (3.9 / 2.1). Z = 6.3571428

Wbudowany predykat is bierze wyrażenie arytmetyczne z jego prawej strony, oblicza je i unifikuje je z wyrażeniem po jego lewej stronie. Wyrażenia w Prologu są bardzo podobne do tych z innych języków programowania. Najprostsze wyrażenie składa się z samej liczby: Cos is 2. Operatory infix: + dodawanie - odejmowanie * mnożenie / dzielenie typu floating-point // dzielenie typu integer mod modulo Funkcje: abs() sqrt() log() exp() wartość bezwzględna pierwiastek kwadratowy logarytm antylogarytm floor() największa wartość typu integer mniejsza lub równa argumentowi round() najbliższa wartość typu integer

Kolejność wykonywania działań jest taka sama jak w innych językach programowanie i matematyce. Prolog obsługuje wartości typu floating-point oraz integer i przekształca je w razie potrzeby. Warto zauważyć, że Prolog nie potrafi rozwiązywać równań matematycznych;?- 5 is 2 + Cos. instantiation error może być to zaskakujące, ze względu na fakt, że Prolog potrafi znaleźć niewiadomą w wyrażeniu ojciec(michal, Kto), ale nie w 5 is 2 + Cos. Po namyśle można jednak zauważyć, że pierwsze zapytanie może zostać rozwiązane poprzez podstawienie wszystkich z możliwych zależności, natomiast drugie zapytanie nie może być rozwiązane w ten sposób, ponieważ, dla is nie ma zdefiniowany zależności, a próba znalezienia rozwiązania oznaczała by przeszukiwanie nieskończonej liczby potencjalnych liczb w kilku wymiarach. Jedynymi metodami rozwiązania byłaby manipulacja algebraiczna(5 2 = Cos) lub numeryczna(omówiona w rozdziale 7).

Konstruowanie wyrażeń Dużą różnicą między Prologiem, a innymi językami programowania jest to, że inne języki wyliczają wyrażenia arytmetyczne, kiedykolwiek wystąpią, zaś Prolog wylicza je tylko w konkretnych miejscach. Dla przykładu; 2+2 jest wyliczane w 4 tylko wtedy gdy jest argumentem predykatu arytmetycznego, w innych przypadkach jest to tylko jakaś struktura danych składająca się z 2, + i 2: is bierze wyrażenie po prawej stronie, wylicza je i unifikuje rezultat z argumentem po lewej stronie, =:= wylicza dwa wyrażenia i porównuje wyniki, = unifikuje dwa wyrażenia, bez ich wyliczania, Zatem:?- Cos is 2+3. Cos = 5?- 4+1 =:= 2+3. true?- Cos = 2+3. Cos = 2+3

Inne porównania(<, >, =<, >=) działają tak jak =:= R is Wyr wylicza Wyr i unifikuje z R Wyr1 =:= Wyr2 - prawda jeśli wyniki obu wyrażeń są równe, Wyr1 =\= Wyr2 - prawda jeśli wyniki obu wyrażeń nie są równe, Wyr1 > Wyr2 Wyr1 < Wyr2 Wyr1 >= Wyr2 Wyr1 =< Wyr2 Uwaga: =< i >=, nie <= i =>.

Należy zauważyć, że predykaty porównawcze wymagają konkretnych argumentów; nie możemy powiedzieć: Podaj mi liczbę mniejszą od 20, ponieważ takie zapytanie miałoby nieskończoną liczbę możliwych odpowiedzi. Uwaga: Liczba zmienno-przecinkowa uzyskana w obliczeniach komputerowych prawie nigdy nie jest dokładnie równa jakiejkolwiek innej liczbie zmienno-przecinkowej, nawet jeśli obie wyglądają tak samo na wyjściu. Dzieje się tak, ponieważ komputery liczą w systemie binarnym, a my piszemy liczby w systemie dziesiętnym. Wiele dziesiętnych liczb, jak 0.1 nie ma binarnej reprezentacji ze skończoną liczbą znaków. (Wyrażanie 1/10 w notacji binarnej jest jak wyrażanie 1/3 lub 1/7 w notacji dziesiętnej znaki po prawej stronie kropki powtarzają się w nieskończoność.) W wyniku tego liczby zmienno-przecinkowe są przedmiotem błędów zaokrągleń.

Należy zauważyć, że predykaty porównawcze wymagają konkretnych argumentów; nie możemy powiedzieć: Podaj mi liczbę mniejszą od 20, ponieważ takie zapytanie miałoby nieskończoną liczbę możliwych odpowiedzi. Uwaga: Liczba zmienno-przecinkowa uzyskana w obliczeniach komputerowych prawie nigdy nie jest dokładnie równa jakiejkolwiek innej liczbie zmienno-przecinkowej, nawet jeśli obie wyglądają tak samo na wyjściu. Dzieje się tak, ponieważ komputery liczą w systemie binarnym, a my piszemy liczby w systemie dziesiętnym. Wiele dziesiętnych liczb, jak 0.1 nie ma binarnej reprezentacji ze skończoną liczbą znaków. (Wyrażanie 1/10 w notacji binarnej jest jak wyrażanie 1/3 lub 1/7 w notacji dziesiętnej znaki po prawej stronie kropki powtarzają się w nieskończoność.) W wyniku tego liczby zmienno-przecinkowe są przedmiotem błędów zaokrągleń.

Ćwiczenie: Wyjaśnij które z podanych zapytań zadziała, nie zadziała lub zwróci błąd, oraz dlaczego:?- 5 is 2+3.?- 5 =:= 2+3.?- 5 = 2+3.?- 4+1 is 2+3.?- 4+1 =:=2+3.?- 4+1 =:= 5.?- Cos is 2+3.?- Cos =:= 2+3.?- Cos is 5.?- Cos =5.

Praktyczne obliczenia Uważny czytelnik zauważy, że używając wyrażeń w Prologu, mieszamy dwa style kodowania. Z logicznego punktu widzenia suma i produkt to zależności między liczbami, tak jak ojciec i matka są zależnościami między ludźmi, więc zamiast?- Cos is 2 + 3*4 + 5 powinniśmy pisać?- produkt(3,4,p), suma(2,p,s), suma(s,5,cos) I rzeczywiście wczesne wersje Prologa tak działały, ale te stare podejście ma dwa problemy; jest niepraktycznie i daje wrażenie, że Prolog posiada strategię wyszukiwania dla liczb, co nie jest prawdą. Używamy więc wyrażeń.

Jeśli chcemy implementować numeryczne algorytmy, musimy zdefiniować własne predykaty. Dla przykłady zdefiniujmy predykat close_enough/2 który zadziała dla dwóch liczb różniących się o 0.0001. To pozwoli nam porównać liczby zmienno-przecinkowe bez bycia zmylonymi przez błędy zaokrągleń. close_enough(x,x) :-!. close_enough(x,y) :- X<Y, Y-X< 0.0001. close_enough(x,y) :- X>Y, close_enough(y,x). Pierwsza klauzula obsługuje przypadek gdy dwa argumenty, jakimś cudem, rzeczywiście są równe, wykrzyknik zapewnia, że jeśli pierwsza klauzula się wykona, dwie następne nie będą uruchomione. Druga klauzula to serce obliczeń; porównaj X i Y, odejmij mniejszą od większej, i sprawdź czy różnica jest mniejsza od 0.0001. Trzecia klauzula zajmuje się argumentami w przeciwnej kolejności zamienia je i wywołuje samą siebie ponownie.

Teraz wykonamy obliczenia, następujący predykat inicjalizuje Y wartością real square root X jeśli istnieje lub atomem nonexistent jeśli nie; real_square_root(x,nonexistent) :- X < 0.0. real_square_root(x,y) :- X >= 0.0, Y is sqrt(x). Przykład użycia:?- real_square_root(9.0,root). Root =3.0?- real_square_root(-1.0,root). Root =nonexistent Należy zauważyć, że zapytanie real_square_root(121.0,11.0) prawdopodobnie zawiedzie, ponieważ 11.0 nie równa się dokładnie zmienno-przecinkowemu wynikowi sqrt pomimo, że pierwiastek ze 121 wynosi dokładnie 11. Możemy to naprawić wykorzystując nasz predykat close_enough. Wymaga to następującej redefinicji real_square_root

real_square_root(x,nonexistent) :- X < 0.0. real_square_root(x,y) :- X >= 0.0, R is sqrt(x), close_enough(r,y). Teraz mamy nasz pożądany rezultat:?- real_square_root(121.0, 11.0) true Wreszcie, wykorzystajmy zdolność Prologa do zwracania alternatywnych odpowiedzi, każda dodatnia liczba rzeczywista ma dwa pierwiastki kwadratowe, jeden dodatni i jeden ujemny.

Potrzebujemy dodatkowych klauzul dla alternatyw, ponieważ arytmetyka w Prologu jest całkowicie deterministyczna; real_square_root(x,y) :- X > 0.0, R is -sqrt(x), close_enough(r,y). To daje nam alternatywną metodę znajdowania pierwiastka. Teraz każde wywołanie zakończone sukcesem zwróci dwa wyniki:?- real_square_root(9.0,root). Root =3.0 Root =-3.0

Testowanie dla inicjalizacji Jak dotąd real_square_root wymaga aby pierwszy argument był zainicjalizowany, ale z drobnymi zmianami może zostać wyposażony w wymienność niewiadomych. Używając X i Y nasza strategia wygląda tak: Jeśli X jest znane unifikuj Y z pierwiastkiem z X lub ujemnym pierwiastkiem z X(alternatywne rozwiązania). Jeśli Y jest znane, unifikuj X z Y*Y. Aby to osiągnąć musimy sprawdzić czy X i Y są zainicjalizowane. Prolog dostarcza dwa predykaty w tym celu: var który zwraca prawdę jeśli argument jest niezainicjalizowany oraz nonvar, który zwraca prawdę, jeśli argument ma wartość. Możemy zatem udoskonalić real_square_root;

real_square_root(x,nonexistent) :- nonvar(x), X < 0.0. real_square_root(x,y) :- nonvar(x), X >= 0.0, R is sqrt(x), close_enough(r,y). real_square_root(x,y) :- nonvar(x), X > 0.0, R is -sqrt(x), close_enough(r,y). real_square_root(x,y) :- nonvar(y), Ysquared is Y*Y, close_enough(ysquared,x). Tutaj klauzula 4 zapewnia metodę wyliczenia X z Y, a użycie nonvar zapewnia, że właściwa klauzula zostanie wybrana i nie będziemy próbować liczyć lub porównywać niezainicjalizowanych zmiennych.

Listy Jedną z najważniejszych struktur danych Prologa jest lista. Lista to posortowana sekwencja zera lub więcej termów wypisanych w nawiasach kwadratowych, oddzielonych kropkami: [alpha,beta,gamma,delta] [1,2,3,go] [(2+2),in(austin,texas),-4.356,X] [[a,list,within],a,list] Elementy listy mogą być termami dowolnego typu, włączając w to inne listy. Pusta lista to []. Warto zauważyć, że jednoelementowa lista [a] nie jest równa atomowi a. Listy mogą być konstruowane bądź rozkładane poprzez unifikacje. Cała lista może oczywiście równać się jednej zmiennej. Unify With Result [a,b,c] X X=[a,b,c]

Także, odpowiadające element dwóch list mogą być unifikowane jedna do drugiej Unify With Result [X,Y,Z] [a,b,c] X=a,Y=b, Z=c [X,b,Z] [a,y,c] X=a,Y=b, Z=c Ta zasada odnosi się także do list lub struktur zagnieżdżonych w listach Unify With Result [[a,b],c] [X,Y] X=[a,b], Y=c [a(b),c(x)] [Z,c(a)] X=a, Z=a(b) Co ważniejsze jakakolwiek lista może zostać podzielona na głowę i ogon poprzez symbol. Głową listy jest pierwszy element, zaś ogonem lista pozostałych elementów(może być pusta). Każda nie pusta lista ma głowę i ogon; [a [b,c,d]] = [a,b,c,d] [a []] = [a]

Term [X Y] unifikuje z jakąkolwiek nie pustą listą, inicjalizując X do głowy i Y do ogona: Unify With Result [X Y] [a,b,c,d] X=a, Y=[b,c,d] [X Y] [a] X=a, Y=[] Jak dotąd jest jak rozróżnienie CAR-CDR w Lispie, ale w przeciwieństwie do CAR i CDR, może wziąć pod uwagę więcej niż jeden początkowy element w pojedynczym kroku: [a,b,c [d,e,f]] = [a,b,c,d,e,f]

Co naprawdę przydaję się w unifikacji: Unify With Result [X,Y Z] [a,b,c] X=a, Y=b, Z=[c] [X,Y Z] [a,b,c,d] X=a, Y=b, Z=[c,d] [X,Y,Z A] [a,b,c] X=a, Y=b, Z=c, A=[] [X,Y,Z A] [a,b] fails [X,Y,a] [Z,b,Z] X=Z=a, Y=b [X,Y Z] [a W] X=a, W=[Y Z] Proces konstrukcji i rozkładania list jest wykonywany głównie poprzez unifikację, nie przez procedury. To znaczy, że serce procedury przetwarzającej listę jest często w notacji, która opisuje strukturę argumentów. Aby się do tej notacji przyzwyczaić, zdefiniujmy przetwarzający predykat: third_element([a,b,c Rest],C).

Osiąga on sukces, jeśli pierwszy argument jest listą, a drugi jest trzecim elementem listy. Posiada kompletną wymienność niewiadomych, więc:?- third_element([a,b,c,d,e,f],x). X= c?- third_element([a,b,y,d,e,f],c). Y= c?- third_element(x,a). X= [_0001,_0002,a _0003] W ostatnim użyciu predykatu, komputer nie wie nic o X poza tym, że jest lista, której trzecim elementem jest, więc tworzy listę z niezainicjalizowanym 1 i 2 elementem, następnie a, a po nim niezainicjalizowany ogon.

Przechowywanie danych w listach Listy mogą przechowywać dane podobnie jak rekordy w w COBOL-u bądź Pascalu. Dla przykładu; ['Michael Covington', '285 Saint George Drive', 'Athens', 'Georgia', '30606'] Jest to rozsądna metoda przedstawienia adresu, z polami dla imienia, ulicy, miasta, stanu oraz adresu pocztowego. Procedury takie jak third_element mogą wydobywać bądź wprowadzać dane do takiej listy.

Zasadniczą różnicą jest fakt, że pomiędzy listą, a rekordem danych jest fakt, że liczba elementów listy nie musi być z góry zadeklarowana. Kolejną różnicą jest to, że elementy listy nie muszą być żadnego konkretnego typu. Atomy, struktury i liczby mogą być dowolnie wykorzystywane w jakiejkolwiek kombinacji. Co więcej, lista może zawierać inną listę jako jeden ze swoich elementów. ['Michael Covington', [['B.A',1977], ['M.Phil.',1978], ['Ph.D.',1982]], 'Associate Research Scientist', 'University of Georgia'] W tym przykładzie główna lista posiada cztery elementy: imię, listę tytułów naukowych, stanowisko w pracy, pracodawca. Lista stopni naukowych posiada 3 elementy z których każdy jest dwu-elementową listą zawierającą stopień i datę. Zauważmy, że liczba stopni naukowych na osobę nie jest ograniczona; ta sama struktura może obsłużyć osobę bez żadnych luz z kilkunastoma.

Listy w Prologu mogą posłużyć jako zamiennik dla tablic z innych języków programowania. Np. macierz liczb może być przedstawiona jako lista list: [[1,2,3], [4,5,6], [7,8,9]] Istnieje jednak ważna różnica, w tablicy każdy element ma taki sam czas dostępu, pracując na liście komputer musi zawsze zaczynać od początku i przechodzić element po elemencie.

Rekurencja Aby całkowicie wykorzystać potencjał list, potrzebujemy metody pracowania z elementami listy, bez potrzeby określania ich pozycji z wyprzedzeniem. Rozwiązaniem jest rekurencja, czyli metoda kiedy procedura wywołuje samą siebie. Zdefiniujmy predykat member(x,y) który zwraca prawdę, jeśli X jest elementem tablicy Y. Nie wiemy z góry ile elementów posiada lista Y, nie możemy więc użyć skończonej liczby predeterminowanych pozycji, musimy kontynuować sprawdzanie, dopóki nie znajdziemy X lub elementy Y nie skończą się.

Zanim zajmiemy się samą rekurencją, pomyślmy nad dwoma specjalnymi przypadkami które nie są powtarzalne: 1) Jeśli Y jest puste, zwróć fałsz bez dalszych czynności, ponieważ nic nie jest elementem pustej listy, 2) Jeśli X jest pierwszym elementem Y zwróć prawdę bez dalszych czynności. Z pierwszą sytuacją uporamy się upewniając się, że we wszystkich naszych klauzulach, drugi argument jest czymś co nie unifikuje się z pustą listą, pusta lista nie ma ogona, więc możemy wykluczyć puste listy pozwalając drugiemu argumentowi być listą, która ma zarówno głowę jak i ogon. member(x,[x _]). Następnie rekurencyjna część: member(x,[_ Ytail]) :- member(x,ytail). Przykładowe uzycie:?- member(c,[a,b,c]).

To nie pasuje do klauzuli 1, więc program kontynuuje do drugiej, ta z kolei generuje nowe zapytanie:?- member(c,[b,c]). Ponownie, klauzula 1 nie pasuje, ale klauzula 2 już tak i generuje nowe zapytanie:?- member(c,[c]). Tym razem zadziała klauzula 1, ponieważ [c] jest równoznaczne z [c []], zapytanie kończy się sukcesem, zgodnie z oczekiwaniami.

Liczenie elementów listy Oto rekurencyjny algorytm do liczenia elementów listy: 1) Jeśli lista jest pusta, to ma 0 elementów, 2) W innym wypadku, pomiń pierwszy element, policz liczbę pozostałych elementów i dodaj 1. Druga z tych klauzul jest rekurencyjna ponieważ, w celu policzenia elementów listy, musisz policzyć elementy innej, mniejszej liczby. Algorytm zapisany w Prologu ma postać: list_length([],0). list_length([_ Tail],K) :- list_length(tail,j), K is J + 1.

Rekurencja kończy się ponieważ lista w końcu stanie się pusta po usuwaniu kolejnych elementów. Przykład użycia:?- list_length([a,b,c],k0).?- list_length([b,c],k1).?- list_length([c],k2).?-listl_ength([],0).?- K2 is 0+1.?- K1 is 1+1.?- K0 is 2+1.

Łączenie list Co jeśli chcielibyśmy połączyć dwie listy? Np. [a,b,c] z [d,e,f], aby uzyskać [a,b,c,d,e,f]. nie spełni zadania; [[a,b,c] [d,e,f]] daje [[a,b,c],d,e,f], a to nie to czego oczekujemy. Będziemy musieli przejść po pierwszej liście element po elemencie, dodając je do drugiej listy. Na początek zajmijmy się warunkiem ograniczającym, gdyż lista ewentualnie zostanie pusta. append([],x,x). Rekurencyjna klauzula jest mniej intuicyjna, ale bardzo zwięzła: append([x1 X2],Y,[X1 Z]) :- append(x2,y,z). Pierwszy element wynikowy jest taki sam jak pierwszy elemente pierwszej listy. Ogon wyniku jest uzyskany poprzez dołączenie ogona pierwszej listy do całej drugiej listy. Bardziej proceduralnie: Weź pierwszy element pierwszej listy(nazwij X1) Rekurencyjnie dołącz ogon pierwszej listy do całej drugiej listy. Nazwij rezultat Z. Dodaj X1 do początku Z.

Z powodu deklaratywnej natury appena jest całkowicie wymienna dla niewiadomych?- append([a,b,c],[d,e,f],x). X= [a,b,c,d,e,f]?- append([a,b,c],x,[a,b,c,d,e,f]). X= [d,e,f]?- append(x,[d,e,f],[a,b,c,d,e,f]). X= [a,b,c] Każde z tych jest deterministyczne istnieje tylko jedno możliwe rozwiązanie, ale jeśli zostawimy dwa pierwsze argumenty niezainicjalizowane, dostaniemy, jako alternatywne rozwiązania, wszystkie możliwe sposoby podzielenia drugiej argumentu na dwie podlisty:?- append(x,y,[a,b,c,d]). X=[] Y=[a,b,c,d] X=[a] Y=[b,c,d] X=[a,b] Y=[c,d] X=[a,b,c] Y=[d] X=[a,b,c,d] Y=[]

Rekurencyjne odwracanie list Klasyczny algorytm rekurencyjny dla odwrócenia elementów na liście: Podziel oryginalną listę na head(głowę) i tail(ogon). Rekurencyjnie odwrócić tail(ogon) oryginalnej listy. Stworzyć liste której jedyne elementy są head(głową) oryginalnej listy. Powiązać odwrócony tail(ogon) oryginalnej listy z listą stworzoną w kroku 3. Ponieważ lista staje się krótsza za każdym razem, ograniczający przypadkiem jest pusta lista, którą chcemy zwrócić nie zmienioną. W Prologu: reverse([],[]). % Klauzula 1 reverse([head Tail], Result) :- % Klauzula 2 reverse(tail,reversedtail), append(reversedtail,[head],result).

Jest to tłumaczenie klasycznego algorytmu odwracania listy Lisp-a (rodzina języków programowania) znany jako naive reversal albo NREV i często używany do testowanie prędkości realizacji Lisp-a i Prolog-a. Jego naiwność polega na jego wielkiej nieskuteczności. Można pomyśleć ze 8 elementowa lista możliwa jest do odwrócenia w 8-9 krokach. Z tym algorytmem jednak odwrócenie 8 elementowej listy potrzebuje 45 kroków 9 wezwań do odwrócenia, a następnie 36 wezwań do dołączenia. Jedną z rzeczy którą można powiedzieć na korzyść algorytmu jest to, że lubi wymienność niewiadomych przynajmniej przy pierwszym rozwiązaniu każdego pytania. Lecz jeżeli pierwszy argument jest niezainicjowany, drugi argument jest listą i prosimy o więcej niż jedno rozwiązanie zaczynają się dziać różne dziwne rzeczy. Ćwiczenie Wstawiając kilka writes i nls, spraw by reverse wyświetliło argumenty każdego wezwania do siebie i każde wezwanie do dodania. Następnie spróbuj pytania reverse(what, [a,b,c]), poproś o alternatywne rozwiązania i zobacz co się stanie. Pokaż swoją zmodyfikowaną wersję reverse i jego wynik.

Szybszy sposób na odwracanie list Oto algorytm który odwraca listy o wiele szybciej ale kosztem możliwości wymiany niewiadomych. fast_reverse(original,result) :- nonvar(original), fast_reverse_aux(original,[],result). fast_reverse_aux([head Tail],Stack,Result) :- fast_reverse_aux(tail,[head Stack],Result). fast_reverse_aux([],result,result). Pierwsza klauzula sprawdza czy oryginalna lista rzeczywiście jest zainicjowana, następnie wywołuje trzy argumentową procedurę nazywaną fast_reverse_aux. Ideą jest przesuniecie elementów jeden po drugim, podnosząc je z początku oryginalnej listy i dodawanie ich do nowej listy służącej jako stos. Nowa lista staje się odwróconą kopią oryginalnej listy. Przez wszystkie wezwania rekurencyjne, Result(wynik) jest niezainicjowany, na końcu zainicjowywujemy go i przekazujemy z powrotem do procedury wezwań.

?- fast_reverse_aux([a,b,c],[],result).?- fast_reverse_aux([b,c],[a],result).?- fast_reverse_aux([c],[b,a],result).?- fast_reverse_aux([],[c,b,a],[c,b,a]). Ten algorytm odwróci n-elementową listę w n+1 krokach. W pierwszej klauzurze załączyliśmy nonvar by fast_reverse zawiodło jeżeli pierwszy argument jest niezainicjowany. Bez tego niezainicjowany argument wprowadził by komputer w niekończące się obliczanie, tworząc dłuższe i dłuższe listy niezainicjowanych zmiennych która żadna nie była by rozwiązaniem. Ćwiczenie Pokaż że fast_reverse działa jak opisaliśmy. Zmodyfikuj by wypisywało argumenty każdego wezwania rekurencyjnego byś mógł zobaczyć co się dzieje z algorytmem.

Ciąg znaków Są trzy sposoby na zaprezentowanie ciągu znaków w Prologu: 1) Jako atom. Atomy są kompaktowe ale ciężkie do rozdzielenia lub manipulowania. 2) Jako lista kodów ASCII. Możesz użyć na nich standardowej listy technik przetwarzania. 3) Jako lista jedno znakowych atomów. Ponownie możesz użyć na nich standardowej listy technik przetwarzania. W Prologu jeśli wpiszesz znak w cuszysłowiu ( tak jak tu ), komputer interpretuje to jako liste kodów ASCII. Tak więc abc i [97,98,99] są takie same w terminologi Prologu. Taki listy kodów ASCII tradycyjnie nazywane są Stringami.

Natychmiastowym problemem jest to że nie ma standardowego sposobu na wyświetlenie znaku, ponieważ write i display wypisują listę numerów:?- write("abc"). [97,98,99] yes Prosta procedura wyświetlania stringów: write_str([head Tail]) :- put(head), write_str(tail). write_str([]). Rekurencja jest prosta do śledzenia. Jeżeli string nie jest pusty (tak więc będzie się zgadzał [Head Tail]) wypisze pierwszą pozycjie i powtórz procedure dla pozostałych pozycji. Kiedy String stanie się pusty, zakończ powodzeniem bez kolejnych akcji. Stringi są listami w każdym sensie słowa i kazda lista technik przetwarzania może być na nich użyta. Tak więc reverse odwróci string-a, append zwiąże lub podzieli string i tak dalej.

Zdefiniuj Prologa przewidź print_split który, gdy otrzyma stringa, wypisze wszystkie możliwe sposoby podziału stringu na dwa tak jak to:?- print_splits("university"). university u niversity un iversity uni versity univ ersity unive rsity univer sity univers ity universi ty universit y university yes

Wprowadzenie linii jako string lub atom Łatwo sprawić by Prolog czytał całą linie wprowadzeń do pojedynczego stringa. Ideą jest unikanie używania read, a zamiast tego używania get 0 do wprowadzania znaków dopóki nie dojdziemy do końca linii. Okazuje się że algorytm potrzebuje jednego znaku LOOKAHEAD- nie może zdecydować co zrobić z każdym znakiem dopóki nie będzie wiedział czy kolejny znak jest znacznikiem końca linii. Wiec oto sposób jak to zrobić % read_str(string) % Accepts a whole line of input as a string (list of ASCII codes). % Assumes that the keyboard is buffered. read_str(string) :- get0(char), read_str_aux(char,string). read_str_aux(-1,[]) :-!. % end of file read_str_aux(10,[]) :-!. % end of line (UNIX) read_str_aux(13,[]) :-!. % end of line (DOS) read_str_aux(char,[char Rest]) :- read_str(rest). Zauważ ze przewidywanie zaczyna się krótkim komentarzem opisującym to. Od teraz taki komentarz będzie naszą standardową praktyką.

Lookahead jest osiągnięty przez przeczytanie jednego znaku, następnie przekazaniem tego znaku do read_str_aux, który dokonuje decyzji, a potem kończy wpisywanie w linii. Konkretnie: 1) Jeżeli Char jest 10 lub 13 (koniec linii) lub -1 (koniec pliku), nie wkładaj nic wiecej, reszta stringu jest pusta. 2) W przeciwnym wypadku postaw Char na początku stringu i rekurencyjnie wkładaj jego resztę w ten sam sposób. Cięcia w read_str_aux zapewniają że jeżeli jakakolwiek z pierwszych trzech klauzul zakończy się powodzeniem ostatnia klauzula nie będzie nigdy wypróbowywana. Celem cięć w tym przypadku jest zachowanie ostatniej klauzuli przed dopasowywaniem nieodpowiednich wartości Char-ów. Zauważcie ze read_str zakłada ze input klawiatury jest buforowany. Jeżeli klawiatura jest nie buforowana read_str będzie wciąż działał, ale jeżeli użytkownik użyje backspace podczas wpisywania backspace nie usunie poprzedniego znaku zamiast tego znak backspace-u pojawi się w stringu.

Często chcemy przeczytać całą linie input-u nie jako string ale jako atom. Jest to również proste, ponieważ wbudowane orzeczenie name/2 konwertuje stringi i atomy w obie strony:?- name(abc,what). What = [97,98,99] % equivalent to "abc"?- name(what,"abc"). What = abc?- name(what,"hello there"). What = 'Hello there' yes?- name(what,[97,98]). What = ab Ćwiczenie Uruchom read_str i read_atom na swoim komputerze i sprawdź czy działają tak jak jest to opisane.

Struktury Wiele warunków Prologa składa się z funktora wynikającego z zera lub więcej warunków takich jak argumenty: a(b,c) alpha([beta,gamma],x) 'this and'(that) f(g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v) i_have_no_arguments Warunki tej formy nazywane są Structures(Struktury). Funktor zawsze jest atomem, ale argumenty mogą być warunkami każdego typu. Struktura bez argumentów jest po prostu atomem.

Dotąd używaliśmy struktur w faktach, zasadach, zapytaniach i wyrażeniach algebraicznych. Struktury są również pozycjami danych w ich własnych prawach. Dla przykładu: person(name('michael Covington'), gender(male), birthplace(city('valdosta'), state('georgia'))) sentence(noun_phrase(determiner(the), noun(cat)), verb_phrase(verb(chased), noun_phrase(determiner(the) ), noun(dog))))

Struktury działają jak listy, jednakże są inaczej przechowywane (i bardziej kompaktowo) w pamięci. Struktura a(b,c) zawiera te same informacje jak lista [a,b,c]. W rzeczywistości oba są wymienialne przez =..?- a(b,c,d) =.. X. X = [a,b,c,d] yes?- X =.. [w,x,y,z]. X = w(x,y,z) Yes?- alpha =.. X. X = [alpha] Yes Zauważcie ze po lewej stronie argument jest zawsze strukturą, kiedy prawa strona argumentu jest zawsze listą której pierwszy element jest atomem.

Jedną ważną różnicą jest to że lista jest rozkładana na head i tail, kiedy struktura nie jest. Struktura zjednoczy się z inna strukturą która ma taki sam funktor i tą samą liczbę argumentów. Oczywiście cała struktura również zjednoczy się z pojedynczą zmienna: Unify With Result a(b,c) X X=a(b,c) a(b,c) a(x,y) X=b, Y=c a(b,c) a(x) fails a(b,c) a(x,y,z) fails Ćwiczenie Używając tego co już znasz o listach wykonywania, stwórz orzeczenie reverse_args które bierze dowolną strukture i odwraca kolejność jej argumentów:?- reverse_args(a(b,c,d,e),what). What = a(e,d,c,b)

Occurs check Można stworzyć dziwną, zapętloną strukturę by ujednolicić zmienne z strukturą lub listą która zawiera tę zmienną. Takie struktury zawierają wskaźniki do nich samych i prowadzą do niekończących się pętli kiedy wydruk staję się rutyną lub cokolwiek innego próbuje pokrzyżować je. Dla przykładu:?- X = f(x). X = f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f...?- X = [a,b,x] X = [a,b,[a,b,[a,b,[a,b,[a,b,[a,b,[a,b,[a,b[a,b,[a,b[a,b,[a,b...?- f(x) = f(f(x)) X = f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(...

Standard ISO zawiera orzeczenie unify_with_occurs_check które sprawdza którykolwiek jeden termin zawierający inny przed próbą ujednolicenia i zawodzi, jeśli tak to:?- unify_with_occurs_check(x,f(x)). no.?- unify_with_occurs_check(x,f(a)). X = f(a) Nasze doświadczenie jest takie ze occurs-check jest rzadko potrzebny w praktycznych programach Prologa, jednak jest czymś czego powinniśmy byś świadomi. Ćwiczenie Który z podanych zapytań tworzy zapętloną strukturę:?- X=Y, Y=X.?- X=f(Y), Y=X.?- X=f(Y), Y=f(X).?- X=f(Y), Y=f(Z), Z=a.

Konstruowanie celów na starcie Ponieważ zapytania w Prologu są strukturami, możesz je traktować jako dane i konstruować je podczas uruchomienia programu. Wbudowane orzeczenie call wykonuje swoje argumenty jako pytanie. Tak więc call(write( hello there )) jest równoznaczne z write( hello there ). Siła call-a pochodzi z faktu ze cel może być stworzony przez obliczenie i wykonanie. Dla przykładu: answer_question :- write('mother or father? '), read_atom(x), write('of whom? '), read_atom(y), Q =.. [X,Who,Y], call(q), write(who), nl.

Jeżeli użytkownik wpisze mother i cathy wtedy Q stanie się mother(who,cathy). Jest to wtedy wykonywane jako pytanie a wartość Who jest wypisana. Tak więc:?- answer_question. Mother or father? father Of whom? michael charles_gordon yes?- answer_question. Mother or father? mother Of whom? melody eleanor yes

Możemy zrobić to nieco bardziej wygodne przez zdefiniowanie orzeczenia Apple (podobne do APPLY w Lisp) które bierze atom oraz listę i tworzy zapytanie używając atomu jako funktora a listy jako argumentów, następnie wykonuje zapytanie. % apply(functor,arglist) % Constructs and executes a query. apply(functor,arglist) :- Query =.. [Functor Arglist], call(query). Cel apple(mother,[who,melody]) ma ten sam efekt co mother(who,melody). Argumenty dawane są przez listę ponieważ ich liczba jest nieprzewidywalna. Prolog zapobiega zdefiniowaniu orzeczenia z dowolną zmienną liczbą argumentów. Ćwiczenie Czy twój Prolog pozwala ci na wpisaniu zmiennej jako cel, zamiast używania call?

Strategie przechowywania danych Istnieją trzy miejsca w których mogą być przechowywane dane w programie Prolog: 1) W instancji zmiennej. Jest to mniej trwały sposób przechowywania informacji ponieważ, zmienne istnieją tylko w zasięgu klauzuli która je definiuje. Dalej zmienne tracą swoje wartości na rzecz backtracking-u. 2) W argumentach orzeczeń. Lista argumentów jest jedynym sposobem by procedura Prologa normalnie komunikowała się ze światem. Przez przekazywanie argumentów do siebie kiedy wzywa się rekurencyjnie, procedura może wykonać rekurencyjny proces i zapisać informacje z jednej rekurencji do następnej. 3) W bazie wiedzy. Jest to najbardziej trwały sposób przechowywania informacji. Informacje umieszczone w bazie wiedzy przez asserta lub assertz pozostaje tam dopóki wyraźnie je wycofamy.

Prostym przykładem przechowywania wiedzy w bazie wiedzy jest orzeczenie count które mówi ci ile razy zostało wywołane. Dla przykładu:?- count(x). X = 1 yes?- count(x). X = 2 Yes % count(x) % Unifies X with the number of times count/1 has been called. count(x) :- retract(count_aux(n)), X is N+1, asserta(count_aux(x)). :- dynamic(count_aux/1). count_aux(0).

Ponieważ count musi zapamiętać informacje z jednego wezwania do następnego bez względu na backtracking lub niepowodzenie, musi przechowywać dane w bazie wiedzy używając assert i retract. Nie ma możliwości by informacje mogły przejść z jednej procedury do innej przez argumenty, ponieważ nie ma możliwości przewidzenia jaka będzie ścieżka wykonywania. Jest kilka powodów by używać assert tylko jako środek ostateczny. Pierwszym jest to ze assert przeważnie zabiera więcej czasu komputerowi niż zwykłe przekazywanie argumentu. Innym powodem jest to że programy używające assert są trudniejsze do zdebugowania. Są jednak słuszne powody używania assert. Jednym z nich jest zapisywanie wyników które zajmują dużo czasu i miejsca do przeliczenia. Dla przykłady algorytm przeszukujący grafy może wykonać ogromną ilość kroków by znaleźć każdą ścieżkę grafu.

Kolejnym słusznym użyciem assert jest ustawienie parametrów kontrolnych duzego skomplikowanego programu takiego jak eksperckiego systemu którego użytkownik może używać w kilku trybach. Przez wykonywanie właściwych assert-ów program może ustawić się do wykonywanie funkcji które użytkownik chce wykorzystać w szczególnych sesjach. Ćwiczenie Zdefiniuj procedurę Gengym(x) która generuje nowy atom za każdym razem gdy jest wywołana. Jedyną z możliwości było by sprawić by działało tak:?- gensym(what). What = a?- gensym(what). What = b...?- gensym(what). What = z?- gensym(what). What = za Jednak możesz generować atomy jak tylko chcesz pod warunkiem by się nie powtarzały.

Dziękujemy za uwagę