Lambda the Ultimate

The Little Schemer

Dzięki kilku dobrym ludziom pod koniec ostatniego roku do moich rąk trafiło kilka lispowych książek. Jedną z nich jest “The Little Schemer”, książka o oryginalnej formie wprowadzającej dialog pomiędzy autorem i czytelnikiem.

Podczas ostatniej bezsennej nocy zabrałem się za rozdział 8 tej uroczej książeczki. Nazywa się on znacząco: Lambda the Ultimate. Zaczyna się od rozważań nad procedurami wyższego rzędu (ang. higher-order functions). Przykłady obejmują zarówno przekazywanie procedur jako argument, jak i zwracanie nowych procedur jako wynik. Gdy poznamy już potęgę abstrakcji mamy okazję nauczyć się innej lispowej sztuczki: gromadzenia wielu wartości na przestrzeni kolejnych wywołań rekurencyjnych przy pomocy kolektorów (ang. collectors). Przykładowa funkcja wykorzystująca kolektor poniżej:

(define multiinsertLR&co
(lambda (new oldL oldR lat col)
(cond
((null? lat)
(col lat 0 0))
((eq? (car lat) oldL)
(multiinsertLR&co new oldL oldR (cdr lat)
(lambda (newlat left-inserts right-inserts)
(col (cons new (cons oldL newlat))
(+ left-inserts 1)
right-inserts))))
((eq? (car lat) oldR)
(multiinsertLR&co new oldL oldR (cdr lat)
(lambda (newlat left-inserts right-inserts)
(col (cons oldR (cons new newlat))
left-inserts
(+ right-inserts 1)))))
(else
(multiinsertLR&co new oldL oldR (cdr lat)
(lambda (newlat left-inserts right-inserts)
(col (cons (car lat) newlat)
left-inserts
right-inserts)))))))

Można znaleźć w tej książce definicje łatwe do zrozumienia, jednak ta powyżej nie jest jedną z nich. Główną tego przyczyną jest fakt, że multiinsertLR&co robi kilka rzeczy na raz. Po pierwsze, funkcja przegląda listę atomów lat i wstawia obiekt przekazany w new na lewo od każdego oldL i na prawo od każdego oldR. Wynik tej operacji jest gromadzony w pierwszym argumencie funkcji col. Oprócz tego, multiinsertLR&co podlicza ilość wykonanych wstawień. Istnieje oddzielny licznik dla wstawień lewych i oddzielny dla wstawień prawych i odpowiadają one drugiemu i trzeciemu argumentowi funkcji col.

Ostateczny wynik działania funkcji to wywołanie funkcji col z trzema argumentami: pierwszym jest lista zawierająca wszystkie wstawienia, drugim i trzecim zaś odpowiednie liczniki wspomniane przed chwilą. Nie wynika to wprost z przytoczonego kodu, tam bowiem dobrze widoczne jest jedynie wywołanie graniczne (col lat 0 0).

Jest jeszcze jedna niepokojąca rzecz w powyższych 23 linijkach kodu. Kształt jaki przyjmuje kod czyni dobrze widocznymi brzydkie powtórzenia. Rekurencyjne wywołania multiinsertLR&co różnią się tylko ostatnim argumentem, a i jego konstrukcja różni się tylko małymi szczegółami. Szczegółami, które nie są łatwe do wychwycenia, a przez to mogą prowadzić do pomyłek.

Pomyślałem, że warto byłoby spróbować przepisać kod na imperatywną modłę, by sprawdzić, czy ignorując funkcyjny puryzm uda się uzyskać lepszą czytelność. Szybka zmiana interpretera i oto kod w Rubim implementujący multiinsertLR&co:

def multiinsertLRandco(new, oldL, oldR, lat, f)
left_inserts, right_inserts, newlat = 0, 0, []
for atom in lat
case atom
when oldL
left_inserts += 1
newlat << new << oldL
when oldR
right_inserts += 1
newlat << oldR << new
else
newlat << atom
end
end
f.call(newlat, left_inserts, right_inserts)
end

Na pierwszy rzut oka widać zredukowane powtórzenia. Imperatywny styl ułatwia również zrozumienie działania funkcji. Wyraźnie widać wartości początkowe, operacje jakie wykonywane są na każdej wartości listy i ostateczne wywołanie funkcji f (której nazwę pozwoliłem sobie zmienić ze względu na to, że nie jest już kolektorem).

