- 2 (50pkt) - około końca semestru, przedostatni tydzień (dyskusyjne). Tylko PROLOG, będzie się wiązał w dużej mierze z projektem.



Podobne dokumenty
Programowanie w logice

Programowanie deklaratywne

Języki programowania deklaratywnego

Programowanie komputerów

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

Programowanie w Logice Struktury danych (Lista 2)

Programowanie w logice Prolog 2

Systemy ekspertowe i ich zastosowania. Katarzyna Karp Marek Grabowski

Paradygmaty programowania

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

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

7. Pętle for. Przykłady

Przeszukiwanie z nawrotami. Wykład 8. Przeszukiwanie z nawrotami. J. Cichoń, P. Kobylański Wstęp do Informatyki i Programowania 238 / 279

Programowanie deklaratywne

Języki programowania zasady ich tworzenia

Programowanie w Logice

Wprowadzenie do Prologa

LOGIKA I TEORIA ZBIORÓW

0.1. Logika podstawowe pojęcia: zdania i funktory, reguły wnioskowania, zmienne zdaniowe, rachunek zdań.

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

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

Uniwersytet Zielonogórski Instytut Sterowania i Systemów Informatycznych. Ćwiczenie 3 stos Laboratorium Metod i Języków Programowania

Elementy języka Scheme

Programowanie Strukturalne i Obiektowe Słownik podstawowych pojęć 1 z 5 Opracował Jan T. Biernat

Elementy kognitywistyki II: Sztuczna inteligencja. WYKŁAD III: Problemy agenta

Definicje. Algorytm to:

Projekt 4: Programowanie w logice

Programowanie obiektowe

Metoda tabel semantycznych. Dedukcja drogi Watsonie, dedukcja... Definicja logicznej konsekwencji. Logika obliczeniowa.

Programowanie w języku Python. Grażyna Koba

Algorytm. a programowanie -

Instrukcja do ćwiczenia P4 Analiza semantyczna i generowanie kodu Język: Ada

Wstęp do programowania. Listy. Piotr Chrząstowski-Wachtel

PODSTAWY SZTUCZNEJ INTELIGENCJI

Programowanie w Logice Przykłady programów. Przemysław Kobylański

Zapisywanie algorytmów w języku programowania

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

Programowanie. Pascal - język programowania wysokiego poziomu. Klasa 2 Lekcja 9 PASCAL

Programowanie deklaratywne

C++ - przeciążanie operatorów. C++ - przeciążanie operatorów. C++ - przeciążanie operatorów. C++ - przeciążanie operatorów

Definicje wyższego poziomu

PRZEWODNIK PO PRZEDMIOCIE

Matematyczne Podstawy Informatyki

Systemy GIS Tworzenie zapytań w bazach danych

5. OKREŚLANIE WARTOŚCI LOGICZNEJ ZDAŃ ZŁOŻONYCH

Informacje ogólne. Karol Trybulec p-programowanie.pl 1. 2 // cialo klasy. class osoba { string imie; string nazwisko; int wiek; int wzrost;

Obiektowy PHP. Czym jest obiekt? Definicja klasy. Składowe klasy pola i metody

Prolog (Pro-Logic) Programowanie w Logice. Dr inż. Piotr Urbanek

Badania operacyjne: Wykład Zastosowanie kolorowania grafów w planowaniu produkcji typu no-idle

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

Wykład 8: klasy cz. 4

Programowanie deklaratywne

KARTA KURSU. Wstęp do programowania

ECDL Podstawy programowania Sylabus - wersja 1.0

Wstęp do Sztucznej Inteligencji

Analiza semantyczna. Gramatyka atrybutywna

Programowanie w logice Wykład z baz danych dla

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.

Składnia rachunku predykatów pierwszego rzędu

Słowem wstępu. Część rodziny języków XSL. Standard: W3C XSLT razem XPath 1.0 XSLT Trwają prace nad XSLT 3.0

Praktyka Programowania

Klasa 2 INFORMATYKA. dla szkół ponadgimnazjalnych zakres rozszerzony. Założone osiągnięcia ucznia wymagania edukacyjne na. poszczególne oceny

Podstawy programowania w języku C

Programowanie w języku C++ Grażyna Koba

Aby przejść do edycji w tym module należy wybrać zakładkę "Dla Pracowników" -> "Sprawdziany".

Języki programowania deklaratywnego

Rozdział 4 KLASY, OBIEKTY, METODY

Matematyczna wieża Babel. 4. Ograniczone maszyny Turinga o językach kontekstowych materiały do ćwiczeń

XQTav - reprezentacja diagramów przepływu prac w formacie SCUFL przy pomocy XQuery

Uwagi dotyczące notacji kodu! Moduły. Struktura modułu. Procedury. Opcje modułu (niektóre)

Zadanie 1 Przygotuj algorytm programu - sortowanie przez wstawianie.

PROLOG. Prolog. Programowanie, W.F. Clocksin, C.S. Mellish, HELION Prolog, język sztucznej inteligencji, Eugeniusz Gatnar, Katarzyna Stąpor, Wyd.

Algorytmy sztucznej inteligencji

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

Ziemia obraca się wokół Księżyca, bo posiadając odpowiednią wiedzę można stwierdzić, czy są prawdziwe, czy fałszywe. Zdaniami nie są wypowiedzi:

Przykład 1: Funkcja jest obiektem, przypisanie funkcji o nazwie function() do zmiennej o nazwie funkcja1

Języki programowania C i C++ Wykład: Typy zmiennych c.d. Operatory Funkcje. dr Artur Bartoszewski - Języki C i C++, sem.

Uniwersytet Zielonogórski Wydział Elektrotechniki, Informatyki i Telekomunikacji Instytut Sterowania i Systemów Informatycznych

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

Podstawy Programowania Obiektowego

Klasy abstrakcyjne i interfejsy

SQL - Structured Query Language -strukturalny język zapytań SQL SQL SQL SQL

Drzewa BST i AVL. Drzewa poszukiwań binarnych (BST)

Wstęp do Programowania Obiektowego. Wykład 13 Paradygmaty. Składnia i semantyka.

Temat: Zastosowanie wyrażeń regularnych do syntezy i analizy automatów skończonych

Metoda Tablic Semantycznych

Podstawy Informatyki. Algorytmy i ich poprawność

1 Podstawy c++ w pigułce.

Uniwersytet Zielonogórski Instytut Sterowania i Systemów Informatycznych. Algorytmy i struktury danych Laboratorium 7. 2 Drzewa poszukiwań binarnych

Adam Meissner.

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

Języki formalne i automaty Ćwiczenia 4

Sortowanie przez wstawianie Insertion Sort

Programowanie obiektowe

Programowanie w logice

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

Heurystyki. Strategie poszukiwań

L E X. Generator analizatorów leksykalnych

Matematyka Dyskretna. Andrzej Szepietowski. 25 czerwca 2002 roku

Wykład 5. Jan Pustelnik

Transkrypt:

