Sürdürmeler ve gelişmiş akış denetimi
Uzman dillerden yeni teknikler öğrenin
Düzey: Orta
Jonathan
Bartlett (johnnyb@eskimo.com),
Teknoloji Direktörü, New Medio
24 Mayıs 2006
Akış denetimi genellikle basit bir süreçtir: sıralama, seçim, yineleme. Bu birincil denetim yapılarını temel alarak yetişmiş pek çok programcı, başka ne tür akış denetimlerinin gerekli olabileceğini görmekte zorlanabilir. Bu makalede, sürdürmeler tanıtılır ve akış denetimine bambaşka açılardan nasıl bakabileceğiniz öğretilir.
Programcılığa ilk başladıklarında, pek çok programcı akış denetimi üzerine düşünmez, çünkü çoğu programcılık dilinde yalnızca birkaç basit akış denetimi yapısı bulunur. Ancak, akış denetimi dünyası, çoğu yaygın programlama dilinin izin verdiğinden daha zengindir. Daha az bilinen birçok uzmanlaşmış dilde, gelişmiş ve ilginç akış denetimi yapıları bulunur.
Gelişmiş akış denetimi örnekleri
Başlamanın en iyi yolu, farklı dillerdeki gelişmiş akış denetimi yapı örneklerine bakmaktır. Ardından, bu bilgileri gelişmiş akış denetimi durumları için genel bir çerçeveye yerleştirebiliriz.
İlk gelişmiş akış denetimini büyük
olasılıkla zaten biliyorsunuzdur:
yerel olmayan çıkışlar. Yerel olmayan çıkışların
birkaç türü vardır;
bunların her biri, yapılandırılmış ve yapılandırılmamış olarak iki
kategoriye
ayrılabilir. Yapılandırılmamış yerel olmayan
çıkışlar, bilgisayar
mühendisliği fakültesinde hocalarınızın asla
yapmamanızı söylediği şeylerdir,
çok korkulan goto
(git) komutu
gibi.
Aslında, bilinçli bir şekilde ve doğru yerde
kullanıldığında, yapılandırılmamış
yerel çıkışlar çok kullanışlı olabilir. Bunlar,
örneğin, karmaşık akış
denetimi içeren programların okunabilirliğini artırabilir.
Karmaşık akış denetiminin kendisi
iç içe geçmiş bir yapıda değilse, onun
üzerine iç içe geçmiş bir
yapı getirilmesi, onu daha fazla
değil, tersine daha az okunabilir hale getirir. Goto
kodlarının
kullanılmasının avantaj ve dezavantajlarıyla ilgili ek bilgi almak
için bu makalenin
sonunda yer alan Kaynaklar
bölümündeki bağlantılara bakın.
Yapılandırılmış yerel olmayan çıkışlar tekniğine gelince, muhtemelen en sık kullanılan türünü tanıyorsunuzdur: istisnalar. Son 20 yıldır kafanızı kaldırmadan C, Fortran ve Cobol programlıyorsanız, işte size istisnalara kısa bir giriş.
İstisna, kod içindeki bir hatanın işaretlenmesi ve yerelleştirilmesi için kullanılan bir yöntemdir. Genellikle, bir hata oluştuğunda, hatanın ele alınmasını ve siz açıkça belirtmedikçe işin geri kalanının sürmemesini istersiniz. Örneğin, Java dilindeki bu basit veritabanı koduna bir bakalım:
Liste 1. Basit veritabanı kodu örneği
//NOTE -- this code will not compile, but that's on purpose. import java.sql.*; ... public static int testdbcode(Connection conn) { //Prepare the statement PreparedStatement s = conn.prepareStatement( "select id from test_table where name = ?"); //Set the parameters s.setString(1, "fred"); //Run the query ResultSet rs = s.executeQuery() //Move to the first result row rs.next(); //Get the answer (first result parameter) int id = rs.getInt(1); return id; } ... |
Bu kodun sorunu, hata işlemesi olmamasıdır ve veritabanlarıyla ya da başka dış varlıklarla uğraşırken, hatalar hemen her yer noktada ortaya çıkabilir. Kod içinde herhangi bir yerde bir hata oluşursa, devam etmenin pek bir anlamı yoktur. Örneğin, sorgumu hazırlarken bir hata oluştuğunda, parametreleri ayarlamanın, sorguyu çalıştırmanın ya da yanıta erişmenin pek bir yararı olmaz. İlk hatadan sonra sorgu artık işe yaramaz sayılır. Bu nedenle, Java'da bir kod bloğunu kapatmanıza olanak tanıyan istisnalar vardır, böylece kod bloğunun bütünü ilk hatada atlanır. Bunu Java dilinde gerçekleştirmek için, kodu aşağıdaki biçimde yeniden yazarsınız:
Liste 2. İstisna işleme gerçekleştiren basit veritabanı fonksiyonu
import java.sql.*; ... public static int testdbcode(Connection conn) { try { //Prepare the statement PreparedStatement s = conn.prepareStatement( "select id from test_table where name = ?"); //Set the parameters s.setString(1, "fred"); //Run the query ResultSet rs = s.executeQuery() //Move to the first result row rs.next(); //Get the answer (first result parameter) int id = rs.getInt(1); return id; } catch (Exception e) { //Put error handling code here return -1; } } ... |
Try
bloğunun içindeki kod,
tamamlanıncaya dek ya da bir
deyim hata durumuyla sonuçlanıncaya dek
yürütülür. Bir hata
atılırsa (throw), try
bloğunun içindeki kodun geri kalanı atlanır ve
catch
bloğu, istisna bilgilerini tutan e
değişkeni ile yürütülür. Java'da
atılan hataların kendileri de tam sınıflardır,
böylece, istisnanın içine istendiği kadar bilgi
yerleştirilebilir. Aslında, her
biri özel bir istisna sınıfını işleyen birden çok catch
blokları oluşturulabilir.
Yukarıdaki kod örneğinde, sistem tarafından
oluşturulan
istisnaları ele aldık. Ancak, uygulama düzeyindeki istisnalar
da
aynı şekilde oluşturulup işlenebilir. Uygulama her an throw
anahtar sözcüğünü
kullanarak bir istisna işaretleyebilir. Örneğin, diyelim ki
yukarıdaki kodda 0 gibi
bir tanıtıcı numarası aldık, bu bir uygulama düzeyinde
istisnadır. Aşağıdakileri yapabiliriz:
Liste 3. Uygulama düzeyinde istisna atma içeren basit bir veritabanı örneği
import java.sql.*; ... public static int testdbcode(Connection conn) { try { //Prepare the statement PreparedStatement s = conn.prepareStatement( "select id from test_table where name = ?"); //Set the parameters s.setString(1, "fred"); //Run the query ResultSet rs = s.executeQuery() //Move to the first result row rs.next(); //Get the answer (first result parameter) int id = rs.getInt(1); //Check for application exception if(id == 0) { //Signal the exception //Assume that InvalidUserIDException is defined elsewhere throw new InvalidUserIDException(); } return id; } catch (InvalidUserIDException e) { //Specialized error handling here return -1; } catch (Exception e) { //Handle all other errors here return -1; } } ... |
Ayrıca, istisnayı işleyen kodun fonksiyonun içinde
bulunması için bir neden
yoktur. Try
ve catch
deyimleri herhangi bir fonksiyonun içinde yer alabilirler.
İstisna işleme
mekanizması, uygun bir istisna işleyiciye ulaşıncaya dek yığını
bırakacaktır
(unwind), bu noktada da istisna işleme kodunu
çalıştıracaktır. Yapılandırılmış yerel
olmayan çıkışları gerçekleştirme yeteneği,
kodların yazılmasını da, bakımını da çok
kolaylaştırır çünkü bazı durumlarda hata
işleme, işi gerçekleştiren asıl koddan
bütünüyle ayrılabilir. Elbette, kolaylığı
yanıltıcıdır.
İstisna işlemenin bu makalenin kapsamına girmeyen bir kaç
başlıca püf noktası
vardır, ancak Kaynaklar
bölümündeki bağlantılardan
bazılarında bunlara ilişkin yönergeler bulabilirsiniz.
Gelişmiş akış denetimi yapısının bir başka tipi de üretici (generator) fonksiyonudur. Üreticiler, bir takım değerleri üretebilen ve fonksiyon çağırıldığında onları birer birer döndürebilen fonksiyonlardır. Üretici, programlamayı kolaylaştırmak üzere geçerli fonksiyondaki yerini "işaretleyebilir".
Örneğin, 1 ile başlayan ve her çağırdığınızda artan bir dizi sayı döndüren bir fonksiyon istediğinizi varsayalım. Bunun kapatmalar ve hatta genel bir değişkenle yapılması güç olmasa da, bunu üreticilerle de çok kolay bir şekilde yapabilirsiniz. Aşağıda, Python'un üretici uygulaması ile bir örnek sunulmuştur:
Liste 4. Basit bir Python üretici programı
#This is our generator function def sequence(start): current = start while true: yield current current = current + 1 generator_func = sequence(1) #Create a new sequence generator starting at 1 print generator_func.next() #prints 1 print generator_func.next() #prints 2 print generator_func.next() #prints 3 |
Gördüğünüz gibi,
üretici her seferinde fonksiyonda kaldığı yere döner,
sonra da bir yield
deyimi buluncaya dek devam eder. Bu tipte bir "işaretleme" ve "işaretli
yerden
devam etme" özelliği, pek çok dilde standart bir
özellik değildir, ancak bunlar oldukça
yararlıdır ve karmaşık mantığı okunabilir ve kolay programlanabilir
hale getirirler.
Gelişmiş akış denetiminin başka bir tipi de, Prolog gibi programlama dillerinde sıkça kullanılan mantık programlamasıdır. Prolog'da, bilgisayara bir takım tanımlar verirsiniz ve o da "büyüleyici bir biçimde" sorgularınızı yanıtlar ve sizin için değerler belirler. Örneğin, aşağıdaki Prolog programına bakın (büyük harfler değişkenleri gösterir):
Liste 5. Prolog dilinde basit bir mantık programı
likes(john,sports). %this asserts that john likes sports likes(john,food). %this asserts that john likes food likes(amy,X) :- likes(john,X). %amy likes X if john likes X likes(brad,food). %brad likes food likes(brad,television). %brad likes television %query for a value that both amy and brad like, and write it out. ?- likes(amy,X), likes(brad,X), write(X). |
Bu kodun işleyişinde, Prolog john ve brad'ın sevdiği şeylerin
bir
listesini oluşturur. Ayrıca amy'nin neyi sevdiğine ilişkin kuralın
john'un sevdiği
şeyler olduğunu da belirtir. Ardından, sorguyu
yürüttüğünüzde,
önce "amy neyi
sever?" sorusunu yanıtlar. Bu sorunun yanıtı, "john'un sevdiği
şeyler"dir. Ardından,
john'un sevdiği şeyler listesine gider ve ilk öğeyi
çekip alır, bu da spordur (sports). Sonra
bir sonraki öneriye gider, o da brad'in amy'nin sevdiği
şeyleri sevdiğidir
(bu ifadede X
olarak belirtilir). Ancak,
brad'in öğeler listesinde
spor yoktur. Bu nedenle, Prolog geriye döner
(backtracking) ve amy'nin
listesinden X
öğesine yeni bir değer
bulur. Bir sonraki değer yiyecektir (food). Sonra
gidip brad'in listesinde yiyeceğin bulunup bulunmadığını denetler.
Listede yiyecek
vardır, bu durumda bir sonraki adıma geçer, bu da X
'e
hangi değerin
bulunduğunun yazılmasıdır.
Bu tür bir programlamaya mantık programlaması, adı verilir, çünkü hedefleri mantıksal ilişkiler çerçevesinde belirtmenize olanak tanır ve bu mantıksal ilişkilere uygun çözümler bulmak için tüm ayak işlerini bilgisayarın yapmasını sağlar. Bizim amaçlarımız açısından en önemli kısmı, bu programlama tipinin kurduğu benzersiz akış denetimidir; geriye dönüş. Buna göre, hesaplamanın herhangi bir noktasında, belirli bir değişkenin içinde uygun bir değer bulunamazsa ya da bir grup değişken arasındaki belirli bir ilişki doğru değilse, program "geriye dönüp" yeni bir değer atayabilir. Bu tür programlama, özellikle de akıllı sistemler alanında, çok sayıdaki sorun kümelerini basitleştirir.
Sürdürmeler: Akış denetimi için çok amaçlı bir olanak
Buraya kadar üç tip gelişmiş akış denetimi inceledik: yerel olmayan çıkışlar, üretim fonksiyonları ve geriye dönüş. Bunların ortak noktası nedir? Temelde, her biri, program yığını ve yönerge işaretçisi ile bir miktar jimnastik yapar. Yerel olmayan çıkışlarda, çıkış işleme bloğunun yanı sıra, çıkış bloğunu içeren yığın çerçevesi de işaretlenir. Yerel olmayan çıkışı çağırdığınızda, yığın işaretlenen noktaya dek geri sarılır (unwind) ve yönerge işaretçisi işleyici bloğa taşınır. Üretim fonksiyonlarında, üreticiyi içeren değişken, üretici fonksiyonunun yığın çerçevesini ve üreticinin fonksiyon içinde en son kaldığı noktayı gösteren bir işaretleyici içerir. Geriye dönüş işleminde ise, atamanın gerçekleştiği yerde bir işaret tutulur ve hesaplama başarısız olursa ve geriye dönülmesi gerekirse akış denetimi o yere döndürülür.
Bu işaretlere "sürdürme noktaları"
denebilir; bunlar, gelişmiş
akış denetimi yapısı çağrıldığında programın
sürdürüleceği yerlerdir. Ya da, daha
doğru bir ifadeyle bunlar sürdürmeler
olarak bilinir. Aslında, tüm bu denetim
yapıları tek bir akış denetimi fonksiyonu kullanılarak
uygulanabilirler:
call-with-current-continuation
(geçerli sürdürme ile çağır).
Call-with-current-continuation
,
Scheme programlama dilinde,
geçerli yığın çerçevesini ve
yönerge işaretleyicisini alan ve bunları tek bir
çağrılabilir varlık (sürdürme)
olarak paketleyen bir fonksiyondur ve sürdürme
ile birlikte başka bir fonksiyonu onun tek parametresi olarak
çağırır. Sürdürme, tek
bir parametre alan, çağırılabilir bir varlıktır ve o
parametre daha sonra sürdürmenin
oluşturulduğu noktaya döndürülür.
Bu anlatılanları karmaşık bulmuş olabilirsiniz,
öyledir de. Uygulamada nasıl işlediğini görmek
için birkaç örneğe bakalım.
Önce, sürdürme kullanılmasına ilişkin kısa bir örnek:
Liste 6. Sürdürme örneği
(display ;;Calling the continuation will return the parameter as the return value to ;;the call-with-current-continuation function. This is the "bookmarked" location (call-with-current-continuation ;;Continuation variables are often written with a "k" or "kont" to remind ;;yourself that it is a continuation. In this example, "kont" will be ;;the continuation. It is a function, that, when called, will return its ;;parameter to the bookmarked location. (lambda (kont) ;;For this example, we will simply run the continuation. This returns ;;the number 5 to the "bookmarked" location (kont 5)))) (newline) |
Önceki program yalnızca 5 sayısını
görüntüler. Sürdürmenin
tek bir
parametreyle çağrılmasının o değeri işaretlenen yere
döndürdüğüne dikkat
edin. Şimdi biraz daha karmaşık bir örneği inceleyelim. Bu
örnekte, sürdürmeyi erken
çıkış olarak kullanacaksınız. Bu örnek yapmacık
olabilir, ancak durumu iyi örnekler. Bu
programda, bir listedeki her sayının karesini alacaksınız. Ancak,
listede sayı
olmayan öğeler varsa, program listeye
döndürmek yerine *error*
simgesini döndürür.
Liste 7. Sürdürme ile hata durumlarından erken çıkış
(define a '(1 2 3 4 5)) (define b '(1 b 2 e f)) (define (square-list the-list) ;;Bookmark here (call-with-current-continuation ;;early-exit-k will return us to our bookmark (lambda (early-exit-k) ;;run the procedure (map (lambda (x) ;;Check to see if it is a number (if (number? x) ;;yes it is, perform the multiplication (* x x) ;;no it isn't, exit _now_ with the value *error* (early-exit-k '*error*))) the-list)))) ;;This will square the list (display (square-list a)) (newline) ;;This will give the error (display (square-list b)) (newline) |
Umarız yukarıdaki örnek, size sürdürmeleri istisnaların uygulanmasında nasıl kullanabileceğinize ilişkin bir fikir vermiştir.
Sıradaki örnek sürdürmelerin başka bir olağan dışı özellik olarak, sınırsız kapsam özelliğine sahip olduklarını gösterir. Bu, istisnaların aksine, sürdürmelerin çıkı noktaları olan kod bloğunun dışında da etkinleştirilebildikleri anlamına gelir. Sürdürme ile bir "işaret" oluşturduğunuzda, bu, sürdürme değeri etkin olduğu sürece, o yığın çerçevesini de etkin kalmaya zorlar. Bu nedenle, sürdürmeyi oluşturan bloktan dönseniz bile, onu döndürürseniz, sürdürme çağrıldığında önceden etkin olan yığın çerçevesi geri yüklenir ve işlem buradan devam eder.
Aşağıdaki örnekte, bir sürdürme genel bir değişkene kaydedilir ve sürdürmenin etkinleştirilmesinden sonra, başlangıçtaki yığın çerçevesi yeniden etkinleştirilir.
Liste 8. Sürdürmelerle bir yığın çerçevesinin yeniden etkinleştirilmesi
;;Global variable for the continuation (define k-global #f) ;;We are using let* instead of let so that we can guarantee ;;the evaluation order (let* ( (my-number 3) (k ;;set bookmark here (call-with-current-continuation ;;The continuation will be stored in "kont" (lambda (kont) ;;return the continuation kont)))) ;;We are using "my-number" to show that the old stack ;;frame is being saved. When we revisit the continuation ;;the second time, the value will remain changed. (display "The value of my-number is: ") (display my-number) (newline) (set! my-number (+ my-number 1)) ;;Save the continuation in a global variable. (set! k-global k)) ;;Is "kontinuation" a continuation? (if (procedure? k-global) (begin (display "This is the first run, k-global is a continuation") (newline) ;;Send "4" to the continuation which goes to the bookmark ;;which will assign it to "k" (k-global 4)) (begin ;;This occurs on the second run (display "This is the second run, k-global is not a continuation, it is ") (display k-global) (newline))) |
Bu özelliklerle, başka her türlü özelliğe öykünen ilginç yordamlar ve makrolar oluşturabilirsiniz.
İstisnaların neye benzediğine bir bakalım:
Liste 9. İstisna örneği
try { //Code here which might generate an exception } catch(SQLException e) { //catch a specific exception //Error handling code } catch(Exception e) { //catch a general exception //Error handling code } //remaining code here |
Yani, temel olarak yapmanız gereken, aşağıdakileri oluşturan bir makro yaratmaktır:
- Hata işleme kodu bloğu
- Kalan kodun yeri
- Çalıştırılan kod
Böylece, makro genişletmesinin sonucu aşağıdaki gibi görünmelidir:
Liste 10. Varsayımsal bir istisna makrosunun istenen genişlemesi
;;This establishes the throw function as globally available, but displays an ;;error message if used without being in a try block. (define throw (lambda (x) (display "No try clause is active!") (newline))) (let* ( ;Save the old containing try block (old-throw throw) ;we are saving the results in retval because we still have to clean up after ;ourselves before we can exit (retval (call-with-current-continuation ;The exception will use this continuation to get back to the ;remaining code (lambda (k-exit-to-remaining-code) (let ( ;This defines the exit handler (error-handler (lambda (exception) (k-exit-to-remaining-code ;;error-handling code here )))) ;This sets our error handler to be the official "throw" function (set! throw error-handler) ;;Regular code here ;;You can throw an exception by doing: (throw 'test-exception)))))) ;Reinstate old try block (set! throw old-throw) ;Return the result retval) |
Bu işlem, iç içe geçme
işlemini, throw
her zaman en
içteki try
bloğuna gönderme yapacak şekilde ayarlar. Artık kodun neye
benzemesini istediğinizi
bildiğinize göre, tüm altyapının ayarlanmasını
halledecek bir makro yazabilirsiniz.
Liste 11. İstisna kodunu oluşturacak makro
;;This establishes the throw function (define throw (lambda (x) (display "No try clause is active!") (newline))) ;;establishes syntax for a "try" block (define-syntax try (lambda (x) (syntax-case x (catch) ( (try expression (catch exception exception-handler-expression)) (syntax (let* ( (old-throw throw) (retval (call-with-current-continuation (lambda (k-exit) (let ( (error-handler (lambda (exception) (k-exit exception-handler-expression)))) (set! throw error-handler) expression))))) (set! throw old-throw) retval)))))) ;;Short test suite ;Function that throws an error (define (test-function) (throw 'ERROR)) ;Function that does not throw an error (define (test-function-2) (display "No error is generated.") (newline)) ;Test out our try block (try (test-function) (catch e (begin (display "Exception! e is: ") (display e) (newline)))) (try (test-function-2) (catch e (begin (display "Exception! e is: ") (display e) (newline)))) |
Temel öğelerin çoğunu bitirdik, ancak hala birkaç sorun daha var. Sorun, sürdürmelerin karıştırılmasında ortaya çıkıyor. Örneğin, istisnalarda sürdürmeler kullanmanın yanı sıra, onları başka tipte erken çıkış mantıklarında da kullanırsanız, o zaman bir sorunla karşılaşırsınız. Aşağıdaki koda bir bakın:
Liste 12. Kötü sürdürme etkileşimleri
;;Uses previously defined try/catch macro (try (begin (call-with-current-continuation (lambda (kont) (try (kont 'value) ;;jumps out of the continuation, but also skips resetting ;;the active continuation (catch e (begin (display "Interior exception handler. Exception e is: ") (display e) (newline)))))) ;;Because we didn't exit out through the try block in a normal fashion, this will ;;actually send us _back_ to the interior catch block the first time it is called! (throw 'ERROR)) (catch e (begin (display "Exterior exception handler. Exception e is: ") (display e) (newline)))) |
Gördüğünüz gibi, serbest
bir şekilde oradan buraya atlama
yeteneği kırtasiye işlemlerinde bazı
güçlüklere yol açabilir. Bu
sorunları
hafifletmek için Scheme'de dynamic-wind
(dinamik sarma) adı verilen
özel bir yordam bulunur. Dynamic-wind
yordamı, sürdürme belirli bir
yığın çerçevesini atladığında, bu durumu saptar.
Sürdürme ileri geri
atladığında, bu yordamı kullanarak yığını ilk durumuna
getirebilirsiniz. Dynamic-wind
üç bağımsız değişken alır, bunların her biri sıfır
bağımsız değişken (zero-argument) yordamıdır. İlki, bir yığın
çerçevesine her
girişinizde çalışacak olan yordamdır. Bir sonraki asıl
çalışacak olan yordamdır. Sonuncusu da yığın
çerçevesinden her çıkışınızda
çalışacak
olan yordamdır. Aşağıdaki örnekte dynamic-wind
yordamının nasıl
çalıştığına ilişkin kısa bir örnek yer alır:
Liste 13. Dynamic wind kullanımı örneği
(let ( (k-var (call-with-current-continuation (lambda (kont) (dynamic-wind (lambda () (display "Entering frame") (newline)) (lambda () (begin (display "Running") (newline) (call-with-current-continuation (lambda (inner-kont) ;;Exit across the dynamic-wind boundary, ;;saving the current continuation (kont inner-kont))))) (lambda () (display "Leaving frame") (newline))))))) (if (procedure? k-var) (k-var 'the-value) (begin (display k-var) (newline)))) |
Bu örnek önce bir dış
sürdürme oluşturur. Sonra, "giriş" yordamını
çağırarak yığın çerçevesine girer.
Ardından, dynamic-wind
içinde
yeni bir sürdürme üreten bir yordam
çalıştırır. Bu sürdürme sonra
başlangıçtaki sürdürmeye
döndürülür. Ancak, dynamic-wind
satırını geçtiği için, "ayrılma" yordamı
yürütülür. Daha sonra,
iç sürdürme
yeniden yürütülür, bu, denetimi
yeniden dynamic-wind
üzerinden
taşır, bu da "giriş" yordamını yeniden çağırır. Sonra dynamic-wind
boyunca geri döner, bu da "ayrılma" yordamını yeniden
çağırır.
Bu çağrı sırası biraz karışık gelebilir, ancak
dynamic-wind
, çok ileriye uzanan
sürdürmelere karşı bir "koruma"
çizgisi olarak
düşünülürse anlamlı gelebilir. Akış
denetiminin, "koruma" çizgisinin
ötesine geçebilmesi için
(sürdürmeyle ya da olağan akış denetimi sayesinde),
temizlik için uygun yordam
yürütülmelidir (yönüne
göre, "giriş" ya da "çıkış").
Bunu kullanarak, try
makrosundaki
bazı sorunlardan
korunabilirsiniz. Kodun hangi try
/catch
bloğunda
yürütüldüğünü ilk
duruma getirmek için dynamic-wind
yordamını
kullanabilirsiniz. Kod şöyledir:
Liste 14. Dynamic-wind ile iyileştirilmiş try/catch
;;This establishes the throw function as globally available, but displays an ;;error message if used without being in a try block. (define throw (lambda (x) (display "No try clause is active!") (newline))) ;;establishes syntax for a "try" block (define-syntax try (lambda (x) (syntax-case x (catch) ( (try expression (catch exception exception-handler-expression)) (syntax ;;Exit point using continuation k-exit (call-with-current-continuation (lambda (k-exit) (let ( ;;These are the two exception handlers, the old and the ;;new. Dynamic-wind sets them up and tears them down ;;upon entering and exiting from scope (old-throw throw) (error-handler (lambda (exception) (k-exit exception-handler-expression)))) (dynamic-wind ;;Entering scope -- set exception handler (lambda () (set! throw error-handler)) ;;Actual processing (lambda () expression) ;;Exitting scope -- set exception handler to old value (lambda () (set! throw old-throw))))))))))) |
Bu örnek biraz daha kısadır ve hem
başlangıçtaki sınama
örneklerini hem de sürdürmeleri kullanan
örneği sağlar. Ayrıca,
dynamic-wind
ile yeni bir denetim yapısı
ekleyerek hile yaptığınızı
düşünüyorsanız, Kent Dybvig, dynamic-wind
yordamının
call-with-current-continuation
'a dayanarak
uygulanabileceğini
göstermiştir.
Try
/catch
yordamlarının beklenmedik davranışlar
sergileyebileceği yerlerin hepsini ele almamış olsak da, bu kadarı
çoğu durum
için yeterlidir. Sıradaki bölümde olası
sorunlardan bazılarına değinilir.
Yukarıda belirtildiği gibi, üreticiler, akış denetiminin bir biçimidir. Python, üreticileri uygularken en sık kullanılan dildir. Şimdi, üreticilerin nasıl çalıştıklarını ve sürdürmelerin onları uygulamakta nasıl kullanılabileceklerini ele alalım.
İlk olarak, bir üretici oluşturulur. Bu, yığın çerçevesinin ya sürdürme ya da kapatma aracıyla kaydedilmesini gerektirir. Ardından, bir değer döndürürsünüz ve fonksiyon içindeki mevcut konumunuzu işaretlersiniz. Bu, döndüğünüz yeri zaten işaretlemiş olmanız gerektiği anlamına gelir.
Böylece, üreticimizde sürdürmelerle uygulanan iki işaret vardır. İlk işaret, üretici ilk yaratıldığında oluşturulan bir değişkendir. Bu, üretici fonksiyonunun içinde bulunduğu konumu tutar. Ardından, üreticiyi çalıştırdığınızda, çağırma fonksiyonunda dönüş noktası olan bir sürdürmeniz olur. Dönmeden hemen önce, mevcut konumun bir sürdürmesini oluşturursunuz ve onu bir sonraki çağrı için kaydedersiniz.
Şimdi, Python tarzındaki üreticiler için isteyebileceğiniz Scheme arabirimine bakalım:
Liste 15. Python tarzında bir üretici kullanımı örneği
(define-syntax generator (syntax-rules () ( (generator (yieldfunc) body ...) (let ( (placeholder #f) ;;placeholder in this function (return-proc #f) ;;how to get out (finished #f)) ;;whether or not it is finished ;;this is the generator entrance (lambda () (call-with-current-continuation (lambda (tmp-return-proc) ;;save the way out in "return-proc" (set! return-proc tmp-return-proc) (let ( ;;"yieldfunc" resets the placeholder and returns the value (yieldfunc (lambda (x) (call-with-current-continuation (lambda (current-placeholder) ;;new placeholder (set! placeholder current-placeholder) ;;return value (return-proc x)))))) ;;If the generator is done, return a special value (if finished 'generator-finished ;;If this is the first time, there will not be a ;;placeholder established, so we just run the body. ;;If there is a placeholder, we resume to that point (if placeholder (placeholder 'unspecified) (let ( (final-value (begin body ...))) (set! finished #t) (return-proc final-value)))))))))))) (define sequence-generator ;;Initial parameters (lambda (start end increment) ;;"yield" will be used to generate a single value (generator (yield) ;;Main function body (let loop ((curval start)) (if (eqv? curval end) curval (begin ;;yield the value (yield curval) ;;continue on (loop (+ curval increment)))))))) ;;Example usage (define my-sequence (sequence-generator 1 3 1)) (display (my-sequence))(newline) (display (my-sequence))(newline) (display (my-sequence))(newline) (display (my-sequence))(newline) |
Bu kodu izlemek biraz güçtür. Üreticide başka değerler ya da başka bir durum sorgusu olup olmadığını sorgulama yeteneği eklendiğinde kod çok daha karmaşık olur. Yine de, iki üreticiye yayılan fonksiyonlara dikkat edin. Biri çağırma programına dönüş, diğeri ise üreticinin yürütüldüğü mevcut yerdir. Dönüş yordamının, üreticinin tümünü kapsayan bir değişkende depolanması tuhaf gelebilir, ancak sürdürme çağrısından sonra değişkenin doğru versiyonunun etkin olması için böyle olmalıdır. Aksi halde, ilk çağrıdan sonra, yer tutucu sürdürme oluşturulduğunda etkin olan versiyona döner.
Yukarıda sözü edildiği gibi, istisna tabanlı
sürdürmeler sorunlara yol
açabilir. Temel olarak, soru şudur; bir üreticiyi
başlattığınızda bir
try
bloğunuz ve bir üreticiyi
çalıştırdığınızda bir try
bloğunuz varsa ve üretim fonksiyonunda bir istisna atılırsa,
hangi
catch
bloğu çalıştırılır? Benim
kullandığım
uygulamalarda, ilk catch
bloğu kullanılır. Bu
en kolay yöntem
midir? Duruma göre değişir. Ancak, bu tipte
sürdürme-sürdürme etkileşimleri
sorunlu olabilir, çünkü "uygun" işlemin ne
olacağı açık seçik bilinmez.
Sürdürme kullanılan geriye dönüş
Son olarak, geriye dönüşe bir bakalım.
Geriye dönüş için kullanılan arabirim
amb
adında bir fonksiyondur. Bu fonksiyon bir
dizi değer alır. amb
, her
değer için bir geriye dönüş işareti
belirler. Listedeki (amb:fail
fonksiyonunu çağırarak gösterilen) mevcut değer
çalışmazsa, program,
son işaretli yere "geri döner" ve yeni bir değer dener.
Parametre
doğru değilse, amb:assert
adındaki bir
yardımcı program fonksiyonu
amb:fail
işareti verir. Liste 16
içinde bu fonksiyonların nasıl
kullanıldığı gösterilir:
Liste 16. Scheme'de geriye dönüşün kullanılması
(let* ( (a (amb 1 2 3 4 5 6 7)) (b (amb 5 6 7 8 9 10))) (amb:assert (> a b)) (display "a is ") (display a) (newline) (display "b is ") (display b) (newline)) |
Bu fonksiyon ilk çalıştığında, a
için 1
'i ve
b
için 5
'i
seçer. a
, b
'den
büyük
olmadığı için başarısız olur ve en son işaretlenmiş olan
geriye dönüş yerine gider. Bu durumda,
bu b
'nin atandığı nokta olacaktır. Sonra b
'nin
her bir
değerini deneyecektir. Hiç biri çalışmayacaktır,
bu nedenle
a
'nın atandığı noktaya geri
dönecektir. Sonra, b
'nin
her değeriyle 2
'yi deneyecektir. Bu, a
'nın
b
'den büyük bir
değerini buluncaya dek devam eder ve bulduğunda bu noktadan işlemi
sürdürür.
Liste 17'de uygulama gösterilir:
Liste 17. Geriye dönüşün uygulanması
;AMB definition (define amb:fail '*) (define amb:initialize-fail (lambda x (set! amb:fail (if (null? x) (lambda () (error "amb tree exhausted!")) (car x))))) (amb:initialize-fail) (define amb (lambda alternatives (letrec ( (amb-internal ;;"sk" returns the value (success continuation), ;;"alts is the list of values (lambda (sk alts) ;;fail if there are no alternatives (if (null? alts) (prev-amb-fail) (begin (call/cc ;;"fk" is where to go when an ;;alternative fails (failure continuation) (lambda (fk) ;;set the failure function to come back here (set! amb:fail (lambda () (set! amb:fail prev-amb-fail) (fk 'fail))) ;;return the first alternative (sk (car alts)))) ;;We get here after a failure, so we ;;run the function again using the rest ;;of the list (amb-internal sk (cdr alts)))))) ;;this is the previous failure function (prev-amb-fail amb:fail)) (call/cc ;;bookmark the way to the assignment into "sk" (lambda (sk) ;;go through each alternative (amb-internal sk alternatives) ;;After going through them, note that we have a failure (prev-amb-fail)))))) ;;Calling "amb" without arguments will cause a failure. This function ;;just makes it a little more obvious what is supposed to be happening. (define amb:assert-failure (lambda () (amb))) (define amb:assert (lambda (pred) (if (not pred) (amb:assert-failure)))) |
Kodu okurken, geriye dönücünün değerler listesinde geriye dönüşünün "başarısızlık sürdürmesi", fonksiyonun sıradaki değeri olağan program akışına döndürmesinin ise "başarı sürdürmesi" olduğunu unutmayın.
Sürdürmeler: Akış denetiminden hep istediğiniz her şey
Gördüğünüz gibi, sürdürmeleri her tür gelişmiş akış denetimi deyimlerini uygularken kullanabilirsiniz. Yalnızca birkaç deyimle, istisnalara, geriye dönüşlere ve başka gelişmiş akış denetimi tiplerine sürdürmeler yerleştirebilirsiniz. Ancak bu makalede yapabileceklerinizin yalnızca pek azına değindik. Sürdürmelerle ayrıca Web uygulamalarının daha geleneksel akış denetimi yapılarına dönüştürülmesi ve kullanıcı düzeyinde iş parçacıkları uygulanması gibi işler de yapabilirsiniz. Ne yazık ki, pek çok dil sürdürmeleri uygulamamıştır ve dolayısıyla, kullanıcılarını çok sayıdaki akış denetimi özelliğinden mahrum bırakırlar. Bir dilde sürdürmeler bulunuyorsa, diğer gelişmiş akış denetimi özelliklerini neredeyse rutin bir şekilde uygulayabilir.
Bilgi Edinme
-
Jonathan Bartlett'ın diğer makalelerini developerWorks'den
okuyun.
Goto
gibi yapılandırılmamış yerel çıkışlara ilişkin sorunlar Edsger Dijkstra'nın 1968 yılında "Go To Statement Considered Harmful" (Communications of the ACM), October 1995) adlı makaleyi yayınladığından bu yana iyi biliniyor.
- Linus Torvalds ve diğerleri goto ile ilgili
görüşlerin abartıldığını
düşünüyorlar.
- Linux
Device
Drivers adlı kitap, yapılandırılmamış
çıkışların kullanılmasının nasıl çok
daha iyi olabileceğine ilişkin güzel bir örnek
veriyor.
- Sun'ın Web sitesinde,
Java'da
istisnaların nasıl atıldığına ilişkin bir öğretici
bulunur.
- "Best
Practices for Exception Handling" (O'Reilly Network, Kasım
2003)
başlıklı makalede, Java dilinde istisna işlemenin zor yanları
tartışılır.
- "Charming
Python: Iterators and simple generators" (developerWorks,
Eylül 2001)
başlıklı makalede Python üreticileri anlatılır.
- Herkes Python
üreticileriyle
ilgili püf noktaları bilmeli.
- Nasıl mantıksal programlama yapacağınızı öğrenmek
için,
Learn
Prolog
Now! , yeni başlayanların kendi kendilerine
çalışabilecekleri bir derstir.
- Sürdürmeler olmaksızın
C
dilinde
geriye dönüş gerçekleştirmek
için neler yapmanız gerektiğini buradan
öğrenebilirsiniz.
- Sürdürmelerin nereden
çıktığına ilişkin ayrıntılı bilgi edinmek isterseniz,
Histories
of Discoveries
of Continuations: Belles-Lettres with Equivocal Tenses
başlıklı yazıda
tarihçeleri bulabilirsiniz.
- "Use
continuations to develop complex Web applications"
(developerWorks,
Eylül 2004) başlıklı makalede, Cocoon'un Java Web
programlamada nasıl sürdürmeleri
desteklediğini gösterilir. Bu, ayrıca,
Scheme'de
ya da
Java
tabanlı bir Scheme'de de gerçekleştirilebilir.
- developerWorks
Linux zone, Linux geliştiricileri için
birçok kaynak sunar.
- developerWorks teknik etkinlikler ve Web yayınları sayesinde güncel bilgiler edinin.
Ürün ve teknoloji edinme
- SEK
for Linux ürününü sipariş
edin. Bu ürün, DB2®, Lotus®,
Rational®, Tivoli® ve WebSphere®
üzerinde Linux için en güncel IBM
deneme yazılımlarını içeren ikili DVD setidir.
- Doğrudan developerWorks sitesinden
yükleyebileceğiniz
IBM
deneme yazılımını kullanarak yeni geliştirme projenizi Linux
üzerinde oluşturabilirsiniz.
Tartışma
- developerWorks
Web
günlüklerine bakın ve
developerWorks
topluluğuna katılın.
Jonathan Bartlett Programming from the Ground Up adlı, Linux çevirici dili kullanılarak programlamaya giriş kitabının yazarıdır. Bartlett, New Medio'da baş geliştiricidir ve istemciler için Web, video, kiosk ve masaüstü uygulamaları geliştirmektedir.