Pôvodne som plánoval v tomto seriáli detailnejšie popísať aj použitie
OOP v Common Lispe, ale z dôvodu veľkého množštva podstatných detailov
si to tu nemôžem dovoliť. Peter Seibel venuje tejto téme tri kapitoly
(viac než 40 strán) svojej knihy (viď odkaz na túto a ďalšie
publikácie o Lispe v závere), kde detailne popisuje všetky dôležité
detaily a vymoženosti týkajúce sa tried, dedenia, generických funkcií
a conditions
. Ja sa v tejto časti zameriam najmä na stručný opis
podstaty OOP v Common Lispe a "výnimkám" s krátkymi ukážkami.
OOP à la Common Lisp
Aj keď sa to nezdá, Lisp je pod kapotou poriadne objektový, používané dátové typy ako čísla, reťazce a zoznamy majú tiež svoje triedy.
Triedy sú podobné triedam v iných jazykoch, no neobsahujú metódy. Ako
je to možné? Common Lisp má generic functions. Generická funkcia
definovaná makrom defgeneric
sa použije (vyberie) podľa typov
argumentov, kde sa vyberá vždy čo najšpecifickejšia funkcia vyhovujúca
všetkým argumentom funkcie. Pripomína vám to niečo? Multiple
dispatch je
samozrejmosťou.
Na ukážke je typický príklad multiple dispatch:
(defgeneric kolizia (x y) (:documentation "Kolízia objektu X s objektom Y."))
defgeneric
neprijíma žiadnu implementáciu, tú definujeme makrom
defmethod
:
(defmethod kolizia ((x asteroid) (y vesmirna-lod)) (format t "Asteroid narazil do vesmirnej lode.~%")) (defmethod kolizia ((x asteroid) (y velka-vesmirna-lod)) (format t "Asteroid narazil do velkej vesmirnej lode.~%"))
Tieto dve metódy sú jednoznačne rozlíšené podľa argumentov. Typ nie
je nutné definovať, keď na ňom nezáleží, napríklad miesto (y
vesmirna-lod)
by sme mohli dať len y
. Ďalšia vychytávka je, že je
možné definovať napr. metódu, ktorá sa bude spúšťať vždy po nej:
(defmethod kolizia :after (x y) (format t "Kolizia bola ukoncena.~%"))
Iné bežné kombinácie metód sú :before
a :around
. Vysvetlenie
výberu, poradia a iných možností tohoto systému je nad rámec nášho
článku. Teraz si skúsime vytvoriť použité triedy:
(defclass asteroid () nil) (defclass vesmirna-lod () (meno)) (defclass velka-vesmirna-lod (vesmirna-lod) nil)
Common Lisp umožňuje viacnásobnú dedičnosť, pričom poradie zadaných nadtried sa používa v prípade kolízíznych situácií (mená slotov).
Teraz otestujeme naše metódy s týmito objektami:
(defparameter *suter* (make-instance 'asteroid)) (defparameter *mala* (make-instance 'vesmirna-lod)) (defparameter *velka* (make-instance 'velka-vesmirna-lod))
Keď zavoláme (kolizia x y)
, kde za x
dosadíme *suter*
a za y
*mala*
alebo *velka*
, dostaneme správny výpis z metódy podľa typov
a navyše výpis z :after
metódy, pretože tá sedí na akýkoľvek typ
argumentov. Naopak sa dostaneme do debuggeru, ak dosadíme za x
argument iného typu než asteroid
.
V triede vesmirna-lod
sme si definovali jeden slot meno
. Ten je
teraz úplne prázdny a volanie (slot-value *mala* 'meno)
nás znovu
dostane do debuggeru. slot-value
je možné použiť ako argument
setf
(rovnako ako napríklad funkciu nth
pre prvok zoznamu, getf
pre property list, správanie setf
si dokonca sami môžeme rozšíriť
podobnými funkciami pri vlastných typoch). Nastavíme do slotu reťazec
volaním (setf (slot-value *mala* 'meno) "meno")
.
Netreba sa ale ničoho báť, triedy nie sú také primitívne, že by sme hodnoty museli nastavovať takto – naopak, umožňujú toho viac než v iných jazykoch. Veľmi pohodlné je použitie implicitných getterov a setterov (tieto názvy sa používajú v iných jazykoch a sú odvodené od mien metód začínajúcich slovom get/set):
(defclass vesmirna-lod () ((meno :reader meno :writer (setf meno))))
Toto sa dá ešte zjednodušiť použitím voľby :accessor meno
.
Ekvivalentný kód, ktorý nemusíme písať, by vyzeral asi takto:
;; reader (defgeneric meno (vesmirna-lod)) (defmethod meno ((lod vesmirna-lod)) (slot-value lod 'meno)) ;; writer (definicia pre setf funkciu) (defun (setf meno) (meno-lode lod) (setf (slot-value lod 'meno) meno-lode))
Teraz môžeme volať writer (meno instancia-lode)
a tento argument
používať vo funkcii setf
.
Ďalšie dôležité možnosti definície:
:initarg
pre definíciu keyword pri vytváraní inštancie:initform
definuje výraz, ktorý sa vykoná, ak nebola zadaná hodnota (napríkladerror
, viď ďalej)- všadeprítomný dokumentačný reťazec –
:documentation
(defclass vesmirna-lod () ((meno :initarg :meno :initform (error "Lod musi mat meno!") :accessor meno :documentation "Meno lode."))) ;; korektne vytvorenie instancie (make-instance 'vesmirna-lod :meno "meno")
Na záver treba spomenúť, že Common Lisp je tzv. "Lisp 2" a pre triedy, funkcie a tiež premenné má oddelené namespace (priestor mien). Okrem toho je tiež možné definovať balíky, ako sme si už ukázali.
conditions
V objektovom systéme by nemali chýbať výnimky, conditions
sú obdobou
výnimiek, presnejšie – sú to výnimky na steroidoch. Používajú
vlastný prototyp triedy odlišný od standard-class
, definujú sa
makrom define-condition
(inštanciu vytvára makro make-condition
).
error
je bežne používaná podtrieda condition
.
(define-condition my-error (error) nil) ;; signalizujeme condition: (error 'my-error)
Základné použitie, aké je dobre známe z iných jazykov (try
-catch
)
vyzerá nasledovne:
(handler-case (progn (func1) (func2)) (exception1 (e) (func3 e)))
conditions
v Lispe umožňuje rozhodovať o správaní aj z vyššej
úrovne, keď sa použije restart-case
miesto handler-case
a miesto
mien tried výnimiek (condition
) sa používajú mená, ktoré vyjadrujú
akciu, ktorá sa vykoná (je to meno, ktoré sa bude používať v kóde,
ktorý bude rozhodovať ako zareagovať na výnimku).
(handler-bind (binding*) form*) (handler-bind ((exception1 #'(lambda (c) (invoke-restart 'meno-akcie)))) (func4) (func5))
Tento kód je možné umiestniť do vyšších úrovní (medzi miestom, kde
došlo k výnimke a miestom, kde sa rozhoduje o akcii nemusí byť vo
funkcii, ktorá ju volala) a volací zásobník zostane neporušený, akcia
sa vykoná akoby sa rozhodovalo priamo na mieste. Miesto lambdy je
možné použiť aj pomenovanú funkciu. Samozrejme je možné definovať
viacero reštartov a tiež je možné im predávať argumenty, systém
conditions
a ich signalizovanie a ošetrovanie nám dáva veľkú
voľnosť.
Okrem error
môžeme signalizovať aj menej striktný warning
:
(define-condition my-warning (warning) nil) ;; signalizujeme condition: (warn 'my-warning)
warn
v prípade neúspechu nespúšťa debugger, len vypíše condition
na chybový výstup.
Tipy a vychytávky
Kompilácia
SBCL kód kompiluje do natívneho kódu a tak je možné ľahko vytvoriť samostatný spustiteľný súbor:
;; načítame náš program (asdf:operate 'asdf:load-op :mandelbrot) ;; funkcia main, ktorá vráti nulu (nutné pre normálne ukončenie programu) (defun main () (mandelbrot:start) 0) ;; uloženie programu do samostatného spustiteľného súboru (sb-ext:save-lisp-and-die "mandelbrot" :executable t :toplevel 'main)
Výstupný program môže byť v prípade SBCL veľký, vývojári sa riešeniu tohoto problému venujú. Na druhej strane komerčné implementácie by na tom mali byť v tomto lepšie.
Jedna, dve, tri hviezdičky
*
, **
, ***
uchovávajú posledné 3 vrátené hodnoty, takže pri
interaktívnom písaní kódu sa k nim šikovne môžeme dostať ak sme ich
neukladali (najmä pokiaľ sa jedná o výsledok nejakej dlhšie
vykonávanej funkcie).
Profiler
Občas sa zíde otestovať výkonnosť, na to SBCL poskytuje pekný profiler:
;; zapneme profilovanie funkcie (sb-profile:profile mandelbrot:start) ;; kontrola (sb-profile:profile)
Môžeme zavolať kód, ktorý chceme otestovať (v tomto prípade bez
argumentov) a potom si nechať vypísať správu (sb-profile:report)
.
Výsledok (jedná sa o optimalizovanú verziu ukážkového kódu):
seconds | consed | calls | sec/call | name ---------------------------------------------------- 0.212 | 94,384 | 1 | 0.211999 | MANDELBROT:START ---------------------------------------------------- 0.212 | 94,384 | 1 | | Total estimated total profiling overhead: .000 seconds overhead estimation parameters: 2.4e-8s/call, 1.424e-6s total profiling, 5.6e-7s internal profiling
Záver
Na záver poslednej časti by som chcel odporučiť uznávanú literatúru o Lispe. Veľmi vhodná pre začiatočníkov aj pokročilých je kniha Practical Common Lisp od Petra Seibela, v ktorej opisuje Common Lisp do väčšej hĺbky aj šírky než tento seriál (zachádza do detailov štandardnej knižnice, dátových typov, makier a uvádza praktické príklady), a upozorní a vysvetlí niektoré nie celkom zrejmé detaily jazyka. Je to kniha odporúčaná mnohými skúsenými Lisp programátormi a každý koho Lisp zaujal by ju určite nemal prehliadnuť.
Dalšia mnohými odporúčaná kniha zaoberajúca sa praktickým využitím Lispu je On Lisp od Paula Grahama.
S Lispom súvisí aj publikácia Structure and Interpretation of Computer Programs (SICP), ktorá je uznávaným textom v odbore a na ukážky používa Scheme, derivát Lispu.
A posledná súvisiaca kniha je Paradigms of Artificial Intelligence Programming (PAIP) Petra Norviga, ktorá sa zaoberá hlavne umelou inteligenciou.
Ospravedlňujem sa, ak sa mi "podarilo" niečo podstatné vynechať, Common Lisp je jazyk, ktorý obsahuje množstvo možností a množstvo funkcií v štandardnej knižnici, ale našťastie je vo vývojovom prostredí dostupná celá dokumentácia štandardu.
Pre pridávanie komentárov sa musíte prihlásiť.