Common Lisp (5) - closures, ASDF, kreslenie

04.05.2008 22:00 | Články | Adam Sloboda
V tejto časti si ukážeme ďalšie vlastnosti jazyka, niektoré funkcionálne aspekty a nakoniec aj malú praktickú ukážku projektu.

destructuring bind

destruturing-bind je blok, v ktorom sa priradí premenným v prvom argumente hodnota z druhého argumentu (presne ako názov naznačuje, rozloží zoznam na samostatné prvky). Tretím argumentom je telo, v ktorom môžeme narábať s týmito premennými.

CL-USER> (destructuring-bind (a b c s) `(1 2 3 ,(+ 1 2 3))
       `(,a + ,b + ,c = ,s))
(1 + 2 + 3 = 6)

values a multiple value bind

Ďalšou špecialitou je možnosť viacerých návratových hodnôt (pokiaľ túto možnosť chceme ignorovať, pracuje sa len s prvou vrátenou hodnotou):

CL-USER> (floor pi)
3
0.14159265358979312d0
CL-USER> (- 3 (floor pi))
0
CL-USER> (multiple-value-bind (a b) (floor (sqrt 2))
       (format nil "~A ~A" a b))
"1 0.41421354"
CL-USER> (multiple-value-bind (x y z) (values 1 2 3)
       (format nil "~A ~A ~A" x y z))
"1 2 3"

Closure

CL-USER> (defun adder (n) #'(lambda (x) (+ x n)))
ADDER
CL-USER> (adder 5)
#<CLOSURE (LAMBDA (X)) {10032C1049}>
CL-USER> (funcall (adder 5) 1)
6
CL-USER> (setf (symbol-function 'add5) (adder 5))
#<CLOSURE (LAMBDA (X)) {10035CBEC9}>
CL-USER> (add5 1)
6

V ukážke funkcia vytvorí lexikálny uzáver (closure), ktoré aj mimo svojho kontextu môže pristupovať k hodnote n.

Techniku currying je možné pomocou closure implementovať.

CL-USER> (defun curry (function &rest args)
               (lambda (&rest more-args)
                (apply function (append args more-args))))
CURRY
CL-USER> (curry #'+ 5)
#<CLOSURE (LAMBDA (&REST MORE-ARGS)) {1002853ED9}>
CL-USER> (funcall (curry #'+ 5) 1)
6
CL-USER> (setf (symbol-function 'expt10) (curry #'expt 10))
#<CLOSURE (LAMBDA (&REST MORE-ARGS)) {1002994E79}>
CL-USER> (expt10 2)
100
CL-USER> (setf (symbol-function 'list-1-2) (curry #'list 1 2))
#<CLOSURE (LAMBDA (&REST MORE-ARGS)) {10034484E9}>
CL-USER> (list-1-2)
(1 2)
CL-USER> (list-1-2 3 4)
(1 2 3 4)

V ukážkach sme vytvorili volanie roznych funkcií s neúplným zoznamom argumentov (v prípade vytvorenia zoznamu je úplný, no môžeme ho rozšíriť aj o ďalšie hodnoty).

Optimalizovať vykonávanie funkcie curry je možné týmto nastavením:

(declaim (ftype (function (function &rest t) function) curry) (inline curry))

ASDF

asdf (Another System Definition Facility) je knižnica, ktorá sa používa na automatizáciu kompilácie a načítania tzv. systémov (programy alebo knižnice pre Common Lisp, majú vlastný namespace) podľa definovaných závislostí.

Jeho použitie si ukážeme na jednoduchom príklade – zobrazíme body pravidelného n-uholníka (budeme potrebovať balíček cl-sdl). Vytvoríme si zdrojový a popisný súbor:

polygon.lisp (okrem štandardnej Common Lisp knižnice používame knižnicu cl-sdl, exportujeme funkciu start)

(defpackage #:polygon
  (:use #:cl #:cl-sdl)
  (:export #:start))
 
(in-package #:polygon)
 
;; tu začína kód

polygon.asd:

(asdf:defsystem #:polygon
  :depends-on (#:sdl)
  :components ((:file "polygon")))

Načítame náš projekt cez SLIME REPL, príkaz začneme zadávať čiarkou, zadáme load-system a následne názov nášho projektu (možno bude nutné predtým zmeniť aktuálny adresár príkazom cd). Príkazy a ďalšie informácie je aj tu možné nechať automaticky dopĺňať tabulátorom. V tomto bode môžeme začať písať kód, globálne premenné (zvyknú sa pomenovávať s hviezdičkami):

;; globálne premenné ovplyvňujúce inicializáciu SDL a obraz
(defvar *zoom* 20)
(defvar *width* 200)
(defvar *height* 200)
(defvar *bpp* 24)
(defvar *flags* (logior sdl:+swsurface+))
 
(defun init-sdl ()
  (sdl:init (logior sdl:+init-video+))
  (let ((surface (sdl:set-video-mode *width* *height* *bpp* *flags*)))
    (when (sgum:null-pointer-p surface)
      (error "Unable to set video mode"))
    (sdl:wm-set-caption "CL-SDL" nil)
    surface))

Hlavná programová slučka, kde prijímame udalosti z knižnice SDL (pri stlačení krížiku na okne alebo pri aktivite klávesy 'q' slučku ukončíme):

(defun run-sdl-event-loop (surface points)
  (plot surface points)
  (sdl:event-loop
   (:key-down (scan-code key mod unicode)
              (cond ((= key (char-code #\q))
                     (return))))
   (:key-up (scan-code key mod unicode)
            (cond ((= key (char-code #\q))
                   (return))))
   (:quit ()
          (return))
   (:idle ()
          (sleep 0.05))))

Ďalšia je funkcia na výpočet bodov na jednotkovej kružnici a funkcia na ich vykreslenie:

(defun points (n)
  (mapcar #'(lambda (x) (list (* 2 (cos x))
                              (* 2 (sin x))))
          ;; uhly na ktorych lezia body
          (loop :for i :from 0 :below n :collect (/ (* 2 pi i) n))))
 
(defun plot (surface points)
  (dolist (p points)
    (let ((x (floor (+ (/ *width* 2) (* *zoom* (+ (first p) 1/2)))))
          (y (floor (+ (/ *height* 2) (* *zoom* (+ (second p) 1/2))))))
      (cl-sdl:draw-pixel surface x y 255 0 0 :update-p nil :clipping-p nil)))
  (cl-sdl:update-screen surface))

Obe obsahujú len veľmi jednoduché výpočty bodov a pozíciu pixelov. Teraz si ich všetky pospájame dokopy:

(defun start (n)
  (unwind-protect
       (progn
         (let ((surface (init-sdl)))
           (run-sdl-event-loop surface (points n))))
    (sdl:quit)))

unwind-protect zabezpečí aby sa vždy zavolala funkcia quit v knižnici SDL. Teraz stačí skompilovať pomocou C-c C-k a môžeme to vyskúšať. Pokiaľ spúšťame program z REPL, používame celý názov polygon:start.

3: 4: 5: 6: 100: 1000:

Na precvičenie môže byť vhodné doplniť interaktívny zoom alebo pospájanie bodov úsečkami pomocou funkcie cl-sdl:draw-line:

(defun draw-line (surface x1 y1 x2 y2 r g b
                  &key (check-lock-p t) (update-p t) (clipping-p t)))

Kompletný a zvýraznený kód

Toto bolo len také zahrievacie kolo na oboznámenie so SDL, nabudúce si skúsime niečo trošku zložitejšie – nakreslíme si Mandelbrotovu množinu.