JPS 1. Wykład 1: Formalności: 2 Kolokwia: - 1 (50pkt) - dwie części: podstawy LISP(20pkt) i PROLOG(30pkt). Dwa kolejne dni poniedziałek i wtorek. Może być to odstęp tygodniowy, ale sprawdzian jest traktowany jako całość. Planowane na 21.04. - 2 (50pkt) - około końca semestru, przedostatni tydzień (dyskusyjne). Tylko PROLOG, będzie się wiązał w dużej mierze z projektem. Projekt: zaliczenie - nie jest punktowany. Ocena w grupach 2-3 osoby, trzeba przekonać prowadzącego, że rozumiemy projekty. Pozytywny wynik rozmowy jest podstawą do drugiego sprawdzianu, który oceni wiedzę punktowo. - wszyscy studenci pracują nad dwoma takimi samymi zadaniami - zadania dotyczą PROLOGu: > Zadanie 1: Maszyna wnioskująca dla systemu eksperckiego o funkcjonalności zbliżonej do realistycznej (prowadzący mówi: 30-40 wierszy kodu). Startujemy od zapisu w pseudokodzie (struktura + komentarze: co, gdzie, jak). Trzeba uważać na pewne "subtelności" w PROLOGU - analiza działania kodu, dokładne poznanie mechanizmów języka. > Zadanie 2: Przeanalizowanie gotowego programu, wprowadzenie do niego odpowiednich modyfikacji. Będzie to planowanie akcji zapisane według bardziej wyrafinowanego algorytmu, niż proste przeszukiwanie drzewa. Należy dokładnie zrozumieć, w jaki program działa, potem wprowadzić modyfikacje i różne optymalizacje. Sam planer (główna procedura) zajmuje około 10 wierszy. Modyfikacje po 3 wiersze, ale trzeba wstawić w odpowiednie miejsca i wiedzieć, co się dzieje - będą organizowane spotkania na wykładzie, które będą pewne aspekty projektów dosyć dokładnie objaśniać (chodzić na wykłady...) - projekt może być dyskutowany w dowolnych grupach, rozmowy 2-3 osoby. Trzeba uzyskać pozytywną ocenę z: Kol1.LISP, Kol1.PROLOG, rozmowa oceniająca, Kol2. Części pierwszego kolokwium osobno oceniane (ciekawe... :/). Literatura: - Bratko - Prolog Programming for Artificial Intelligence ed.3, Pearson Education 2001 Wszystko objaśnione na przykładach, bardzo dobra do nauki. www.booksites.net/bratko - przykłady - Clocksin, Mellish - Programming in PROLOG ed.5, Springer-Verlag 2003 Dużo objaśnień, mało przykładów. Wartość praktyczna i dydaktyczna słaba. - Winston, Horn - LISP, Addison-Wesley 1984 Pozycja nadal na czasie... Po prostu w LISP niewiele się zmieniło - Graham - ANSI Common LISP, Prentice Hall 1995 Charakter bardziej praktyczny, implementacyjny - Luger - Artificial Intelligence ed.5, Pearson Education 2005 Podejście implementacyjne, LISP i PROLOG, podstawy sztucznej inteligencji. [Przeszukać torrenty!]

Języki przetwarzania danych symbolicznych: Języki przystosowane do przetwarzania danych symbolicznych. Korzysta z nich sztuczna inteligencja. Dane symboliczne to struktury zbudowane e składników elementarnych zwanych symbolami. Symbol określa znaczenie przypisane elementowi danych w trakcie przetwarzania, odniesione do modelu świata. Za pewną formę przetwarzania symbolicznego można uznać języki zapytania do baz danych. M.in. PROLOG może zastąpić w pewnym stopniu SQL. Jednak głównym polem jest sztuczna inteligencja. Podstawowe obiekty i związki modelowanego świata są reprezentowane przez symbole (semantyka). Składniowo, symbole są ciągami znaków. W trakcie przetwarzania są traktowane jako niepodzielne elementy danych. Ciągi znaków pełnią rolę identyfikatorów obiektów przetwarzanych jako reprezentujące pewne obiekty świata rzeczywistego. Decydują o tożsamości. Symbole to elementy danych używane do reprezentowania obiektów i związków występujących w dziedzinie rozpatrywanego problemu. W kodzie źródłowym symbol ma postać ciągu znaków jednak w przetwarzaniu jest traktowany jako niepodzielna jednostka. Przykładowa struktura danych symbolicznych: Przedstawiona struktura reprezentuje regułę bazy wiedzy systemu eksperckiego Definicja (zapisana w pseudokodzie) STRUKTURA reguła { poprzednik: <lista list symboli> następnik: <lista symboli> } Przykładowy egzemplarz struktury typu reguła { } [ [zwierzę należy do gromady ssaki] [zwierzę ma kopyta] ] // poprzednik [ zwierzę należy do grupy kopytne] // następnik Symbole to w sumie każdy wyraz z listy. Przetwarzanie polega na: dekomponowaniu, przetwarzaniu, porównywaniu, porządkowaniu. Natomiast nigdy nie polega na operacjach typu zliczanie znaków identyfikatora symboli itp. Powszechnie znanymi językami używanymi w dziedzinie sztucznej inteligencji są LISP i PROLOG. LISP: Język proceduralny (imperatywny). Zapis problemu i sposobu rozwiązania ma postać algorytmu (sekwencja operacji do wykonania). Język funkcyjny - każdy identyfikator języka jest funkcją. Przetwarzanie w języku funkcyjnym przebiega na zasadzie : f(g(h(),h()), h()) itp. Przekazywanie danych odbywa się przez wartość funkcji. W LISP nie istnieją parametry wynikowe. Istnieją zmienne globalne (z reguły używamy tylko wtedy, gdy jest to niezbędne).

PROLOG: Język deklaratywny. Podajemy warunki, jakie ma spełniać wynik. Mniejszy nacisk kładziony na sekwencję operacji. Opisujemy problem w kategoriach specyfikacji związków zachodzących pomiędzy obiektami z dziedziny problemu. Problem jest zapisywany za pomocą logiki formalnej, z użyciem trochę innej specyfikacji: ZACHODZI f(x,y) WTEDY GDY ZACHODZI g(x,z) I ZACHODZI h(z,y) Pomimo charakteru deklaratywnego, daje się przetłumaczyć sprawnie na sekwencję operacji (wykorzystywane w interpreterach). Ma aspekt proceduralny. Pisząc kod należy mieć na uwadze to, jak on zostanie wykonany. Zajęcia będą kładły nacisk na język PROLOG (gdyż jest pojęciowo nowy :) ), zwłaszcza pod kątem formułowania problemów i zastosowań w sztucznej inteligencji. 2. Wyklad 2: Przykład przetwarzania danych symbolicznych: W zastosowaniach AI zmienia się model świata. Metody i algorytmy poszukiwania rozwiązań oraz problemy z nimi związane pozostają te same. Także metody konstruowania modeli nie zmieniają się. Dlatego będziemy pracowali na prostych przykładach. Układanie planu sekwencji akcji: W tzw. świecie klocków. Przyjmijmy, że w naszym świecie są cztery pola, na których możemy rozmieszczać klocki. Mamy dany pewien stan początkowy i docelowy. Klocki mogą być ustawione jeden na drugim. W naszym świecie operujemy pojęciami, cechami stanów: - co na czym stoi - pole / klocek wolny (nic na nim nie stoi) lub nie. Przykładowo: klocek2 klocek1 klocek5 --------- --------- --------- --------- pole1 pole2 pole3 pole4 Tworzymy struktury danych: Struktura danych definiująca związki przestrzenne: class ZW_P - typ związku (tutaj tylko LEZY_NA) - element_nad - element_pod Przykładowy związek: ZW_P LEZY_NA KLOCEK5 KLOCEK2

Struktura danych reprezentująca status: class STATUS - element - status Przykładowy status: STATUS KLOCEK1 WOLNY ; nic na nim nie leży Struktura danych reprezentująca stan świata: class STAN - lista związków przestrzennych - lista statusów Przykładowy stan: STAN [<LEZY_NA KLOCEK1 POLE4>,...] [<KLOCEK1, WOLNY>,...] Struktura danych reprezentująca cel: class CEL - lista związków przestrzennych - lista statusów! Niektóre cechy statusów i związków mogą być dla celu nieistotne. Nie wszystkie związki i stany nas interesują przy rozwiązywaniu danego problemu. Przykładowy cel: CEL [<LEZY_NA KLOCEK1 KLOCEK2>] [<POLE1 WOLNE>] Struktura danych reprezentująca akcję: class AKCJA - rodzaj akcji - podmiot - miejsce źródłowe - miejsce docelowe Przykładowa akcja: AKCJA PRZENIEŚ KLOCEK1 POLE1 POLE3

