Recursion
Prolog Dersleri - 3.Hafta
Tekrarlama ve
rekürsiyon
Prosedür ve veri yapılarında tekrarlama iĢlemleri Visual Prolog‟da kolay bir şekilde yapılır. Bu bölümde önce tekrarlı işlemler (döngüler ve rekursif prosedürler), daha sonra ise rekursiv veri yapıları incelenecektir.
Tekrarlı İşlemler
Pascal, BASIC veya C gibi konvansiyonel programlama dilleriyle çalışanlar, Prologla çalışmaya başladıklarında FOR, WHILE, REPEAT gibi ifadeleri göremeyince şaşırabilirler. Çünkü Prologda iterasyonu anlatan direkt bir yol yoktur. Prolog sadece iki türlü tekrarlama-geriye dönüş imkanı tanır. Bu işlemlerde bir sorguya birden fazla çözüm bulmak ve bir prosedürün kendisini çağırdığı rekürsiyon işlemine imkan tanır.
Geriye İz Sürme
•Bir prosedür, istenilen bir hedef için uygun bir çözüm yerine alternatif başka çözümler aramak için geriye döner. Bunun için geriye henüz denenmemiş bir alternatifi kalan en son alt hedefe gidileceğini, bu noktadan tekrar aşağıya doğru inileceği bilinmektedir. Geriye dönüşü iptal edip tekrarlı işlemler yaptırmak mümkündür.
Örnek•PREDICATES
•nondeterm ulke_adi(symbol)
•ulke_adlarini_yaz
•CLAUSES
•ulke_adi("Türkiye").
•ulke_adi("Kazakistan").
•ulke_adi("Azerbaycan").
•ulke_adi("Amerika").
•ulke_adlarini_yaz:-
•ulke_adi(Ulke), write(Ulke), nl, fail.
•ulke_adlarini_yaz.
•GOAL ulke_adi(Ulke).
Yukarıdaki ulke_adi yüklemi sadece ülke isimlerini sıralar. Dolayısıyla GOAL ulke_adi(Ulke) şeklindeki bir hedefin birden fazla sonucu vardır ve ulke_adlarini_yaz yuklemi bunların hepsini görüntüler.
•ulke_adlarini_yaz :- ulke_adi(Ulke), write(Ulke), nl, fail. satırıyla söylenmek istenen şey şudur: “Bütün ülke isimlerini yazmak için, önce ulke-adi(Ulke) cümlesine cevap bul, bunu yaz, yeni bir satıra geç ve işlemi yeniden başlat.”
•„fail‟ komutunun programa yüklediği görev şöyle özetlenebilir: “GOAL cümlesine uygun bir çözüm bulunduğunda, geriye dönüş yap ve başka alternatiflere bak”.
•„fail‟ yerine, sonucu daima yanlış olan ve bu yüzden geriye dönüşü zorlayan başka bir alt hedef kullanmak mümkündür. Örneğin, 10=5+6 satırı her zaman yanlış olacağı için, Prolog başka alternatifler bulmak için daima geriye dönüş yapar.
•Örneğimizde ilk önce Ulke=Türkiye olur ve sonuç ekrana yazılır. „fail‟ komutuna sıra geldiğinde program, bir alt hedefe geri döner. Fakat nl veya write(Ulke) satırları için kullanılabilecek herhangi bir veri olmadığı için, bilgisayar ulke_adi(Ulke) iliĢkisi için baĢka çözümler arar.
•Ulke_adi(Ulke) iliĢkisi çalıĢtırıldığında, önceden boĢ değiĢken olan Ulke değiĢkeni „Türkiye‟ değerini almıĢtı. Bu yüzden bu iliĢkiyi yeniden kullanmadan önce Ulke değiĢkeni yeniden serbest hale getirilir. Daha sonra Ulke değiĢkeninin alabileceği baĢka bir olgu aranır. Ġkinci oluguda bu sağlanır ve ulke_adi yüklemindeki Ulke değiĢkeni „Kazakistan‟ değerini alır. Bu iĢlem böylece devam eder ve sonuçta Ģu satırlar görüntülenir.
•Türkiye
•Kazakistan
•Azerbaycan
•Amerika
•4 Solutions
•Eğer ulke_adlarini_yaz yüklemi „fail‟ komutundan sonra yazılmamıĢ olsaydı, cevap yine aynı olurdu fakat „yes‟ yerine „no‟ satırı görüntülenirdi.
Önceki ve Sonraki Eylemler Bir hedef için gerekli olan bütün çözümleri sağlayan bir program, çözüm yapmadan ve yaptıktan sonra başka şeyler de yapabilir. Örneğin
1. Yaşanacak güzel yerler
2. Ulke_adi(Ulke) yükleminin bütün sonuçlarını yaz.
3. Başka yerler de olabilir...
şeklinde bir mesaj yazarak bitirebilir.
Ulke_adlarini_yaz cümlesin ulke_adi(Ulke) yükleminin bütün sonuçlarını içerir ve sonunda bir bitiş mesajı yazar.
Örnekte geçen ilk ulke_adlarini_yaz cümlesi yukarıdaki adımlardan ikincisi içindir ve bütün çözümleri yazar. ikinci cümlesi ise üçüncü adıma tekabül eder ve sadece hedef cümlesini başarılı bir şekilde bitirmek içindir. Çünkü ilk cümle daima yanlıştır.
•Programı başka şekilde yazmak gerekirse:
•PREDICATES
•nondeterm ulke_adi(symbol)
•ulke_adlarini_yaz
•CLAUSES
•ulke_adi("Türkiye").
•ulke_adi("Kazakistan").
•ulke_adi("Azerbaycan").
•ulke_adi("Amerika").
•ulke_adlarini_yaz:-
•write("YaĢanacak bazı yerlerin listesi.."), nl, fail.
•ulke_adlarini_yaz :-
•ulke_adi(Ulke), write(Ulke), nl, fail.
•ulke_adlarini_yaz:-
•write("BaĢka güzel yerler de vardır..."), nl.
•İlk cümledeki „fail‟ komutu çok önemlidir. Çünkü bu komut ilk cümle çalıştırıldıktan sonra programın ikinci cümleye geçişini sağlar. Buradaki write ve nl http://alikoker.name.tr
•76
•komutlarının başka bir iş yapmaması çok önemlidir. Son „fail‟ komutundan sonra programın ikinci cümleciğe geçişi sağlanmalıdır.
Döngülü Geriye Dönüşün
Uygulanması Geriye dönüş işlemi bir hedefin bütün çözümlerinin bulunması açısından son derece
önemlidir. Birden fazla çözüm sunamayan hedefler için yine de geriye dönüş işlemi
yapılabilir. Bu da tekrarlama işlemini yapar. Örneğin:
tekrar.
tekrar:-tekrar.
gibi iki cümlecik sonsuz sayıda çözüm olduğunu göstermektedir.
Örnek:
PREDICATES
nondeterm tekrar
nondeterm karakteri_ekrana_yaz
CLAUSES
tekrar.
tekrar:-tekrar.
karakteri_ekrana_yaz:-
tekrar, readchar(Harf), /*Klavyeden girilen harfi oku ve C'ye ata*/
write(Harf),
Harf='\r', !. /* Satır sonu tuşuna (Enter/Return) basılmadıysa devam et*/
•GOAL karakteri_ekrana_yaz, nl.Yukarıdaki örnekte tekrar işleminin nasıl yapılacağını görülebilir. Karakteri_ekrana_yaz:-... kuralı, „Enter/Return‟ basılmadığı müddetçe, klavyeden girilen kararterleri kabul edip ekranda gösteren bir prosedür tanımlamaktadır.
•Karakteri_ekrana_yaz kuralının çalışma mekanizması şöyle sıralanabilir:
•1. tekrar‟ı çalıştır. (Hiçbir şey yapmaz)
•2. bir karakter oku (Harf)
•3. Harf karakterini yaz
•4. Harf‟in satır sonu karakteri olup olmadığını kontrol et.
•
•5. Eğer satır sonu elemanı ise, işlemi bitir, değilse, geriye iz sürme işlemini yap ve alternatif ara. Buradaki write ve readchar kurallarının hiçbiri alternatif sağlayamaz. Dolayısıyla geriye dönüş hemen tekrar kuralına gider, bunun ise alternatif sunması tabiidir.
•6. İşlem devam eder. Bir karakter oku, onu ekrana yaz, satır sonu elemanı olup olmadığını kontrol et.
•
Rekursif Prosedürler (A³)
● Kendisini çağırabilen prosedüre rekursif prosedür
diyoruz.
● Tekrarlama işlemin yapmanın diğer bir yolu da
rekursiyondur.
● Rekursif prosedürler yaptıkları işlerin sayısını, toplamını
veya işlmelerin ara sonuçlarını saklayabilir ve bunları bir
döngüden diğerine rahatlıkla aktarabilirler.
Örnek: N sayısının faktöryelini (N!) hesaplamak
Standart Sistem
● Başlangıç değeri 1 olan bir
return değişkeni oluştur.
● 1 den N e kadar bir döngü
oluştur.
● return değerini döngünün
indisi ile çarparak return
değişkenine ata.
● Sonuç return değişkenidir.
Rekursif Sistem
● Kural 1: N sayısı 0 ise sonuç
1 dir.
● Kural 2: N sayısı 0 den
büyük ise sonuç N-1
faktöryeldir ((N-1)!).
Örnek Kod
factorial(0,1).
factorial(N,F) :-
N>0,
N1 is N-1,
factorial(N1,F1),
F is N * F1.
Ama ??
Faktöriyel kuralını N=6 olacak şekilde
çağırılırsa, faktöriyel kendini N=5 için
çağırılacaktır. Bu durumda N deki 6 değeri
nereye gitti ?
Rekursif Prosedürlerin Avantajları
● Mantıksal olarak iterasyondan çok daha basittir.
● Listeleri işlemede çok yaygın olarak kullanılır.
● Rekursiyon işlemi özellikle probleme içerisinde dallanmaların mevcut
olduğu, yani bir problemin çözümünün bir alt probleme bağlı olduğu
durumlarda çok faydalıdır.
Sondan Rekursion Optimizasyonu
Rekursiyon işleminin en önemli dezavantajı, belleği fazlaca kullanmasıdır.
Bir prosedür başka bir alt prosedürü çağırdığında, çağrıyı yapan prosedürün
çağrıyı yaptığı anki çalışma durumu mutlaka kaydedilmelidir. Böylece çağrılan
işlemini bitirdikten sonra çağıran kaldığı yerden devam edebilir. Ama dallanma
çok fazla olursa hafızaya kaydettikleri de fazla olacağından bu bir hafıza
şişmesine bile neden olabilir.
Yani biz 6! i hesaplamak isterken otamatik olarak 5! , 4! , 3! , 2! , 1! , 0! i de
hesaplamış oluyoruz ve bu hesaplamalar için ekstradan hafızaya bunları atmış
oluyoruz.
Peki bunun için ne yapmalı?
Bir prosedürün, başka bir prosedürü kendisinin en son adımı olarak çağırdığını
düşünelim. Çağrılan prosedür görevini yaptıktan sonra, çağrıyı yapan prosedürün
yapması gereken başka bir şey kalmaz. Çağrıyı yapan prosedürün kendisinin çalışma
anını kaydetmesi gerekmez, çünkü o andaki bilgi artık gereksizdir. Çağrılan prosedür
biter bitmez, program akışı normal biçimde devam eder.
Bu durum daha açık olarak aşağıdaki şekilde ifade edilebilir. A prosedürünün B
prosedürünü, B prosedürünün ise C prosedürünü son adım olarak çağırdığını düşünelim.
B prosedürü C’yi çağırdığında, B’nin başka bir şey yapması gerekmez. Yani C‟nin o anki
çalışma durumunu B olarak kaydetmek yerine, B’nin kaydedilen eski durumun C’ya
aktarmak, depolanan bilgi içinde uygun değişiklik yapmak mümkündür. C bittiği zaman,
doğrudan A prosedürü tarafından çağrılmış gibi olacaktır.
Sondan Rekursiyonun Kullanımı
Prologda bir prosedürün başka bir prosedürü ‘kendisinin
en son adımı olarak çağırması’ bu prosedürü sondan
rekursiyon yapacaktır. Bu da bu prosedürün geriye dönüş
yapması olasılığını kaldırır. (Yani heryede kullanamayız.)
######yani
sayac(Sayi):-
write(Sayi), nl,
yeni_sayi=Sayi+1,
sayac(Yeni_sayi).
Sondan Rekursiyonu Engelleme
Engellemek için :
● Başka prosedürü son adımda çağırmamak.
● Başka bir alternatif bırakmak.
(Bu ne midir? *)
Rekursiyonda Cut Kullanımı
Rekursif Prosedürleri çalıştırırken bazı noktalarda durdurmamız gerekir. bunun
için içinde bir koşul yazarak bu prosedürü durdurabiliriz.sayac(Sayi):-
Sayi>=0,!, /*Burada sayı 0 dan küçük olma durumunda boş bir sonuç döndürür*/
Sayi<1000,!, /*Burada ise sayı 1000 i geçtiği takdirde programı sonlandırması için*/
write(Sayi),
write(","),
Ysayi is Sayi+1,
sayac(Ysayi).
sayac(_):-
write("Sayi negatiftir.").
swipl_hakkinda.pl #oguzhancoo
%Değişken tanımları
?- assert(like(x,y)). %kabukta
like(x,y). %dosyada
%kabukta dosya çağırırken
?- [‘dosya.pl’].
%Konsole’da çalıştırırken
swipl -f dosya.pl
Args On The Loop%factorial
fact(Num,NumF):-
m_fact(Num,NumF,1,1).
m_fact(Num,NumF,I,P):-
I<=Num,!,
NewP=P*I, %kendisini yeni değerlerle çağırır
NewI=I+1, % //
m_fact(Num,NumF,NewI,NewP).
m_fact(Num,NumF,I,P):-
I>Num,
NumF=P.
%P=P+1 prologda değişken ataması yapmaz.
%YP = P+1 şeklinde yeni bir değişken sorunu çözecektir.
fact.pl%factorial
factorial(N,Fact) :-
fact_iter(N, 1, Fact).
fact_iter(0, SoFar, SoFar) :- !.
fact_iter(N, SoFar, Ans) :-
N1 is N - 1,
SoFar1 is N * SoFar,
fact_iter(N1, SoFar1, Ans).
%source http://www.cs.toronto.edu/~sheila/384/w11/simple-prolog-examples.html
hanoi.pl%hanoi towers
move(1,X,Y,_) :-
write('Disk '),
write(X),
write(' ten '),
write(Y),
write(' ye taşındı.'),
nl.
move(N,X,Y,Z) :-
N>1,
M is N-1,
move(M,X,Z,Y),
move(1,X,Y,_),
move(M,Z,Y,X).
Recursive Data Structures
recursive_exmp.pl
%A(B(D,E),C(F,G))
A
B C
D E F G
%Depth First Searching
tree_exmp.pl%DOMAINS
agac_yapisi=agac(string, agac_yapisi, agac_yapisi); bos_dal
%PREDICATES
agaci_tara(agac_yapisi)
%CLAUSES
agaci_tara(bos_dal).
agaci_tara(agac(Isim, Sol, Sag)):-
write(Isim, '\n'),
agaci_tara(Sol), agaci_tara(Sag).
%GOAL
agaci_tara(agac("Emine", agac("Ali", agac("Hasan", bos_dal, bos_dal),agac("Fatma", bos_dal, bos_dal)), agac("Ayse",
agac("Fuat", bos_dal, bos_dal),agac("Leyla", bos_dal, bos_dal)))).
swipl -s tree.exmp.pl ?-agaci_tara(agac("Emine", agac("Ali", agac("Hasan", bos_dal, bos_dal),agac("Fatma", bos_dal, bos_dal)),
agac("Ayse", agac("Fuat", bos_dal, bos_dal),agac("Leyla", bos_dal, bos_dal)))).
Emine
Ali
Hasan
Fatma
Ayse
Fuat
Leyla
yes
?-
ağaç oluşturmak
#s
Ağaç biçiminde bir yapı oluşturmanın bir yolu
operatörlerden ve argümanlardan oluşan iç içe
geçmeli bir yapı yazmaktır.
agac_olustur(Sayi, agac(Sayi, bos_dal, bos_dal))
# Eğer Sayi bir sayı ise, agac(Sayi, bos_dal,
bos_dal) tek hücreli bir ağaç olup veri olarak bu
sayıyı içerir.
sola_yerlestir(Sayi, agac(A, _,B), agac(A,Sayi,B))
#İlk ağacı, ikinci ağacın alt dalı olarak alır ve üçüncü ağacı
da sonuç olarak verir.
domains
agac_yapisi=agac(string,agac_yapisi,agac_yapisi)
tek daldan oluşan ağaç
agac_olustur(String, deger)
dal birleştirme
sola_yerlestir(deg1,degx,degy)
saga_yerlestir(deg1,degx,degy)