Mantık programlama: Prolog, unification ve resolution
Önerme mantığından Prolog'a: fact, rule ve query, unification ve instantiation, resolution ile derinlik öncelikli arama ve backtracking, listeler, aritmetik (is), cut ve closed-world assumption. Sorguların cevaplarını ve sırasını bulmak.
Mantık programlama, programı mantık önermeleriyle yazar: neyin doğru olduğunu söylersin, sistem sorulan soruyu bu önermelerden kanıtlamaya çalışır. Bu yüzden declarative (bildirimsel) bir yaklaşımdır: sonucun ne olduğunu tarif edersin, nasıl hesaplanacağını değil. Dersin dili Prolog (1972, Colmerauer ve Roussel). Sınavda çıkan: bir veritabanı ve kurallar verilir, bir sorgunun tüm cevaplarını sırasıyla yazman ve unification sonuçlarını bulman istenir.
Altın kurallar:
- Clause’lar yukarıdan aşağı, alt hedefler soldan sağa denenir.
- Başarısızlıkta en son seçim noktasına geri dönülür (backtracking).
- Büyük harfle başlayan değişkendir, küçük harfle başlayan atom.
ishesaplar,=sadece unify eder.- Cut başka clause’ların denenmesini engeller.
Mantıktan Prolog’a
Proposition (önerme) doğru ya da yanlış olabilen mantıksal bir ifadedir. Predicate calculus önermeleri compound term’lerle yazar: parent(ayse, can), bir functor (parent) ve argümanlar. Değişkenli önermeler için niceleyiciler var: (her X için), (bir X vardır).
Otomatik kanıt için önermeler standart bir biçime çevrilir: clausal form. Prolog bunun da kısıtlı bir halini kullanır: Horn clause. Bir Horn clause’un sol tarafında (sonuç kısmında) en fazla bir terim vardır:
Okunuşu: “parent(X, Y) ve parent(Y, Z) doğruysa grandparent(X, Z) doğrudur.” Sol tarafı boş olmayan Horn clause’lar Prolog’un fact ve rule’larıdır.
Prolog’un üç ifadesi
% fact: koşulsuz doğru
parent(ayse, can).
parent(ayse, deniz).
parent(mehmet, can).
parent(can, elif).
parent(deniz, kerem).
parent(deniz, lale).
male(mehmet). male(can). male(kerem).
% rule: baş :- gövde. virgül = VE
father(X, Y) :- parent(X, Y), male(X).
grandparent(X, Z) :- parent(X, Y), parent(Y, Z).
?- parent(X, can). % query (goal)
X = ayse ;
X = mehmet.
| Terim | Örnek | Not |
|---|---|---|
| Atom | ayse, can, [] | Küçük harfle başlar ya da tırnaklı |
| Sayı | 17, -3 | |
| Değişken | X, Kim, _ | Büyük harf ya da alt çizgi. _ adsız değişken |
| Structure | parent(ayse, can), f(X, g(a)) | functor(argümanlar) |
Değişkenin “değer alması” atama değildir; instantiation (örneklenme) bir kanıt arayışı sırasında geçicidir ve backtracking’de geri alınır. Bir clause içindeki aynı isimli değişkenler aynı şeyi gösterir; farklı clause’lardakiler bağımsızdır.
Unification
İki terimi aynı yapan değişken bağlamalarını bulma işlemi. Kurallar:
- Atom ya da sayı yalnızca kendisiyle eşleşir.
- Bağlanmamış bir değişken herhangi bir terimle eşleşir ve ona bağlanır.
- İki structure, functor’ları ve argüman sayıları aynıysa ve argümanları sırayla eşleşiyorsa eşleşir.
?- f(X, b) = f(a, Y). X = a, Y = b
?- f(X, X) = f(a, b). false (X hem a hem b olamaz)
?- g(X, h(Y)) = g(k, h(X)). X = k, Y = k
?- [H|T] = [1, 2, 3]. H = 1, T = [2,3]
?- [A, B|C] = [x, y, z, w]. A = x, B = y, C = [z,w]
Resolution ve arama sırası
Resolution, bir hedefi kanıtlamak için o hedefle unify olan bir clause bulup, clause’un gövdesini yeni hedefler olarak almaktır. Hedef listesi boşalınca kanıt tamamdır. Prolog backward chaining yapar: hedeften başlayıp fact’lere doğru geriye gider (fact’lerden başlayıp yeni fact türetmek forward chaining’dir; hedef az, fact çoksa backward daha verimli).
Prolog’un kararları:
- Derinlik öncelikli (depth-first): bir alt hedefin kanıtını bitirmeden sonrakine geçmez. Bellek açısından ucuz.
- Clause sırası: veritabanında yazıldığı sırayla, yukarıdan aşağı.
- Alt hedef sırası: soldan sağa.
- Backtracking: bir alt hedef başarısız olunca, en son seçim yapılan alt hedefe dönülür ve onun bir sonraki alternatifi denenir.
İzleme: ?- grandparent(ayse, G).
grandparent(ayse, G) :- parent(ayse, Y), parent(Y, G).
parent(ayse, Y) 1. çözüm Y = can
parent(can, G) G = elif → CEVAP 1: G = elif
başka parent(can, _) yok → backtrack
parent(ayse, Y) 2. çözüm Y = deniz
parent(deniz, G) G = kerem → CEVAP 2
G = lale → CEVAP 3
parent(ayse, Y) başka çözüm yok → bitti
?- grandparent(ayse, G).
G = elif ; G = kerem ; G = lale.
?- father(F, can).
F = mehmet.
father(F, can) için önce parent(F, can) → F = ayse; male(ayse) başarısız → backtrack; F = mehmet, male(mehmet) ✔.
Kardeş kuralı ve \=
sibling(X, Y) :- parent(P, X), parent(P, Y), X \= Y.
?- sibling(can, S).
S = deniz.
?- sibling(kerem, S).
S = lale.
\= olmasa her kişi kendi kardeşi de çıkardı.
Aritmetik
?- X is 2 + 3 * 4. X = 14
?- X = 2 + 3. X = 2+3 (yapısal terim, hesaplanmadı)
?- X = 2 + 3, Y is X * 2. X = 2+3, Y = 10
?- X is Y + 1. instantiation error
?- 6 =:= 2 * 3. true (=:= iki tarafı hesaplayıp karşılaştırır)
is sağ tarafı hesaplar ve sol tarafla unify eder. Sağdaki tüm değişkenler önceden bağlı olmalı. Bu yüzden aritmetik içeren kurallarda alt hedef sırası önemlidir:
len([], 0).
len([_|T], N) :- len(T, N1), N is N1 + 1.
fact(0, 1).
fact(N, F) :- N > 0, N1 is N - 1, fact(N1, F1), F is N * F1.
speed(ford, 100). time(ford, 2).
distance(C, D) :- speed(C, S), time(C, T), D is S * T.
?- len([a, b, c], N). N = 3
?- fact(4, F). F = 24
?- distance(ford, D). D = 200
Listeler
[a, b, c] ve [H|T] gösterimi. Klasik ilişkiler:
member(X, [X|_]).
member(X, [_|T]) :- member(X, T).
append([], L, L).
append([H|T], L, [H|R]) :- append(T, L, R).
?- member(X, [p, q]). X = p ; X = q
?- append([1], [2, 3], L). L = [1,2,3]
?- append(X, Y, [1, 2]). X = [], Y = [1,2] ;
X = [1], Y = [2] ;
X = [1,2], Y = []
Predicate’ler fonksiyon değil ilişkidir: aynı append birleştirir, böler, bir önek ya da sonek arar. Hangi argümanın girdi hangisinin çıktı olduğunu sorgu belirler.
Cut ve negation
Cut (!) her zaman başarılı olan bir hedeftir; ama backtracking ile üzerinden geri geçilemez. Geçildiği anda, bu predicate için yapılmış seçimler (hem önceki alt hedefler hem kalan clause’lar) dondurulur.
max(X, Y, X) :- X >= Y, !.
max(_, Y, Y).
maxbad(X, Y, X) :- X >= Y.
maxbad(_, Y, Y).
?- max(7, 3, M). M = 7.
?- maxbad(7, 3, M). M = 7 ; M = 3. (ikinci cevap yanlış)
Cut verimlilik kazandırır ama programın anlamını clause sırasına bağlar; saf mantıktan uzaklaşır.
Negation as failure: \+ G, G kanıtlanamazsa başarılı olur. Bu, Prolog’un closed-world assumption’ına dayanır: veritabanından kanıtlanamayan her şey yanlıştır.
?- \+ parent(elif, _). true (elif'in çocuğu kayıtlı değil)
?- \+ parent(can, _). false
Mantıksal “değil” ile aynı değildir: \+ bir değişkeni bağlamaz ve “bilinmeyen” durumu yoktur.
Prolog’un eksiklikleri
| Sorun | Açıklama |
|---|---|
| Çözüm sırası kontrolü | Mantıksal olarak aynı iki program farklı davranabilir; programcı sırayı düşünmek zorunda |
| Sol özyineleme | p :- p, ... sonsuz döngü |
| Closed-world assumption | “Bilmiyorum” ile “yanlış” ayrılamaz |
| Negation | \+ mantıksal değil değildir |
| Verim | Cut olmadan büyük aramalar yavaş |
bad_anc(X, Y) :- bad_anc(Z, Y), parent(X, Z). % sol özyineleme, önce
bad_anc(X, Y) :- parent(X, Y).
ancestor(X, Y) :- parent(X, Y). % taban önce
ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).
?- bad_anc(ayse, elif). sonsuz özyineleme (yığın taşar)
?- ancestor(ayse, D). D = can ; D = deniz ; D = elif ; D = kerem ; D = lale
Uygulama alanları: ilişkisel veritabanı sorguları, uzman sistemler (expert systems), doğal dil işleme, kanıt sistemleri.
Sık yapılan hatalar
| Yanlış | Doğrusu |
|---|---|
Can bir atom | Büyük harf: değişken. Atom can |
| Cevapları alfabetik yazmak | Veritabanı sırası ve derinlik öncelikli arama |
X = 2 + 3 → X = 5 | X = 2+3; hesaplamak için is |
X is X + 1 X’i artırır | Asla doğru olmaz; değişkenler yeniden atanmaz |
N is N1 + 1 özyinelemeden önce | N1 henüz bağlı değil: hata |
f(X, X) = f(a, b) → X = a | false |
| Cut sadece sonraki clause’ları keser | Gövdede önceki alt hedeflerin alternatiflerini de |
\+ G = mantıksal değil | Kanıtlanamazlık (closed world) |
Kendini test et
Sınavdaki gibi İngilizce sorular. Önce kâğıt kalemle çöz, sonra cevabı aç.
Given the facts below, what are all answers to ?- parent(X, can). in order?
parent(ayse, can).
parent(ayse, deniz).
parent(mehmet, can).
parent(can, elif).
parent(deniz, kerem).
parent(deniz, lale).Cevabı göster
Prolog veritabanını yukarıdan aşağı tarar ve sorguyu her fact ile unify etmeye çalışır. 1. fact: X = ayse ✔. Kullanıcı ; ile başka cevap isteyince backtracking ile aramaya devam eder: 2. fact can değil deniz, uymaz; 3. fact X = mehmet ✔. Cevaplar veritabanındaki sırayla gelir.
With the same facts and the rule below, what are all answers to ?- grandparent(ayse, G).?
grandparent(X, Z) :- parent(X, Y), parent(Y, Z).Cevabı göster
Kural: X, Z'nin büyük ebeveyni ise X'in bir çocuğu Y var ve Y'nin çocuğu Z. İlk alt hedef parent(ayse, Y): Y = can → parent(can, Z): Z = elif. Backtrack: can'ın başka çocuğu yok, Y = deniz → Z = kerem, sonra Z = lale. Sıra: önce ilk alt hedefin ilk çözümüyle tüm devamlar, sonra ikinci çözüm.
With the same facts, what are all answers to ?- sibling(can, S).?
sibling(X, Y) :- parent(P, X), parent(P, Y), X \= Y.Cevabı göster
can'ın ebeveynleri ayse ve mehmet. P = ayse: ayse'nin çocukları can (can \= can başarısız) ve deniz ✔. P = mehmet: tek çocuğu can, elenir. \= "unify edilemez" demektir; olmasaydı herkes kendinin kardeşi çıkardı.
What does each unification produce? (a) f(X, b) = f(a, Y) (b) f(X, X) = f(a, b) (c) g(X, h(Y)) = g(k, h(X))
Cevabı göster
Unification iki terimi aynı yapan değişken bağlamalarını bulur. (a) Argüman argüman: X ↔ a, b ↔ Y. (b) İlk argüman X = a yapar; ikincide X (artık a) b ile eşleşmeli: başarısız. (c) X = k; sonra h(Y) = h(X) → Y = X = k.
What are H and T after ?- [H|T] = [1, 2, 3].? And A, B, C after ?- [A, B|C] = [x, y, z, w].?
Cevabı göster
[H|T] listeyi baş (head, ilk eleman) ve kuyruk (tail, geri kalan liste) olarak böler; Scheme'in car ve cdr'si gibi. Kuyruk her zaman listedir. [A, B|C] ilk iki elemanı ayırır, geri kalanı C'ye verir.
With the standard append/3 below, how many solutions does ?- append(X, Y, [1, 2]). have? List them in order.
append([], L, L).
append([H|T], L, [H|R]) :- append(T, L, R).Cevabı göster
Prolog'da predicate'ler ilişkidir, yön yoktur: append birleştirmenin yanında bölme de yapar. İlk clause X = [] verir. İkinci clause baş elemanı X'e alıp kalan listeyle özyinelenir. n elemanlı bir listenin n + 1 bölünüşü var.
What does each query print? (a) ?- X is 2 + 3 * 4. (b) ?- X = 2 + 3. (c) ?- X is Y + 1. (Y unbound)
Cevabı göster
is sağ tarafı aritmetik olarak hesaplar, öncelik olağan: 2 + 12 = 14. = ise hesaplamaz, sadece unify eder: X yapısal terim +(2, 3) olur. is'in sağındaki her değişken önceden bağlı olmalı, yoksa hata. Ayrıca X is X + 1 asla doğru olmaz: X'i değiştirmez, bağlıysa kendisiyle karşılaştırır.
What does ?- len([a, b, c], N). return?
len([], 0).
len([_|T], N) :- len(T, N1), N is N1 + 1.Cevabı göster
Boş listenin uzunluğu 0. Aksi halde kuyruğun uzunluğu N1 ise N = N1 + 1. Özyineleme: len([b,c]) → len([c]) → len([]) = 0, sonra geri dönerken 1, 2, 3. _ adsız değişken: değeri önemsiz. N is N1 + 1 özyinelemeden sonra gelmeli, çünkü N1 ancak o zaman bağlı.
What does ?- fact(4, F). return?
fact(0, 1).
fact(N, F) :- N > 0, N1 is N - 1, fact(N1, F1), F is N * F1.Cevabı göster
fact(4) = 4 · fact(3) = 4 · 3 · fact(2) = ... = 4 · 3 · 2 · 1 · 1 = 24. N > 0 koşulu önemli: olmasaydı fact(0, F) için birinci cevaptan sonra backtracking ikinci kurala girer ve negatif sayılara sonsuza kadar inerdi.
What are all answers to ?- max(7, 3, M). and to ?- maxbad(7, 3, M).?
max(X, Y, X) :- X >= Y, !.
max(_, Y, Y).
maxbad(X, Y, X) :- X >= Y.
maxbad(_, Y, Y).Cevabı göster
Cut (!) başarıyla geçildiğinde, bu predicate için o ana kadar yapılan seçimleri dondurur: başka clause denenmez. max'te ilk clause 7 verir ve cut ikinci clause'u kapatır. maxbad'de backtracking ikinci clause'a da girer ve yanlış bir cevap (3) üretir. Cut verimlilik sağlar ama programı sıraya bağımlı ve okunması zor hale getirir.
Why does ?- bad_anc(ayse, elif). never terminate, while the ancestor version answers correctly?
bad_anc(X, Y) :- bad_anc(Z, Y), parent(X, Z).
bad_anc(X, Y) :- parent(X, Y).
ancestor(X, Y) :- parent(X, Y).
ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).Cevabı göster
Prolog clause'ları yukarıdan aşağı, alt hedefleri soldan sağa, derinlik öncelikli dener. bad_anc'ın ilk clause'unun ilk alt hedefi yine bad_anc: hiçbir fact'e bakmadan sonsuz özyineleme (yığın taşar). Mantıksal olarak iki tanım aynı, ama Prolog'un çözüm sırası programcıyı clause ve hedef sırasını düşünmeye zorlar. Saf mantık programlamanın en büyük kusurlarından biri.
Given only the facts above, what do ?- \+ parent(elif, _). and ?- \+ parent(can, _). return, and what assumption does this rely on?
Cevabı göster
\+ G G kanıtlanamazsa başarılı olur. elif'in çocuğu veritabanında yok → true. can'ın var (elif) → false. Bu mantıksal değil değil: "kanıtlanamıyorsa yanlıştır" varsayımı (closed-world assumption). Veritabanında olmayan her şey yanlış kabul edilir; Prolog "bilmiyorum" diyemez.
Doğrulama: Prolog kurulu olmadığı için node ile küçük bir Prolog yorumlayıcısı yazıldı (verify/SEN2001/lib/prolog.mjs: unification, derinlik öncelikli SLD resolution ve backtracking, cut, is, karşılaştırmalar, \=, \+, listeler). Sayfadaki her sorgu bu yorumlayıcıda çalıştırıldı ve cevaplar, gerçek Prolog'un yazdığı sırayla listelendi (verify/SEN2001/14.mjs). Sol özyinelemeli kuralın sonsuz döngüye girdiği de yorumlayıcıda gözlendi (yığın taşması); N > 0 koşulu silinmiş fact'in ikinci cevapta sonsuza indiği verify/SEN2001/01.mjs içinde denendi.