Algorytm planowania: Jest to pewien algorytm przeszukiwania przestrzeni stanów. Reprezentujemy go przez drzewo. Węzłami są stany, gałęzie to akcje prowadzące do kolejnych stanów. s0-------------------------------------- a01 a02 a03 s11----- s12 s13 a11 a12 s21 s22... Korzystamy ze struktury danych - trójki opisu stanu: <stan aktualny, stan poprzedni, akcja przenosząca> Utrzymujemy listę stanów poprzednich (w postaci listy opisów stanów) oraz kolejkę stanów aktualnie badanych (także lista opisów stanów). Algorytm: - zdejmujemy stan z kolejki - tworzymy listę akcji, które można podejmować w tym stanie - wyznacz dla każdej akcji nowy stan - jeżeli stan odpowiada celowi, to zwróć plan akcji - jeżeli stan różny od celu, twórz nowy opis stanu i dołącz go do kolejki stanów Przykładowy zapis - konstruowanie akcji: [<LEZY_NA KLOCEK2 KLOCEK 5>, <LEZY_NA KLOCEK1 POLE4>,...] [<POLE2 WOLNE>, <KLOCEK1 WOLNE>,...] - wyodrębnienie listy opisów statusu - przeszukanie listy opisów statusu w celu znalezienia klocka o opisie WOLNE (pomysł: dodatkowe pole typu KL / PL w opisie) - znalezienie klocka / pola, na którym leży - znalezienie na tej samej zasadzie wolnego pola - skonstruowanie akcji ze związanych elementów Dygresja: ze względu na charakter symboli jako niepodzielnych jednostek, porównywanie symboli odbywa się technicznie przez porównywanie wskaźników. Jakie kryteria musi spełniać język do przetwarzania danych symbolicznych / AI: - Potrzebujemy języka w którym prosto możemy manipulować danymi. - Wymagana jest także odpowiednia optymalizacja wszystkich operacji, aby software szybko działał. - Wreszcie, czasami potrzeba odpowiedniego sprzętu (istnieją wyspecjalizowane procesory LISPowe, przetwarzające w odpowiednio szybki sposób operacje, umożliwiające prostą manipulację z punktu widzenia sprzętu). - Wbudowane przeszukiwanie przestrzeni możliwych rozwiązań (w AI trzeba przeszukiwać masywne przestrzenie - PROLOG ma to zaimplementowane, LISP nie) Przykład

3. Wykład 3: Sprawy formalne: Zajęcia odbywają się w każdy poniedziałek i co drugi wtorek. Najbliższy harmonogram: wt. 11.03 pon. 17.03 pon. 31.03 wt. 01.04 Czego oczekujemy od języka przetwarzania symbolicznego revisited: - na poziomie kodu źródłowego: odpowiednio wysoki poziom abstrakcji bez bawienia się w szczegóły manipulacji - na poziomie manipulacji odpowiednia efektywność czasowa działania na strukturach, ze szczególnym uwzględnieniem implementacji w postaci zespołu wskaźników poszczególnych symboli - translator powinien mieć wbudowane rozwiązanie przeszukiwania rozwiązań (odpowiednie pętle, techniki za pomocą odpowiednich instrukcji) Przykład w języku LISP: Definicja funkcji wydobywającej z listy listę dwuelementową złożoną z pierwszego i ostatniego elementu: (defun skrajne-elementy (lista) (cons (first lista) (last lista) ) ) ; Można sobie przeformatować dla czytelności ( defun skrajne-elementy(lista) ( cons (first lista) (last lista) ) ) Omówienie: LISP składa się na podstawowym poziomie z atomów: liczby i symbole. Instrukcja defun definiuje funkcję. Pierwszy element jest nazwą, drugi to lista argumentów, trzeci listą instrukcji. Wszystkie te elementy są listami! W języku LISP nie istnieje coś takiego jak instrukcja. Wszystko jest wywołaniem procedury. Nie ma zwykłych instrukcji postaci np. instrukcji charakterystycznych dla języka C. Wszelkie przekazywanie danych pomiędzy procedurami odbywa się przez wartości zwracane przez funkcje. Stają się one argumantami dla innych funkcji. W ten sposób możemy np. zrealizować instrukcję przypisania bez przypisywania pod zmienną. Istnieje odpowiednia procedura (funkcja), nawet cała rodzina, która jest implementacją operacji przypisania. Używamy jednak tylko wtedy, gdy to jest wygodne bądź uzasadnione: (setf x y) - przypisz liście Wartość zwracana jest wartością przypisywaną. Wywołanie procedury jest listą. Pierwszy element to nazwa procedury wywoływanej, następne są interpretowane jako argumenty. Tak jak w przykładzie, first i last są po prostu wywołaniami odpowiednich procedur na argumencie lista. Obydwie zwracają listę. cons jest procedurą wbudowaną tworzącą listę przyrostowo, z głowy i ogona. Pierwszy element dołączany na początek, drugi argument jest listą - ogonem. Istnieje coś takiego,

jak lista pusta: () lub nil. Wartością wywołania funkcji defun jest lista będąca definicją funkcji. Lista napotkana w programie to tzw. forma, my będziemy używali określenia "wyrażenie listowe". Dygresja programistyczna ponadjęzykowa :) Lista jest to para <obiekt> <lista>. class LISTA - obiekt - lista Wywołanie funkcji skrajne-elementy: (skrajne-elementy lista1). Zmienne a symbole w LISP: Zmienne w LISP nie są deklarowane. W takim razie jak rozróżnić (na poziomie translatora / interpretera) procedurę od zmiennej? Translator tego nie odróżnia. Każdemu symbolowi można przypisać pewną wartość. Symbol, który w momencie wykonania ma przypisaną wartość jest z definicji zmienną. Przykładowo wywołanie: (f1 x y), gdy nie jest określona wartość x, jest błędne. Każdy element wywołania z wyjątkiem pierwszego jest poddawany ewaluacji. Interpreter usiłuje wyznaczyć wartość tego elementu. Jeżeli trafia na wyrażenie listowe, to zachodzi to rekurencyjnie. Jeżeli jest to symbol, to interpreter usiłuje wyznaczyć wartość. Brak wyznaczonej wartości jest równoznaczny błędowi. Wartości natychmiastowe uzyskiwane są przy pomocy wyspecjalizowanej funkcji quote. Przykładowe wywołanie: (f1 (quote x) y). Skrótowa notacja: (f1 'x y). Czyli w sumie chodzi o cytowanie wartości natychmiastowej bez dalszego poddawania ewaluacji. Kontrola typów: W LISP funkcjonuje RTTI. Np. jeżeli podamy zamiast listy atom, zwracany jest błąd, gdy oczekiwano atomu. W przykładzie błąd zgłoszony zostanie dopiero na poziomie podstawowych procedur, gdyż nie akceptują pojedynczych atomów. Przykład w LISP nr 2: Zwrócenie listy bez ostatniego elementu. Jest to implementacja własna procedury wbudowanej but-last: ( defun my-butlast (lista) ( if (endp (rest lista)) () ( const (first lista) (my-butlast (rest lista) ) ) ) Omówienie: endp zwraca prawdę, gdy lista podana jest pusta.

LISP: Powstał w latach 50' na MIT. Autor: John McCarthy. Powstał w wyniku zamierzenia, żeby język programowania był uporządkowany (!). Chodziło o pewien rygorystyczny język, w którym nie byłoby zbyt dużo dowolności. Powstał język programowania oparty na teorii matematycznej funkcji rekurencyjnych Church'a. Czynnikiem rozwojowym dla LISP były początki badań nad sztuczną inteligencją. LISP był pod ręką i doskonalono go pod kątem sztucznej inteligencji. Wszystkie struktury zdecydowano się implementować w oparciu o listy (LISt Processing). Zaczęły powstawać także procesory LISPowe, dostosowane do przetwarzania struktur listowych w pamięci. Wszystko razem komponowało się w sprawnie działającą całość. W efekcie, LISP jeszcze przed narodzinami PROLOGu stał się językiem dominującym w dziedzinie AI. Na poziomie podstawowym języka nie ma procedur sprawnego przeszukiwania przestrzeni rozwiązań. Wypracowano natomiast odpowiednie biblioteki zawierające procedury przeszukiwania wprzód, w tył i mieszane. Zastosowanie bibliotek umożliwia sprawne kodowanie zagadnień sztucznej inteligencji. Dla porównania, mechanizmy tego typu są wbudowane w PROLOGu na poziomie podstawowym. Przykład w PROLOGu: Przykładowy program to prezentowany w zeszłym tygodniu program planowania akcji w świecie klocków. plan (State, Goals, [], State) :- goals_achieved(goals, State) plan(initstate, Goals, Plan, FinalState) :-...... PROLOG w porównaniu z LISP jest językiem deklaratywnym. Zapisujemy tylko to, co ma zostać rozwiązane, a nie to, jak ma to być rozwiązane. 1) :- 2) == zachodzi 1) gdy 2), lub 1) wynika z 2). Jest to odwrotny zapis implikacji, odpowiednie reguły wnioskowania zapisujemy i dajemy je maszynie wnioskującej z wbudowanymi mechanizmami przeszukiwania, wnioskowania. Przykład nr 2 w PROLOGu: Przeszukiwanie grafu. Graf jest zdefiniowany przez szereg definicji łuków skierowanych: arc(a,b). arc(a,c). arc(b,c). arc(b,d). arc(c,d). Zdania prologowe są wyrażeniami rachunku predykatowego, pierwszego rzędu. Pierwszy symbol to symbol predykatowy p(a,b). p ma wartość T tylko wtedy, gdy zachodzi dla a i b. Najprostsza konstrukcja w prologu jest to zdanie predykatowe (ważna kropka!). Bez kropki jest to tylko formuła (WFF - Well Formed Formula). W taki sposób wpisane proste zdania rachunku predykatów to FAKTY. Co to znaczy, że istnieje ścieżka w grafie? Jest to pewien związek: path (X, Y, [X,Y]):-arc(X,Y). Pierwszy argument to początek ścieżki, drugi koniec, trzeci to sama ścieżka. Czyli jeżeli istnieje łuk od X do Y, To istnieje ścieżka o początku X, końcu Y, ścieżce [X,Y].