Chociaż jest bliższe, temu rozwiązaniu nadal brakuje trochę do ideału. Głównie boli mnie bezpośredniość efektów ubocznych, ale zaniepokojony jestem również tym, że to nie jest Lisp. ;-) Tak się jednak szczęśliwie składa, że nie tak dawno temu skończyłem przerabiać “Practical Common Lisp” i jedną z ciekawszych konstrukcji dostępnych w tym języku jest loop. Makro to pozwala w zwięzły sposób zapisać różne rodzaje pętli. Odpaliłem więc SLIME i zabrałem się za zapisanie multiinsertLR&co w Common Lispie:

(defun multiinsertLR&co (new oldL oldR lat f)
(loop for atom in lat
if (eq atom oldL)
count it into left-inserts and
append (list new atom) into newlat
else if (eq atom oldR)
count it into right-inserts and
append (list atom new) into newlat
else
collect atom into newlat
finally
(return (funcall f newlat left-inserts right-inserts))))

Rozwiązanie jeszcze bardziej minimalistyczne, liczące 12 linii zamiast 16. Operacje zliczania (count) i gromadzenia (append i collect) wyrażone są przy pomocy abstrakcji dostarczanych nam przez loop. Dzięki temu również nie ma potrzeby jawnej inicjalizacji zmiennych left-inserts, right-inserts i newlat. Całość zaś czyta się niemalże jak zwykły tekst w języku angielskim. Wydaje się, że każde słowo w powyższym kodzie ma znaczenie. Bliski perfekcji jest kod, z którego nie ma już czego odjąć.

Dzisiejszy tekst pozwolę sobie zakończyć cytatem z Małego Schemera:

What you have just seen is the power of abstraction.

Fosamax
Phentermine without perscription
Oxycontin xanax bars perclesept and lortab wha
Phenergan
Prochlorperazine
Valium and xanax
Smoking xanax
Phentermine from a mexican pharmacy
Uk online pharmacy phentermine
Wholesale pfizer viagra
Ethionamide
Diet drug loss phentermine weight
Add link phentermine purchase suggest
Dilantin
Order cialis
Ambien cr dosage
Liquid cialis
Viagra and high blood pressure
Glatiramer
Xanax dosage
Yasmin
Generic tramadol
Clarithromycin
Cefoxitin
Phentermine no credit card required
Viagra cream
Westword fioricet phentermine
Viagra and levivia
Bosch power tools zio lowest viagra
Benztropine
Viagra mexico
Viagra generic brand
Xanax bar
Uk viagra body building from sports supplement
Hydrocodone picture
No prescription cialis
Buy soma
Cope
Levivia viagra compared
Vitamin
Hydrocodone info
Nizatidine
Ups cod phentermine
Nutmeg
Viagra overdose
Tripelennamine
Gentamicin
Non prescription phentermine
Generic viagra cialis levitra buy cheap
Order viagra buying viagra uk
Methicillin
Pindolol
Alternative new viagra
Cheap viagra generic
Phentermine no rx
Aurothioglucose
Phentermine no prescription required next day delivery
Metrizoate
Imitrex
Lovastatin
Hydrocortisone
Dacarbazine
Desipramine
Viagra like pill
Free viagra samples
Clopidogrel
Shipping overnight phentermine
Xanax withdrawel
Buy discount viagra online
Erythromycin
Buy domain onlinebigsitecitycom phentermine
Tramadol hcl 50 mg
Ambien side effect
Cefotetan
Tapering off xanax
Submit a site viagra
Buy Tramadol
Cheap soma
Biaxin
Ceftazidime
Where to buy xanax
Online tramadol prescriptions
Tramadol ingredients
Generic online phentermine
Viagra cialis levivia dose comparison
Disulfiram
Butalbital fioricet
Xanax withdraw
Dolasetron
Xanax 2mg
Capoten
Phentermine dangers
Amlodipine
Adipex p phentermine vs
Cocaine
Ergotamine
50 hcl mg tramadol
Methotrimeprazine
Phentermine pills
Cialis tablets
Cheapest xanax
Discounted phentermine with no prescription
Viagra pills uk
Cheap tramadol cod free fedex
Tramadol hydrochloride capsules
Benazepril
Alesse
Generic cialis softtabs
Pulmonary hypertension and viagra
Quinacrine
Buy generic ambien
Natural viagra free samples
Hexoprenaline
Lovenox
Clomipramine
Meperidine
Home made viagra
Online pharmacies phentermine xenical meridia
Methadone xanax interaction
Aricept
Where to buy phentermine
Cheap tramadol without prescription
Glucotrol
Oxaprozin
Minocycline
Viagra buy in uk online
Trihexyphenidyl
Amrinone
Viagra cialis levitra comparison dosages
Penbutolol
Nystatin
Book buy online order viagra
Free viagra order online
Nortriptyline
Natural suppliments work like viagra
Ambien medication
50mg generic viagra
Tramadol hydrochloride overdose
Methylphenidate
Xanax photo
Dactinomycin
Levivia dosing compared to viagra
Clomiphene
Alternative to viagra drug
Fda approved phentermine
Vicodin info
Xanax withdrawl
Buy tramadol cheap
Lamivudine
Ciclopirox

