
CLOS -- objektově orientované programování v Lispu
Common Lisp byl jedním z prvních programovacích jazyků, který měl
objektově orientované programování zakotveno ve svém ANSI standardu.
Standardizovaný objektový systém CLOS je při prvním přiblížení jednoduchý a
snadno zvládnutelný a přitom nabízí rysy v mnoha jiných prostředích
neobvyklé. A pod slupkou rozhraní nejvyšší úrovně se skrývají zajímavé
rysy, činící z CLOSu, jak se na Lisp sluší, nesmírně mocný nástroj.
Co je CLOS
Lisp je ze své podstaty rozšiřitelný programovací jazyk. Proto když přišlo
objektově orientované programování, nebylo třeba navrhovat nový programovací
jazyk, nýbrž stačilo objektově orientované programování do Lispu implementovat.
V důsledku toho vzniklo několik různých a nezávislých systémů objektově
orientovaného programování v Lispu. V 80. letech, kdy se pracovalo
na ANSI standardu Common Lispu, byl jako standardní objektový systém přijat
CLOS (Common Lisp Object System). Názory na vhodnost této
volby se liší, nicméně CLOS zde již pro jednou máme a jedná se o systém
nesporně zajímavý a mocný.
Třídy a metody
Na rozdíl od většiny objektově orientovaných programovacích jazyků, CLOS má
daleko volnější vazbu mezi třídami a metodami. Definuje-li se třída, definují
se pouze její parametry, tj. zejména seznam poděděných tříd, sloty
(v jiných jazycích nazývané například proměnné třídy, atributy nebo
políčka) a jejich vlastnosti, přístupové metody ke slotům, dokumentace a
metatřída.
Místo metod vázaných na jedinou konkrétní třídu CLOS používá
generické funkce, aplikované na jednu nebo
více
instancí tříd. Pokud tedy například chceme definovat funkci vykreslující
grafický prvek na výstupní zařízení, nespojujeme tuto funkci ani s třídou
grafického prvku ani s třídou výstupního zařízení, nýbrž
s oběma třídami. Tento mechanismus je obecnější a při
zavádění nových prvků a nových výstupních zařízení umožňuje přidávat potřebnou
funkcionalitu bez zásahů do původního kódu.
Představme si že máme generickou funkci render-element
s parametry element a device
(defgeneric render-element (element device))
a její metodu pro konkrétní třídy triangle grafického prvku a
screen výstupního zařízení
(defmethod render-element ((element triangle) (device screen)))
S pomocí této metody můžeme snadno definovat metodu vykreslující čtverce,
nezávislou na konkrétním výstupním zařízení:
(defmethod render-element ((element square) device)
(render-element (square-triangle-left-top element) device)
(render-element (square-triangle-right-bottom element) device))
Dále můžeme definovat metodu vykreslující trojúhelníky na černobílé
zařízení, využívající volání metody předka prostřednictvím funkce
call-next-method:
(defmethod render-element ((element triangle) (device black-and-white-device))
(call-next-method (convert-to-black-and-white element) device))
Vykreslení černobílého čtverce daného proměnnou the-square
obsahující instanci třídy square na výstupní zařízení dané
proměnnou the-black-and-white-device obsahující instanci třídy
black-and-white-device pak dosáhneme přirozeným voláním
(render-element the-square the-black-and-white-device)
Automaticky se vyvolají obě výše uvedené konverzní metody a kýženého je
dosaženo. V jazycích s metodami vázanými na jedinou třídu, jako jsou
například C++, Java nebo Python, je dosažení podobně elegantního řešení
složitější.
Pořadí dědičnosti
Aby výše uvedené dobře fungovalo, musí být rozumně definováno pravidlo pro
pořadí volání metod předků v případě vícenásobné dědičnosti. Některé
objektově orientované jazyky používají prohledávání seznamu předků do hloubky.
Common Lisp místo toho používá poněkud komplikovanější třídění seznamu předků,
které je možno neformálními pravidly vyjádřit takto:
-
V seznamu dědičnosti od nejspecializovanější třídy po nejobecnější mají
vždy přednost potomci před svými předky (to je nejvýznamnější rozdíl oproti
prohledávání do hloubky).
-
Není-li mezi třídami vztah potomek-předek, mají přednost třídy umístěné
v hierarchii dědičnosti více "vlevo".
-
Není-li mezi třídami ani "vertikální" ani "horizontální" vztah, mají přednost
potomci níže postavených předků.
Příklad: Je-li mezi třídami A až F vztah daný následujícím diagramem, přičemž
předci jsou v diagramu níže pod svými potomky,
F
/ / \ \
E D C A
\ /
B
je pořadí dědičnosti třídy následující: F, E, D, C, B,
A. Požádáme-li v metodě jednoho argumentu, kterým je instance třídy F,
aplikované na třídu E o zavolání metody předka, zavolá se metoda
aplikovaná na třídu D. Pro jakékoliv volání metody bezprostředně následující
v hierarchii dědičnosti přitom stačí zapsat prosté
(call-next-method)
bez argumentů a CLOS už se postará o vše potřebné včetně předání
argumentů. Kdo někdy udržoval hierarchii tříd a měnil v ní během vývoje
jména tříd, hierarchii dědičnosti a argumenty metod, tak toto zautomatizované
volání velmi ocení.
Uvedená pravidla pořadí dědění tříd je mnohem praktičtější, přestože na
pochopení mírně složitější, než dědění do hloubky. O tom svědčí
i skutečnost, že programovací jazyk Python přešel od své verze 2.2
z pořadí dědičnosti daného prohledáváním do hloubky na způsob dědičnosti
bližší CLOSu, používaný v jednom ze starších systémů objektově
orientovaného programování v Lispu.
Přizpůsobení metod
Jedním z poměrně unikátních a přitom velmi užitečných rysů CLOSu jsou
pomocné metody, nazývané before, after a around methods,
umožňující definovat kód provedený před zavoláním, po zavolání nebo místo
zavolání určité metody pro konkrétní seznam tříd. To umožňuje provádět
modifikaci definovaných metod bez nutnosti asistence a dostatečné předvídavosti
jejich původního autora.
Představme si, že bychom chtěli počítat počet vykreslených primitivních
trojúhelníků v metodách uvedených výše. Je to velmi jednoduché:
(defmethod render-element :after ((element triangle) device)
(incf number-of-triangles))
(incf je makro inkrementující hodnotu proměnné.) Chceme-li mít
ještě k tomu statistiku o počtu vykreslených černobílých
trojúhelníků, přidáme navíc:
(defmethod render-element :after ((element triangle)
(device black-and-white-device)) (incf number-of-black-and-white-triangles))
Metoda počítání všech trojúhelníků zůstává v platnosti, definice jsou
vzhledem k odlišnému seznamu tříd, na který jsou aplikovány, nezávislé.
V hierarchii dědičnosti si lze pomocné metody představit přibližně jako
slupky cibule.
Obalovací, around, metody můžeme využít třeba následujícím způsobem pro hlídání
přílišného využívání výstupního zařízení:
(defmethod render-element :around ((element triangle) device)
(if (< number-of-triangles triangle-limit)
(call-next-method)
(error "Sorry, too many triangles!")))
V případě dosažení limitu na počet vykreslených trojúhelníků metoda ohlásí
chybu, jinak zavolá metodu obalenou.
Všechny výše uvedené příklady ilustrují jednu důležitou vlastnost: Veškeré
změny jsou i přes svou jednoduchost a přirozenost prováděny bez
jakéhokoliv zásahu do stávajícího kódu.
Kombinace metod
Volá-li se metoda předka pro instance tříd, obvyklým postupem je vyvolání první
metody ze seznamu všech stejnojmenných metod předků uspořádaného dle pravidel
dědičnosti. To je také standardní chování CLOSu. Kromě tohoto implicitního
mechanismu však CLOS nabízí i možnost aplikovat jakoukoliv operaci nad
seznamem všech stejnojmenných metod předků, pomocí mechanismu zvaného
kombinace metod (method combination). Předdefinovanými
operacemi jsou například součet hodnot vrácených metodami předků, maximum
z vrácených hodnot či spojení vrácených seznamů. Pokud to programátorovi
nestačí, může si nadefinovat libovolnou vlastní metodu.
K čemu je to dobré? Pokud například hierarchie tříd reprezentuje
hierarchii různorodých objektů, umožňuje kombinace metod snadno kombinovat
jejich vlastnosti. Pokud bychom například reprezentovali státy světa, jejich
administrativní jednotky, světadíly, atd. odpovídající hierarchií tříd,
umožňuje nám kombinace metod snadno definovat metody počítající například
celkové počty úředníků v kterékoliv jednotce uvedené hierarchie.
Metaobject Protocol (MOP)
CLOS klade na každou standardu odpovídající implementaci Common Lispu určitou
množinu požadavků, které umožňují používat programátorské konstrukce,
o kterých jsme se zmiňovali. Přesto je občas užitečné mít
možnost zasahovat do systému na ještě nižší úrovni, například chceme-li si
definovat speciální režim přístupu ke slotům instancí nebo zavést transparentní
perzistenci objektů.
CLOS záměrně nejde až na nejnižší úroveň, aby implementátoři jazyka nebyli
nutně stavěni před příliš náročné implementační úkoly, ať už po stránce
množství požadovaných rysů nebo kvůli svazování rukou při hledání
efektivní implementace. I velmi náročné programátorské požadavky však
uspokojí Metaobject Protocol (MOP). MOP definuje přístupový
protokol k třídám, instancím, generickým funkcím i metodám do velmi
podrobných detailů a dává tak programátorům možnost zasahovat prakticky do
čehokoliv.
Nevyhovují vám pravidla pro dědičnost v CLOSu? MOP je umožňuje změnit.
Chcete definovat přístupová práva ke slotům instancí tříd analogickým způsobem,
jako je používá Java? MOP to opět umožní. Změny v CLOSu můžete dělat
dvojím způsobem: využitím zmíněných pomocných metod (before/after/around) nebo
specializací metod CLOSu a MOPu pro různé metatřídy. (V pokročilých
systémech objektově orientovaného programování je každá třída instancí nějaké
jiné třídy, sloužící právě k definici tříd, tyto definiční třídy se
nazývají metatřídy. Lze-li zařídit, aby nějaká třída byla
instancí jiné metatřídy než implicitní, je prostřednictvím takového mechanismu
možné měnit obecné vlastnosti tříd, jako jsou například pravidla pro tvorbu
seznamu předků.)
MOP není součástí ANSI standardu Common Lispu. Jeho de facto
definicí je vynikající kniha The Art of Metaobject Programming, kterou lze
doporučit všem, bez ohledu na jejich vztah k Lispu, kteří se zajímají
o vrstvené programové systémy a přístupové protokoly k objektům.
Kniha obsahuje výkladovou a referenční část a její součástí je také ukázka
jednoduché implementace CLOSu včetně MOP.
Ze svobodných common lispových systémů podporují MOP CMU Common Lisp a SBCL a
podporují jej i mnohé proprietární systémy.
Objektové systémy příbuzných jazyků
Myšlenky obsažené v CLOSu se promítly i do Lispu příbuzných
programovacích jazyků. Pro Guile (GNU implementace jazyka Scheme míněná jako
rozšiřující jazyk GNU aplikací) existuje systém GOOPS velmi podobný CLOSu a
implementující i některé funkce metaobjektového protokolu. Implementace
jednoduchého systému podobného CLOSu existuje též pro Emacs Lisp, navíc
standardní součástí Emacs Lispu jsou tzv. advices, nabízející možnost definovat
before/after/around kód pro kterékoliv funkce.
Závěr
Objektově orientované programování v Common Lispu je nejen významným a
mocným programovacím nástrojem pro lispové programátory, nýbrž i zdrojem
nápadů a poučení pro zájemce o programování obecně. Podobně jako
v případě funkcionálního nebo logického programování lze bližší seznámení
se s ním doporučit všem zvídavým zájemcům o programovací techniky,
bez ohledu na to jaké konkrétní prostředí používají.
CLOS má další rysy, o kterých jsme se zde nezmínili. Nezabývali jsme se
například vůbec možnostmi dynamického přidávání a modifikace tříd a metod nebo
prostředky usnadňujícími rychlé prototypování. Chcete-li se s CLOSem
seznámit podrobněji, můžete využít následující odkazy:
|