path(x, Y, [X L]):-arc(X,Z),path(Z,Y,L). Ścieżka pomiędzy X,Y o przebiegu X L (X łączony z pewną listą L - ogonem) istnieje wtedy, gdy istnieje łuk pomiędzy X a Z i istnieje ścieżka od Z do Y o przebiegu L. Zmienną w prologu jest każdy symbol zaczynający się od wielkiej litery lub od symbolu specjalnego. Maszyna wnioskująca w PROLOGu wnioskuje wstecz. Formułujemy twierdzenie i każemy udowodnić je interpreterowi. Przykładowo, jeżeli chcemy stwierdzić, czy istnieje ścieżka z a do d:?_path(a, d, Path) Domyślnym kwantyfikatorem przy formułowaniu twierdzeń jest kwantyfikator szczególny "istnieje". Po sformułowaniu takiego zapytania, maszyna wnioskująca wstecz znajdzie ścieżkę i zapise ją w zmiennej "Path". Sformułowanie jest celem początkowym (goal). Interpreter sprawdza fakty w poszukiwaniu natychmiastowego rozwiązania. Jeżeli nie, to sprawdza reguły (rules) w poszukiwaniu konkluzji, którą da się uzgodnić z celem. Jeżeli znajdzie, to proces wnioskowania standardowo przebiega według wzorca poszukiwania wstecz z nawrotami. Domyślnym kwantyfikatorem reguł jest kwantyfikator ogólny "dla każdego". Uzgodnienie zmiennych określamy tutaj jako związanie zmiennych. Implementacja wiązania zmiennych jest realizowana przez zastosowanie rezolucji. [Można sobie przypomnieć IW: Rezolucja w PROLOGU z wykładu...]. Interpreter może wykonywać nawroty do węzłów przeszukiwanego drzewa rozwiązań, w którym zachodził jakikolwiek wybór. Przykładowe wnioskowanie z wiązaniem zmiennych może przebiegać następująco: path(a,d,path) arc(a,z1) path(z1,d,l1) Z1=b Path=[a L1] path(b,d,l1) L1=[b,d] arc(b,d) <cel pusty> Konstruowane struktury, które przynajmniej częściowo składają się ze zmiennych, to tzw. "struktury nieukonkretnione" lub "struktury nie do końca ukonkretnione". Tworzenie odpowiedzi przebiega już na etapie wnioskowania wstecz, przez dopisywanie "w drugą stronę". Nie potrzeba wracać, tak jak u Traczyka na wykładach w zadaniach. Może to jest mniej intuicyjne dla człowieka, ale znacznie szybsze dla maszyny. 4. Wykład 4: Przykład ze ścieżkami w grafie - krok po kroku: Potencjalnie, w problemie jest więcej niż jedno rozwiązanie. Jak jest to realizowane? Interpreter prologu przeszukuje przestrzeń celów poszukiwaniu takich, które są osiągalne z celu początkowego. Cele osiągalne przedstawiają drzewo - mówimy o drzewie celów. <rys1>

Przykładowe drzewo celów: <rys2> Czyli możemy skorzystać albo z jednej albo z drugiej reguły na każdym etapie. Schodząc wgłąb, tworzymy drzewo celów. Na gałęziach wpisujemy uzgodnienia zmiennych, na końcu listy celów pośrednich (w węzłach). Traktujemy fakt jako regułę o pustej prawej stronie. Zdanie o pustym poprzedniku, poprzednik jest rozpatrywany jako "prawda". Drzewo celów jest przeglądane od lewej wgłąb. Przy natrafieniu na brak rozwiązania w liściu, wraca do ostatniego "rozwidlenia" (backtrack) i kontynuuje. Prolog domyślnie przeszukuje tylko do pierwszego rozwiązania. Żądanie znalezienia wszystkich może przebiegać w dwóch trybach: 1)?_path(a,d,S) S=[a,b,d] XeS ; // średnik mówi, żeby szukać wszystkich spełniających celów 2) qs(x,y,s) :- path (X,Y,S), check(s) // sprawdzenie S wymusza kolejne nawroty Sprawy formalne: - LISP nie będzie niezbędny do zaliczenia, ale konsekwencja w postaci niższych ocen może się zdarzyć. Dziwna skala ocen. - rozmowy projektowe przewidziane 3,4,5.06 - dopuszczenie do 2 kolosa na podstawie zrozumienia zadań. Prolog - historia i zarys: Współcześnie tłumaczone: PROgramming in LOGIC, było PROgrammation en LOGique (tak, koleś który to wymyślił - Alain Colmerauer - oczywiście jest Francuzem). Koncepcja poracowana okoo roku 1970. Powstał specjalnie jako język do przetwarzania gramatyk, nie przypuszczano, że rozwinie się na taką skalę. Do popularności przyczyniły się prace prowadzone w Londynie przez Roberta Kowalskiego. Powstaje podręcznik "Logic for problem solving". Początkowo Prolog był językiem interpretowanym. Stał się popularny nie tylko do zastosowań akademickich po skonstruowaniu efektywnego kompilatora Prologu. Konstrukcja opracowana około 1970 roku przez Grenoble i Marseilles (tzw. składnia marsylska, obecnie historyczna). Kompilator używający współczesnej składni na uniwersytecie w Edynburgu, Warren i Pereira. Nareszcie znalazł zastosowanie jako język do szerokiego przetwarzania danych, właściwie nawet język ogólnego przeznaczenia. Do zastosowania w dziedzinie sztucznej inteligencji predestynowało go środowisko powstania i specyficzne właściwości. Duża część rynku sztucznej inteligencji należy do Prologu i LISP. Od jakiegoś czasu wchodzi na ten rynek Java, głównie moduły do integracji z większymi systemami. M.in. wsparcie dla prologu. Innymi znanymi dla sztucznej inteligencji są m.in. SmallTalk i kilka innych. W porównaniu z LISP Prolog jest bardziej rozwojowy, umożliwia łatwiejsze poszukiwanie nowych rozwiązań, jest dobrym językiem do prototypowania sztucznej inteligencji. Dodatkowo, zmusza do poprawnego formułowania problemów, jest bardziej poprawny "politycznie" i logicznie :). Rozłożenie języków na świecie: USA podzielone (LISP=EastSide, Prolog=WestSide ;]). W Europie dominuje Prolog. [piejemy dalej na cześć prologu...] Prolog jest nazywany "Tool for thinking" - konstrukcja języka wymusza na nas logiczne myślenie o problemie i prowadzi do rozwiązania i poprawnego formułowania problemów. LISP wprowadza pewną dyscyplinę do programowania proceduralnego przez to, że

