Nesnemiz Kalmadı Onun Yerine "Closure" Versek?
Ne demiştik? Elimizde sınıf, nesne, metod, vs. mekanizmasının olmadığını var sayacaktık ama en nihayetinde elimizde Common Lisp varsa, o zaman fonksiyon var demektir, lambda var demektir. Halbuki biz istiyoruz ki mesela "hesap" gibi bir nesnemiz olsun, bu hesabın bir ad özelliği olsun, sonra mesela bakiye ayarlayan, para çekip para yatırmamızı sağlayan bazı metodları olsun. Bir nevi OO programlama olsun, hayat daha güzel olsun...O halde şimdi sadece defun, lambda, vb. Lisp yapılarını kullanarak bir fonksiyon yazalım, öyle bir fonksiyon olsun ki ismi yeni-hesap olsun, "ad" isimli bir parametre alsın, bir de eğer verirsek aldığı ama vermezsek birtakım varsayılan değerler atadığı "bakiye" ve "faiz-oranı" gibi iki parametre daha olsun. Ve bu fonksiyon bize aslında basit bir değer yerine bir fonksiyon döndürsün, öyle bir fonksiyon döndürsün ki bu döndürdüğü şey de aldığı "message" isimli parametrenin içeriğine bakıp iş güç yapan bir fonksiyon olsun. Yeterince kafa karıştırıcı oldu mu? İşte bu kafa karışıklığını gidermek için doğal dil yerine Common Lisp kullanıyoruz ve şunu yazıyoruz:
(defun yeni-hesap (ad &optional (bakiye 0.00) (faiz-orani .06))
"Belli mesajlara tepki veren yeni bir hesap yaratalim"
#'(lambda (mesaj)
(case mesaj
(cek #'(lambda (miktar)
(if (<= miktar bakiye)
(decf bakiye miktar)
'yetersiz-bakiye)))
(yatir #'(lambda (miktar) (incf bakiye miktar)))
(bakiye #'(lambda () bakiye))
(ad #'(lambda () ad))
(faiz #'(lambda ()
(incf bakiye
(* faiz-orani bakiye)))))))
Pekiyi güzel ama yukarıdaki fonksiyonu çalıştırırsak ne döndürecek?
Süper etkileşimli REPL (Read Eval Print Loop) ortamına geçelim
ve deneyelim:
CL-USER> (yeni-hesap "fz'nin banka hesabi")
#
Çok anlaşılır görünmüyor. Acaba bu döndürdüğü şeyi bir değişkene
atasak?
CL-USER> (defvar *hesabim* (yeni-hesap "fz'nin banka hesabi" 100 0.09))
*HESABIM*
CL-USER> *hesabim*
#
Hala çok anlaşılır gibi değil. Bir "closure" yarattığımızı ve bunu
*hesabim* isimli bir değişkene yerleştirdiğimizi belirtiyor ama acaba
gerçekten nesne benzeri bir şey yarattık mı? (Bu arada eğer başarılı olduysak
havadan 100 YTL sahibi olacağım, üstelik %9 faiz oranı ile, o halde
devam etmeye ve denemeye değer! ;-)
Şimdi madem fonksiyon döndüren fonksiyon gibi bir şey yazdığımızı söylemiştik o halde, *hesabim* gerçekten de böyle garip bir varlık mı bir kontrol edelim:
CL-USER> (funcall *hesabim* 'ad)
# (funcall *hesabim* 'bakiye)
#
CL-USER> (funcall *hesabim* 'yatir)
#
*hesabım* isimli "closure"a "madem ki sen closure türünden bir şeysin,
fonksiyon döndüren bir şeysin ve öyle bir fonksiyon ki bir parametre
bekliyorsun o halde seni funcall ile çağırayım ve beklediğin parametreyi
vereyim" dedik ve yukarıdaki birkaç sonucu elde ettik. Bize yine
"closure" döndürdü, yani çağrılmayı bekleyen fonksiyonlar. O halde
zincirdeki son halkayı tamamlayalım:
Yukarıdaki ilk satıda dikkat ederseniz *hesabım*'a 'ad parametresi geçildiğinde bize onunla ilgili bir "closure" yani çağrılmayı bekleyen bir fonksiyon döndürüyor, pekiyi bu dönen fonksiyon parametre olarak ne bekliyor? Hiçbir şey. Yani NIL. O halde:
CL-USER> (apply (funcall *hesabim* 'ad) nil)
"fz'nin banka hesabi"
Şimdi durum biraz berraklaşmaya başladı gibi değil mi? Ama hala çok
ilkel durumdayız. İşleri kolaylaştıracak birkaç fonksiyon tanımlasak,
hani şu alışık olduğumuz OO tarzı var ya, bir nesneye bir mesaj (Java'cılar,
C#'çılar bunu metod diye okuyabilir) geçiyoruz, o mesaja da bazı parametreler
veriyoruz ve sonuç elde ediyoruz filan yani şöyle bir yapımız olsa:
send nesne mesaj p1 p2 p4 ... pN
bir nevi
nesne.metod(p1, p2, ..., pN)
gibi bir şey elde etmez miyiz? O halde gelin kendimizi her seferinde funcall ve apply yazmaktan kurtaralım:
(defun get-method (nesne mesaj)
"Bu nesne icin verilen mesaja göre ilgili metodu döndür."
(funcall nesne mesaj))
(defun send (nesne mesaj &rest args)
"Mesaji cagirmak icin gerekli metodu elde ettikten sonra
buna gerekli argümanlari verip sonucu döndür."
(apply (get-method nesne mesaj) args))
Bakalım mekanizma nasıl işliyor:
CL-USER> (send *hesabim* 'ad)
"fz'nin banka hesabi"
CL-USER> (send *hesabim* 'bakiye)
100
CL-USER> (send *hesabim* 'yatir 100)
200
CL-USER> (send *hesabim* 'bakiye)
200
CL-USER> (send *hesabim* 'faiz)
218.0
CL-USER> (send *hesabim* 'faiz)
237.62
CL-USER> (send *hesabim* 'faiz)
259.0058
Oturduğum yerden para kazanmakla kalmadım, ufak ve sevimli bir nesne
elde ettim. Pardon "closure". Ya da bir tür nesne. İsmin ne önemi var
(bazıları için var tabii, buzzword şeklinde ama şimdilik bununla
işimiz yok). Bir taşla kuş katliamına gittikçe yaklaşıyoruz.
Şimdi yukarıdaki durum, yukarıdaki sözdizimi bazı Lispçileri rahatsız etmiş olabilir ve Norvig'in sorduğu gibi şu soruyu sormuş olabilirler:
- Bir sürü hesap olsa elimizde ve bunların bakiyelerini öğrenmek istesek bunun için geleneksel Lisp tarzı olarak
(mapcar 'bakiye hesaplar)
gibi bir şey kullanamayacak mıyız yani?
Mevcut durumda maalesef şöyle yazmak zorunda kalıyoruz:
(mapcar #'(lambda (hesap) (send hesap 'bakiye)) hesaplar)
Bu durumu düzeltmek, kolaylaştırmak gene biz Lisp programcılarının elinde. Mesela cek metodunu şu şekilde yazsak:
(defun cek (nesne &rest args)
"cek ismini nesneler üzerinde calisan generic bir fonksiyon
olarak tanimla."
(apply (get-method nesne 'cek) args))
ve bir de yukarıdaki "tarz"ı denesek:
CL-USER> (cek *hesabim* '10000000)
YETERSIZ-BAKIYE
CL-USER> (cek *hesabim* '195)
64.0058
Biraz para kaybettik ama olsun, Lisp tarzına biraz daha yaklaşabilmek için
buna değer doğrusu.
Sınıfsız OO Bir Ütopyadır
Okurun aklına şu gelebilir: her seferinde bir nesneye ihtiyaç duyduğumda gidip öyle defun, lambda filan mı kullanacağım uzun uzun, hani OO programlama yapıyorduk, nerede sınıflar? Sınıf tanımlama mekanizmasını göster bana.Gayet haklı, makul bir talep! Bir tür sınıf tanımlama yapısının olması şart. Yine elimizde CLOS (Common Lisp Object System) olmadığını var sayıp devam edelim ve Common Lisp'teki "macro"ların gücünü kullanalım (kemerlerinizi bağlayın!):
(defmacro define-class (class inst-vars class-vars &body methods)
"Nesneye yönelik programlama icin sinif tanimlama macrosu."
;; constructor ve metodlar icin generik fonksiyonlari tanimla
`(let ,class-vars
(mapcar #'ensure-generic-fn ',(mapcar #'first methods))
(defun ,class ,inst-vars
#'(lambda (message)
(case message
,@(mapcar #'make-clause methods))))))
(defun make-clause (clause)
"define-class'tan gelen mesaji case yapisinin icine yerlesecek sekle cevir."
`(,(first clause) #'(lambda ,(second clause) .,(rest (rest clause)))))
(defun ensure-generic-fn (message)
"Eger daha önce tanimlanmadiise mesaj icin nesneye yönelik
bir 'dispatch' fonksiyonu tanimla."
(unless (generic-fn-p message)
(let ((fn #'(lambda (object &rest args)
(apply (get-method object message) args))))
(setf (symbol-function message) fn)
(setf (get message 'generic-fn) fn))))
(defun generic-fn-p (fn-name)
"Bu generic bir fonksiyon mu."
(and (fboundp fn-name)
(eq (get fn-name 'generic-fn) (symbol-function fn-name))))
Kısa ama yoğun bir Lisp kodu. Tek tek açıklamakla uğraşmak yerine
söz konusu makronun açılımına bir göz atmayı tercih edeceğiz:
CL-USER> (macroexpand-1 '(define-class hesap (ad &optional (bakiye 0.00)) ((faiz-orani 0.09)) (cek (miktar) (if (<= miktar bakiye) (decf bakiye miktar) 'yetersiz-bakiye)) (yatir (miktar) (incf bakiye miktar)) (bakiye () bakiye) (ad () ad) (faiz () (incf bakiye (* faiz-orani bakiye))))) (LET ((FAIZ-ORANI 0.09)) (MAPCAR #'ENSURE-GENERIC-FN '(CEK YATIR BAKIYE AD FAIZ)) (DEFUN HESAP (AD &OPTIONAL (BAKIYE 0.0)) #'(LAMBDA (MESSAGE) (CASE MESSAGE (CEK #'(LAMBDA (MIKTAR) (IF (<= MIKTAR BAKIYE) (DECF BAKIYE MIKTAR) 'YETERSIZ-BAKIYE))) (YATIR #'(LAMBDA (MIKTAR) (INCF BAKIYE MIKTAR))) (BAKIYE #'(LAMBDA () BAKIYE)) (AD #'(LAMBDA () AD)) (FAIZ #'(LAMBDA () (INCF BAKIYE (* FAIZ-ORANI BAKIYE)))))))) TDikkat edecek olursanız, macroexpand-1 ile gerçekleştirilen açılımdaki "(defun hesap..." ile başlayan kısım ta en başta bizim yazdığımıza çok benziyor.
Artık elimizde yeni yeni sınıflar tanımlamak için define-class diye bir yapı var ve bunu kullanabiliriz:
CL-USER>(define-class hesap
(ad &optional (bakiye 0.00))
((faiz-orani 0.09))
(cek (miktar)
(if (<= miktar bakiye)
(decf bakiye miktar)
'yetersiz-bakiye))
(yatir (miktar) (incf bakiye miktar))
(bakiye () bakiye)
(ad () ad)
(faiz ()
(incf bakiye (* faiz-orani bakiye))))
HESAP
Bakalım çalışacak mı? Madem bir sınıf tanımladık bundan bir "instance" oluşturalım
ve oluşan "nesne"yi de *diger-hesap* isimli bir değişken ile gösterelim:
CL-USER> (setf *diger-hesap* (hesap "fz'nin diger hesabi" 1000))
#
CL-USER> (ad *diger-hesap*)
"fz'nin diger hesabi"
CL-USER> (bakiye *diger-hesap*)
1000
CL-USER> (faiz *diger-hesap*)
1090.0
CL-USER> (cek *diger-hesap* 3456345896340598)
YETERSIZ-BAKIYE
CL-USER> (cek *diger-hesap* 99)
991.0
Artık elimizde sınıfları, sınıf değişkenlerini ve sınıf metodlarını tanımlamak
için bir yapı mevcut. OO dünyasında geçen "encapsulation" sağlanmış durumda
(kolaysa closeru'ın içine girip müdahale etmeye kalkın ;-) ve buna ek olarak
"generic" fonksiyon yaklaşımımızdan ötürü tanımladığımız metodlar, tamamen isimleri
aynı kalmak sureti ile bambaşka nesneler için de tanımlanıp çağrılabilirler. Söz
gelimi define-class
ile "silah" isimli bir sınıf ve buna özgü bir
cek
metodu tanımlayıp ardından *silahim* diye bir şey yapıp (cek *silahim*)
çağırmamız mümkün olabilir.
Bitirirken - Eve Dönüş
Tabii ki bir OO sisteminde yukarıdakilerden daha fazlası gerekir, söz gelimi hiç miras alma yani "inheritance" mekanizmasına ve başka detaylara değinmedik, çoklu miras alma yani "multiple inheritance" gibi girift konuların yakınından dahi geçmedik.Bu kısa makalenin amacı Common Lisp'i biraz daha tanıtmak, dilin esnekliğini göstermek idi. Bunun için Norvig'in örneklerinden faydalandık ve eğer Common Lisp içinde herhangi bir OO programlama yapısı olmasa idi bunu geliştirmeye nasıl başlardık, bunu birkaç örnek üzerinden inceledik.
Common Lisp ile ciddi anlamda ve gayet sofistike şekilde nesne yönelimli programlamayı merak eden okur tam teşekküllü CLOS (Common Lisp Object System) sistemini öğrenip OO programlamanın harika dünyasına ilk adımları atabilir.
Common Lisp'te nesne yönelimli programlama yapmamızı sağlayan CLOS ile ilgili detaylı bilgiler kaynakçadaki eserlerde mevcuttur.
Kaynakça
- Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp; Norvig, P.
- Practical Common Lisp; Seibel, P.
- Object-Oriented Programming in Common Lisp: A Programmer's Guide to CLOS; Keene, S.
Emre "FZ" Sevinç
16 Ekim 2005
İstanbul
Not: Yazının özgün adresi burasıdır.
Ayrıcana niye Ali değil de Alice ?
Ali bak bu Lisp, bazıları onu çok sever.
Ali Lisp'i çok sev (ya da terket aanadın mı.)
:)