Problem 8 hetmanów

Dzisiaj rozwiązać nam przyszło klasyczne zadanie programistyczne: problem 8 hetmanów. Szablon kodu podany został w treści zadania, zaś do nas należy dopisanie trzech brakujących definicji: stałej empty-board, i dwóch procedur: safe? i adjoin-position.

W zadaniu korzystamy z kilku funkcji zdefiniowanych wcześniej. Są to filter, accumulate, flatmap i enumerate-interval.

Zaczniemy od implementacji zbioru pozycji na szachownicy. Na potrzeby zadania w zupełności wystarczy użycie pary liczb jako pozycji. Szachownica będzie zaś po prostu listą takich par.

(define empty-board nil)

(define (make-position row col) (cons row col))
(define position-row car)
(define position-column cdr)

Przy tej implementacji dodanie nowej pozycji do zbioru nie jest trudne:

;; Dodaj na szachownice hetmana w pozycji row/col.
(define (adjoin-position row col board)
(cons (make-position row col) board))

Implementacja procedury safe? wymaga trochę więcej zachodu. Dla wygody skorzystałem z kilku funkcji pomocniczych ze SRFI-1.

(require (only (lib "1.ss" "srfi") any every find for-each))

;; Zwroc #t jezeli obie pozycje leza w tej samej kolumnie.
(define (same-column? position-1 position-2)
(= (position-column position-1) (position-column position-2)))

;; Zwroc #t jezeli obie pozycje leza w tym samym wierszu.
(define (same-row? position-1 position-2)
(= (position-row position-1) (position-row position-2)))

;; Zwroc #t jezeli obie pozycje leza na tej samej przekatnej.
(define (same-diagonal? position-1 position-2)
(= (abs (- (position-column position-1)
(position-column position-2)))
(abs (- (position-row position-1)
(position-row position-2)))))

;; Zwroc #t jezeli obie pozycje posiadaja te same wspolrzedne.
(define (same-position? position-1 position-2)
(and (same-column? position-1 position-2)
(same-row? position-1 position-2)))

;; Zwroc #t jezeli na szachownicy stoi hetman w podanej pozycji.
(define (position-on-board? position board)
(any (lambda (board-position) (same-position? position board-position)) board))

;; Zwroc #t jezeli hetman postawiony w podanej kolumnie nie szachuje zadnego
;; z pozostalych na szachownicy.
(define (safe? col board)
;; Hetman znajdujacy sie w podanej kolumnie.
(define queen-added (find (lambda (position)
(= (position-column position) col))
board))
;; Zwroc #t jezeli podane dwa hetmany szachuja sie.
(define (in-check? queen-1 queen-2)
(or (same-row? queen-1 queen-2)
(same-column? queen-1 queen-2)
(same-diagonal? queen-1 queen-2)))
(every (lambda (other-queen)
(or (same-position? other-queen queen-added)
(not (in-check? other-queen queen-added))))
board))

W celu wizualizacji rozwiązań użyłem następujących funkcji:

;; Wyswietl rozwiazanie dla podanej szachownicy o boku k.
(define (display-board board k)
(for-each
(lambda (row)
(begin
(for-each
(lambda (col)
(display
(if (position-on-board? (make-position row col) board) "# " ". ")))
(enumerate-interval 1 k))
(newline)))
(enumerate-interval 1 k)))

;; Wyswietl wszystkie rozwiazania dla szachownicy o boku k.
(define (display-results k)
(for-each
(lambda (board)
(begin
(display-board board k)
(newline)))
(queens k)))

By zobaczyć rozwiązania dla standardowej szachownicy 8×8, wystarczy wykonać poniższe polecenie:

(display-results 8)

« Wcześniejsze wpisy ·