większość wartości przekazywanych jest przez wywołania funkcji. [rzeczywiście, bardzo to pozytywne zjawisko, gdzie zagnieżdżamy 15 stopni struktur nawiasowych zamiast używać zmiennych, ale wykładowca ma rację...] Przykład w Prolog deklaratywnego wyznaczania konkatenacji dwóch list: <następnik implikacji> {zachodzi związek KONKATENACJA między obiektami ListaA, ListaB, ListaC} JESLI <poprzednik implikacji> { } {ListaA=[] ORAZ ListaB=ListaC} LUB { ListaA=[Pierwszy Reszta] ORAZ ListaC=[Pierwszy NowaReszta] ORAZ {zachodzi związek KONKATENACJA między obiektami Reszta, ListaB, NowaReszta} } Prolog dopuszcza alternatywę w poprzedniku, ale bardziej "poprawnie" jest używać po prostu koniunkcji. Na slajdach z wykładu mamy zapis w Prologu. Najpierw "beznadziejny i mało efektywny". Dalej mamy optymalizację tego zapisu i działania. Co się dzieje dla postaci uporządkowanej i nieuporządkowanej??_conc([a,b], [c,d], L). - tutaj dla pierwszej postaci programu mamy uzgadnianie wielu zmiennych. W postaci skompresowanej / zoptymalizowanej, mamy za to jedno uzgodnienie. Nie ma sensu przyrównywanie do prawej strony w przesłance tak, jak w wersji niezoptymalizowanej. Złe przyzwyczejenie z czasów programowania proceduralnego. Możemy od razu wielkości przyrównywane po prawej stronie wstawić jako argumenty tworzonej funkcji. Rysunek przedstawia budowanie wyniku z postaci zoptymalizowanej: <rys3> 5. Wykład 5: Środowisko interpretacyjne Prologu: Programy napisane w Prologu są standardowo interpretowane. Można je kompilować. Użytkownik ma dostęp do kompilatora i interpretera wywołań. Użytkownik podaje cel. Procedura może być skompilowana lub nieskompilowana. Użytkownik nie widzi wewnętrznej reprezentacji. <rys1> Możemy ogólnie mówić w takim razie o interpretacji (z punktu widzenia użytkownika). Wywołania są interpretowane, natomiast procedury mogą być skompilowane (wywołanie) lub nie (interpretacja). Środowisko zalecane przez Parewicza: SWI PROLOG (http://www.swi-prolog.org). Opracowane na wydziale nauk społecznych, katedra psychologii. Do badań modeli psychologicznych. Procedura conc jest inaczej zbudowana, należy ją przesłaniać. Do przykładu przeszukiwania grafu: Pojawiło się opisane całe drzewo poszukiwań w materiałach wykładowych. Założenia dotyczące grafu: acykliczny, skierowany.

Do przykładu przeszukiwania listy: Wszelkie przetwarzanie danych w Prologu występuje w ramach operacji uzgodnienia. Polega na tworzeniu struktur, budowaniu ich przy pomocy zmian wskaźników. Efektem jest skonstruowana struktura. Struktury danych w języku Prolog: Powiedzmy, że chcemy stworzyć strukturę, która przedstawia zamówienie w pewnej firmie. Dysponujemy numerem zamówienia, datą, nazwami klientów. <rys2> Złożona struktura jest tworzona przez zdefiniowanie funktora, który transformuje zbiór iloczynu kartezjańskiego prostych danych w zbiór struktur. Przykładowym zamówieniem jest ZAM(l217, 20080312, 'XYZ'). Przypuśćmy, że mamy teraz w zamówieniu zagnieżdżony funktor zwracający datę: Zam(l217, data(2008,03,12), 'XYZ'). Chcemy zdobyć zamówienia z zeszłego miesiąca. Jak to robimy? zamow_miesiac(zam(nr, data(d,m,r), Klient), M) W zależności, co chcemy zrobić, możemy zastosować zdefiniowany selektor po numerze miesiąca w różny sposób.?_zamow_miesiac(zam(l217, data(17,03,2008), 'XYZ'), M) - otrzymanie miesiąca danego zamówienia, jeżeli zamowienie jest zmienną związaną. Jeżeli miesiąc jest zmienną związaną, zaś zamówienie nie jest, to dostajemy listę zamówień z określonego miesiąca. Dla funktorów występujących w tym samym miejscu w wywołaniu, kontrola typu w czasie wywołania polega na porównaniu funktorów celu i reguły. Jeżeli funktory są identyczne, następuje uzgodnienie. Inaczej otrzymujemy błąd. Procedura uzgadniania jest rekurencyjna, możemy dowolnie zagnieżdżać uzgodnienie. Jeżeli w celu występuje zmienna, w regule pewien funktor, to zmienna zostaje związana z wynikiem działania funktora. [ Po prostu uzgadnianie w Prologu - dokładnie tak, jak na wykładzie z IW ] Pary obiektów w Prologu: Domyślnym funktorem dla pary obiektów w Prolog jest "." Np. wyrażenie:.(a,b) reprezentuje parę obiektów. Listę można również zdefiniować jako parę:.(a,[]) dla listy jednoelementowej, dla większej liczby elementów, np. dla dwóch:.(a,.(b,[])). Każda lista jest widoczna jako para, której pierwszym elementem jest pewien obiekt, a drugim inna lista. Porównanie funktorów dla par odbywa się zgodnie ze schematem:.(x1, R1).(a,.(b,[])) Taka wewnętrzna reprezentacja powoduje, że lista musi być "rozbierana" od przodu. Przykład kolejny - procedura sprawdzająca, czy dany obiekt jest elementem listy: MEMBER(X,L):- L=[X R]. MEMBER(X,[X R]). MEMBER(X,[X _]) Ostatnia konstrukcja tego pierwszego zdania zawiera symbol specjalny "_" oznaczający

cokolwiek, z punktu widzenia do pominięcia. "_" nie przenosi wartości, nie interesuje nas. MEMBER(X,L):- L=[Y R], X\=Y, MEMBER(X,R). // czyli X nie jest pierwszym elementem, ale należy do reszty listy. Po przepisaniu: MEMBER(X,[Y R]) :- X\=Y, MEMBER(X,R). Ostatecznie nasza procedura member wygląda następująco: MEMBER(X,[X _]). MEMBER(X,[Y R]) :- X\=Y, MEMBER(X,R). Na materiałach wykładowych jest rozrysowane drzewo przeszukiwania przestrzeni celu dla przykładu. Własność nierówności jest interpretowana jako cel tak, jakby była uzgodniona z faktem. Jest to związane z implementacją wewnętrzną (różne wskaźniki?). Zastępowane jest przez cel pusty. Co się dzieje, jeżeli cel nie może być uzgodniony: W kontekście pytania użytkownika po prostu nie uzyskujemy nic w odpowiedzi, odpowiedź negatywna, np. na pytanie czy jest MEMBER, odpowiedź NO. W kontekście przetwarzania w programie, jeżeli wywołanie MEMBER : P(L1, L2):- Q(L1, X), MEMBER(X,L2). X jest uzgadniany najpierw przez Q. Jeżeli MEMBER zakończy się niepowodzeniem, następuje nawrót do ostatniego punktu wyboru X, czyli sprawdzenie, czy Q może w inny sposób uzgodnić X. Ogólnie, w przypadku braku możliwości ustalenia celu, następuje powrót do najbliższego punktu przetwarzania, gdzie mogła zostać podjęta inna decyzja, czyli inne uzgodnienie zmiennych. Procedury deterministyczne w kontekście celu: Procedura jest deterministyczna w kontekście rozpatrywanego celu, jeżeli dla tego celu początkowego w każdym kroku istnieje możliwość wybrania co najwyżej jednego celu. Procedury CONC i MEMBER są deterministyczne w kontekście celu w zastosowaniach do tej pory rozpatrywanych. Niedeterministyczne zastosowania CONC:?_conc(LA, LB, [a,b,c]) Gdzie zmienne LA i LB nie są związane. Wtedy rozwiązaniem są wszystkie pary LA, LB takie, że lista [a,b,c] jest ich konkatenacją. W takim użyciu, pytamy o wszystkie możliwe podziały listy. Prawie w każdym kroku (z wyjątkiem liścia związanego z drugim celem) może być uzgodniony każdy z dwóch celów: conc([], L2, L2). conc([x L1], L2, [X,RN]) :- conc(r1, L2, RN). Na slajdach z wykładu jest zademonstrowane drzewo przeszukiwania przestrzeni celów. Jeżeli cel początkowy jest określony przez użytkownika, wtedy otrzyma wartości zmiennych i odpowiedź YES. Jeżeli kontekstem jest przebieg programu, nastąpi nawrót do najbliższego punktu przetwarzania, w którym możliwe jest podjęcie innej decyzji, inne uzgodnienie celu.

Przykładowe zastosowanie operacji CONC w kontekście programu final_split: final_split(list1, LA, LB, List2) :- conc(la,lb,list1), check_list(lb, List2). Rozdzielamy listę w taki sposób, że 6. Wykład 6 - nie byłem... (cholerne PKP...) 7. Wykład 7: Deterministyczne i niedeterministyczne usuwanie elementu: Wersja funkcji usuwającej deterministyczna - usuwa pierwsze wystąpienie i tylko pierwsze wystąpienie usuwanego obiektu. Wersja niedeterministyczna - obydwie reguły mogą zostać wykonane równorzędnie, brak warunku różności. Usunięcie czegoś z listy pustej daje listę pustą. Usunięcie w przypadku deterministycznym nieistniejącego obiektu powoduje zwrócenie listy bez zmian. W przypadku niedeterministycznym - zwrócenie listy bez zmian (?)... Rozrysować sobie drzewka... Deterministyczne i niedeterministyczne usuwanie wszystkich elemntów: Czy usunięcie warunku w przypadk usuwania wszystkiego zmieni coś? Co? Czy to będzie poprawne? Dodawanie elementu przez usuwanie: Argument 2 jest nieukonkretniony. Czyli oczekujemy listy, która po usunięciu arg1 dałaby arg3. Dostajemy w ten sposób, przy nieukonkretnionym L, wszystkie listy, które po usunięciu a dałyby [b,e,c]?_delete1(a, L, [b,e,c]); Modyfikacja member: member1(x,[x _]). member1(x,[y Rest]) :- member1(x,rest). - wprowadzony niedeterminizm, po nawrotach analizuje dalej i wyszukuje Wywołanie?_member(X, [a,b,c]). zwróci nam po kolei wszystkie elementy listy... search_list(list1, List2) :- member1(x,list1), member(x,list2). Co to robi...? Sprawdza, czy kolejne elementy List1 należą do List2. znajdz_zma(lista_zam, Klient) :- member1(zamowienie, Lista_zam), zam_klient(zamowienie, Klient). zam_klient (zamowienie(numer, Data, Wartosc, Klient), Klient). Generowanie szablonów: /= - to nie jest różność, tylko "nieuzgadnialne z..."!!!

?_member1(a,l). // gdzie L jest nieukonkretnione Wygeneruje szablony list, na których a jest na 1, 2, 3 itd. miejscu. Sprawy formalne: Kolokwium I z PROLOG 30 - w postaci opracowania domowego, 5.05 + 2 lub trochę więcej dni = termin realizacji. Lepiej zrobić wcześniej. Kolokwium I z LISP (opcja) 20 27.05 Kolokwium II z PROLOG 50 - przedostatni poniedziałek semestru Bez LISP 57-80=4 Z LISP normalnie. Deklaratywność: Wystartować z postaci proceduralnej, biorąc pod uwagę możliwości interpretera prologu, deklaratywnie sformułować zadanie. Zdefiniowaliśmy tak concat. A co, jeżeli proceduralnie? CONC(LA, LB, LC) { if LA=[] then LC = LB else { X = <pierwszy_element LA> R = <reszta LA> conc(r, LB, LC1) LC = dolacz(x,lc1) } } Tłumaczenie na PROLOG: Nagłówek i if: uproszczenie: conc(la,lb,lc):- LA = [], LC = LB. conc([], L, L). Jeżeli chcemy mieć regułę deterministyczną, to reguły muszą się nawzajem wykluczać. // Hint: niepusta lista nie uzgodni się z pustą... Część else: Czyli wynik: conc([x R]... // odpowiednik X = pierwszy, R = reszta conc([x R], LB, [X LC1]) :- conc(r, LB, LC1). // [X LC1] - dolaczenie X na poczatku LC1 conc([], L, L). conc([x R], LB, [X LC1]) :- conc(r, LB, LC1). Procedura dająca prefiksy listy: prefix(l1, L2) :- L1=[]. => prefix([], _). // _ - zmienna anonimowa, nie odwol. prefix(l1, L2) :- L1=[X R1], L2=[X R2], prefix(r1, R2). => prefix([x R1], [X R2]) :- prefix(r1, R2).

a proceduralnie byłoby: prefix(p, L) { P=[] OR // rozejscie, nie xor! X=<pierwszy L> R=<reszta L> prefix(p1, R) P=<dodaj X do P> } Operacja podlisty: Deklaratywnie: sublist(s, L) :- prefix(s,l). sublist (S, [X R]) :- sublist (S, R). 8. Wykład 8: A jeszcze bardziej deklaratywnie? Tak: - S ma być sufiksem dowolnego prefiksu. Wykorzystamy do tego conc: sublist(s,l) :- conc(p,sf,l), conc(pp, S, P). // S jest podlistą L wtedy, gdy P jest prefixem L a Sf jest sufiksem (zachodzi związek, że [P Sf]==L. I : prefix P składa się z pewnej listy PP, w przypadku granicznym pustej, oraz listy S. Czyli jak w definicji: podlista jest sufiksem dowolnego prefiksu L. Do generowania wszystkich podlist: S niezwiązane, L związane, np. [a,b,c]. A co będzie, jeżeli przestawimy kolejność sublist? Nic nie jest związane dla pierwszego wyrażenia: sublist(s,l) :- conc(pp, S, P), conc(p, Sf, L). Wiemy, że: conc([], L, L). conc([x R], L2, [X RN]) :- conc(r,l2,rn). Jak to się będzie rozwijało drzewo? Powstanie generator szablonów: conc(l,m,n) L<-[] M<-N L=[], M=_G32, N=_G32 L<-[X1 R1] M<-LZ1 N<-[X1 RN1] conc (R1, LZ1, RN1) R1<-[]

LZ1<-RN1 L=_G15, M=_G38, N=[_G15 _G38] Generuje nam szablony podlist, w końcu zostaną wypełnione. Tutaj kolejność nie ma do końca znaczenia, może jedna metoda trochę dłuższa niż druga, za to przy rekurencji... Związek przełożony: przelozony(x,y):- bezp_przelozony(x,y). przelozony(x,y):- przelozony(z,y), przelozony(x,z). Jeżeli w drugiej regule zmienimy kolejność predykatów w przesłance, wejdziemy w nieskończoną pętlę, gdyż interpreter PROLOGU szuka wgłąb. Przestawienie reguł też spowoduje nieskończoną pętlę. HEURYSTYKA: Zawsze, jeżeli mamy definicje rekurencyjne, to reguła nierekurencyjna przed rekurencją. Zwracać uwagę na kolejność predykatów! Procedura tworzenia podzbioru: Powiedzmy, że mamy: [a,b,c]. Chcemy podzbiorów: [], [a], [a,b], [a,b,c], [], [b], [b,c]... // zmienne wielkimi literami subset([], _). subset(subset, Set):- Set=[X R], Subset=[X R1], subset(r1, R). uproszczenie: subset([],_). subset([x R1], [X R]):- subset(r1, R). druga możliwość: subset(subset, Set) :- Set=[X R], Subset = R1, subset(r1, R) uproszczenie całości: subset([], _). subset([x R1], [X R]) :- subset(r1, R). // podzbiory z elementem X subset(r1, [X R]) :- subset(r1, R). // podzbiory bez elementu X Przecięcie dwóch zbiorów: set_intersec(set1, Set2, Int) { if Set1=[] then Int=[] else { X=<pierwszy Set1> R=<reszta Set1> if member(x, Set2) then { set_intersec(r, Set2, Int1) Int<dodaj X do Int1> } else { set_intersec(r, Set2, Int1) Int=Int1 }

} } Zapisanie do PROLOGU: set_intersec([], _, []). // W Prologu nie można zwracać true ani false, ale procedura może się powieść, albo nie. I to wykorzystujemy: // member zakończyło się powodzeniem set_intersec([x R], Set2, [X Int1]) :- member(x, Set2), set_intersec(r, Set2, Int1). // member zakończyło się niepowodzeniem set_intersec([x R], Set2, Int1) :- not member(x, Set2), set_intersec(r,set2,int1). (?!) W sumie negację można sobie samemu napisać, nie patrząc na procedury wbudowane. Jeżeli wywołujemy set_intersec po prawej stronie jakiejś reguły, to wiadomo, że trzecia reguła nigdy się nie wykona przy pierwszym podejściu. Przy nawrocie, interpreter wybierze pozostałą możliwość i przy nawrocie wejdzie w trzecią możliwość. Jeżeli wyrzucimy z prawej strony 3 reguły "not member...", otrzymamy niedeterminizm niezamierzony, robi się błąd. Konieczny jest pewien mechanizm siłowy, który umożliwia pisanie czegoś w stylu 'else'. Warunek już raz został spełniony i w drugą gałąź nie trzeba wchodzić. Taki mechanizm istnieje. Wstawianie do listy i sortowanie: insert(x, [Y Rest], [Y Rest1]) :- X>Y, insert(x, Rest, Rest1). // Przesuwanie X po liście do coraz wyższych elementów, aż w końcu znalezienie odpowiedniego miejsca i... insert(x, [Y Rest], [X,Y Rest]) :- X =< Y. //... wstawienie Żeby nie sprawdzać dla drugiej reguły warunku, który już raz został sprawdzony, przy możliwości nawrotu w wywołaniu po prawej stronie, stosujemy ODCIĘCIE. Oznaczone przez "!". insert(x, [Y Rest], [Y Rest1]) :- X>Y,!, insert (X, Rest, Rest1). insert(x, List, [X List]). Odcięcie: Zinterpretowanie odcięcia powoduje wymazanie z pamięci interpretera wszystkich punktów, które wystąpiły przed odcięciem w kontekście przetwarzania procedury. P' :- Q,!, R. P'' :- S, T. P' Q1,R1 x P0 x P'' Definicja operacji z negacją z odcięciem: not_member(x, L) :- member(x,l). // jeżeli member zakończy się nie powodzeniem

not_member(_,_). // to not_member zakończy się powodzeniem not_member(x, L) :- member(x, L), fail // jeżeli member zakończy się powodzeniem, to predykat fail spowoduje niepowodzenie dla not_member not_member(x, L) :- member(x, L),!, fail // Teraz zabezpieczamy, żeby nie wykonało się przy nawrocie wejście w gałąź, która spowoduje zwrócenie prawdy i błąd. Ostatecznie: not_member(x, L) :- member(x, L),!, fail. not_member(_, _). Dla dowolnego predykatu P: not(p) :- call(p),!, fail. // metaprogramowanie, wartość zmiennej staje się celem. not(p). W nowszej wersji można sobie napisać po prawej stronie samą zmienną i automatycznie zostanie to obsłużone. Przy takim zapisie, wywołanie not(member(...)). Da się te nawiasy jakoś opuścić - na przyszłych zajęciach. 9. Wykład 9: Punkt wyjścia do projektu książka Bratki. Bazy danych ciąg dalszy: Uzyskanie listy wszystkich wyników z bazy danych klientów (uzyskanie widoku zamówień dla klienta): suma_zam(klient, Suma) :- findall(w, zamowienie(_,_,w,klient), Lista_wartosci), sumuj_liste(lista_wartosci, Suma). sumuj_liste([], 0). sumuj_liste([x R], S) :- sumuj_liste(r, S1), S is S1+X. Zbiorczy termin dla procedur i faktów w PROLOGu to klauzula (clause). Do dostępu do baz danych jest używany podzbiór PROLOGu bez struktur DATALOG. Subtelności dotyczące wprowadzania do bazy danych: Przez klauzulę typu 'jest': jest_klient(klient(...)) jest_zamówienie(zamowienie(...)) [WTF?! Dlaczego on tego nie może prosto wytłumaczyć...]

Procedura sortowania przez wstawianie: insertsort([],[]). insertsort([x R], Sorted) :- insertsort(r, Sorted1), insert(x, Sorted1, Sorted). insert(x, [Y Rest], [Y Rest1]):- X>Y,!, insert (X, Rest, Rest1). insert(x,list[x List]). insert(x, [], [X]). // jak to zmienia działanie? Co by było, jakby tego nie było? Takie sortowanie jest nieefektywne, gdyż złożoność jest w tym przypadku kwadratowa. Jak zdefiniować sortowanie tak, żeby było efektywne? Trzeba zdefiniować dodatkowy związek pomiędzy wynikiem pośrednim, a końcowym. Zmienną gromadzącą wyniki pośrednie nazywamy akumulatorem. insertsort1([x R], Acc, Sorted) :- insert(x, Acc, Acc1), insertsort1(r, Acc1, Sorted). insertsort1([], Acc, Acc). // przecież to już wynik końcowy, czyli Sorted jest po prostu akumulatorem... Wywołanie: insertsort1([1,5,7,3], [], Wynik). // pusty akumulator Procedura rekurencyjna tutaj wykorzystuje rekursję w ogonie (tail recursion). Dzięki temu silnik może to rozwinąć w iterację (wiemy, że nie będziemy korzystali z wyniku pośredniego, więc możemy tak sobie rozwinąć i pominąć). 10. Wykład 10: Materiał do kolokwium / projektu: - uzgadnianie argumentów - nawroty (zapamiętywanie punktów wyboru) - sens logiczny - konsekwencje: * operacje na strukturach wbudowanych w interpreter * obiekty częściowo / (nie) ukonkretnione * konstrukcje generuj / testuj - procedury niedeterministyczne - generatory szablonów - odpowiednik konstrukcji if-then-else (oraz uogólnienie w postaci możliwości wycofania się z dowolnego poziomu) Rozwiązania zadań można wysyłać mailem, będzie tydzień na wykonanie, ale pracy na 2 godziny ( ;) ). Kolokwium 30 pkt. Projekt Planer Sekwencji Akcji: Planer wybiera sekwencję akcji tak, aby osiągnąć pewien stan wyjściowy z wejściowego w świecie klocków, zwracając sekwencję akcji. Jednym z podejść analizy przestrzeni rozwiązań jest MEANS-ENDS ANALYSIS (analiza celów środków) bardziej optymalna od zwykłego przeszukiwania. W metodzie tej startujemy od celów (np. zbioru położeń klocków). Zastanówmy się, co jest potrzebne, żeby któryś cel osiągnąć w jednym kroku (jednej akcji). Wybieramy arbitralnie któryś z nich. Tworzymy pewien stan pośredni, w którym cel (np. G2) jest osiągnięty, a pozostałe nie. Tworzymy stany pośrednie przed G2, które dzieli od G2 jeden krok. Bierzemy pod uwagę tylko te stany, które są możliwe z punktu widzenia dostępnych ruchów w przestrzeni. Zbiór stanów pośrednich staje się nowym zbiorem celów. Za każdym razem sprawdzamy, czy osiągnięty stan pośredni nie zawiera celów częściowo zawartych w stanie początkowym.

Utworzone drzewo ma własność następującą: przy przeszukiwaniu wgłąb w lewo, jeżeli nie prowadzi nas do rozwiązania pewna ścieżka, to ta ścieżka staje się nieskończona. Należy przeszukiwać wszerz, inaczej narażamy się na przepełnienie stosu i nieotrzymanie odpowiedzi. Przeszukiwanie wszerz da także najkrótszą możliwość. Program pierwotny implementuje taki algorytm przeszukiwania, który należy odpowiednio zmodyfikować / poprawić. Jedna z modyfikacji dostępna w materiałach internetowych do książki Bratki: www.booksites.net/bratko/. Podany wybrany kod, m.in. A simple means-ends planer. [Poczytać z Bratki...]. Omówienie kodu: plan(state, Goals, [], State) :- goals_achieved(goals, State). // przypadek graniczny, stany docelowe=początkowe, koniec rekurencji. plan(state, Goals, Plan, FinalState) :-... State zbiór stanów początkowych, lista egzemplarzy funktorów reprezentujących stan. Goals podobnie, tylko zbiór stanów docelowych W przestrzeni klocków mamy dwa funktory reprezentujące stan: on(b5, p7) - coś stoi na czymś, clear(b3) nic nie stoi np. na klocku. Plan - lista akcji, związki przestrzenne o postaci przykładowo: move(<co>,<skąd>,<dokąd>). Plan będzie listą takich akcji. FinalState lista stanów końcowych Przechowujemy listę wszystkich celów (dlaczego, o tym za chwilę). choose_goal wybiera pewien cel achieves daje akcję, która prowadzi do celu requires daje warunek dla wykonania akcji prowadzącej do celu, Condition staje się nową listą celów plan tworzy stan pośredni 1 na podstawie akcji perform_action przejście do stanu pośredniego 2 na podstawie akcji plan twórz plan dla następnego zbioru celów (stanu pośredniego 2), lista Goals zostaje rozszerzona! conc dołącz To jest przeszukiwanie wgłąb. Modyfikacja 1: zaimplementowanie przeszukiwania wszerz: Conc wywoływane jako pierwsza przesłanka. Powoduje to wygenerowanie szablonów, ograniczenia dają przeszukiwanie wszerz. Pierwsze zadanie badawcze: jak to działa, w jaki sposób i dlaczego działa to jako przeszukiwanie wszerz (sens deklaratywny jest zachowany, ale jak to wygląda proceduralnie?). Sprawdzić, gdzie następują nawroty (w której procedurze i na jakiej zasadzie?). Nawrót następuje w przypadku nieuzgodnienia (niepowodzenia). Dokąd nawracamy? Generalnie, co można powiedzieć o nawrotach. W jaki sposób następuje sprawdzenie, że wszystkie cele znajdują się na liście? Wywołanie member (niedeterministycznego! u nas to było member1!). Pobierane są kolejne elementy listy celów i sprawdzane (w goals_achieved). choose_goal wybieramy cel, który nie został jeszcze osiągnięty

achieves - ma nam podać akcję, która osiąga zadany cel. Sprawdzamy, co akcja ma dodać do stanu, aby go spełnić. Pomocnicze procedury adds i removes na związkach przestrzennych. requires kolejna modyfikacja: requires(move(block, From, To), [clear(block), clear(to), on(block,from)]):- block(block), object(to), Block\=From, From\=To, To\=Block. // i właśnie to w materiałach jest modyfikacją. Usuwa informacje dotyczące From. Ma to spore konsekwencje dla sprawdzenia, na razie po prostu przyjmujemy. Rozpatrując procedury pomocnicze trzeba patrzeć, gdzie jest możliwość wyboru, nawrotu itp. Podpowiedź do problemu nawrotów: pomocna będzie umiejętność wyszukania w kodzie, jak następują nawroty w kodzie (czerwone i zielone strzałki w materiałach wykładowych). 11. Wykład 11: Projektu ciąg dalszy ku uciesze gawiedzi... Wstawianie checkpointu w Prologu: pokaz_krok(rezultaty) :- write(co_sie_dzieje), write( ), read( ),... Można też wprowadzić znacznik pracy krokowej jako dodatkowy argument. Następuje sprawdzenie wartości znacznika, praca krokowa / debug dla wartości true, dla fail normalne wykonanie. Projekt: 1 Przeszukiwanie wszerz na planie bliższym i dalszym: - kiedy następują i jak wyglądają nawroty - jak działa przeszukiwanie wszerz plan bliższy i dalszy 2 Ochrona osiągniętych celów: - w pewnym miejscu w kodzie jest wstawione odwołanie do procedury, która zapobiega zniszczeniu już znalezionego celu. Gdzie? Jak działa? 3 Powtarzanie celów i stanów czy trzeba zabezpieczyć przed wpadaniem w cykle i jak? - czy osiągnięcie stanu wcześniej w czasie planowania, w danym podejściu, danej rekurencji, a potem ponownie, świadczy o możliwości zapętlenia? - jak sobie radzić z takim problemem, jeżeli wystąpi (zapętlenie?) - trzeba skorzystać z akumulatora, ze zmiennej akumulacyjnej. 4 Obiekt From z aktualnego stanu: Coś z wiązaniem planerowi rąk i usuwaniem jednego z celów (konkretnie From) z requires, żeby można było wymieniać obiekty i tworzyć nowe i szukać bardziej optymalnych: - zmiana requires (usunięcie From) - bardzo istotne zmiany w perform_action action_block i action_from to są selektory, trochę inaczej zrealizowane niż do tej pory. Action ukonkretnione, Block nieukonkretnione, From podobnie. Użycie arg do rozbierania struktur, użycie functor do wyłuskania funktora: arg(2, f(a,b), b) // sprawdzamy, czy b jest drugim argumentem wywołania funktora functor(f(a,b),f,n) // F=f, N=2

Czyli po prostu nie zamrażamy From, tylko przez odpowiednie selektory ukonkretniamy sobie From wcześniej, jednocześnie nie wiążąc planerowi rąk... Arg wykorzystywane w postaci generatora odpowiedniego From. Pokazanie elastyczności Prologu. OŚWIECENIE w kwestii even i odd1: być może even i odd1 działają inaczej, bo lista jest reprezentowana w postaci pary [head tail], lista jednoelementowa ma pusty ogon, czyli jest to para (element,lista_pusta). Ale sama lista pusta musi być reprezentowana w jakiś inny sposób, chyba nie może być reprezentowana przez parę. 5 Obiekt z To z aktualnego stanu: Podobnie jak dla 4, nie ograniczamy planera. Pozbawiamy requires prawej strony i spodziewamy się, że sam sobie wszystko znajdzie... Bajera... Zamiast rzucać przypadkowe rozwiązania, przedstawiamy nieukonkretnione obiekty i gdzieś one się prawidłowo ukonkretniają. Gdzie, jak, kiedy, jakie procedury? Prolog, operacje na strukturach nieukonkretnionych, elastyczność, itp. Jakoś trzeba jeszcze zawrzeć sprawdzenia, że Block\= To itp. Warto się przynajmniej zastanowić, jak. Spróbować włączyć różności typu From\=To, dlaczego to jest bez sensu itp. 12. Wykład 12: