equal?

W ćwiczeniu 2.54 autorzy proszą nas o napisanie funkcji equal? zdolnej do porównywania list. Jej definicja wynika bezpośrednio z opisu.

(define (equal? a b)
(if (and (pair? a) (pair? b))
(and (equal? (car a) (car b))
(equal? (cdr a) (cdr b)))
(eq? a b)))

Implementacja ta radzi sobie doskonale z symbolami i (dowolnie zagnieżdżonymi) listami symboli, nie bierze jednak pod uwagę liczb. Jeżeli przyjmiemy, tak jak radzą nam autorzy w przypisie, że dwie liczby są równe, jeżeli ich wartości numeryczne są równe, to otrzymamy następującą implementację:

(define (equal? a b)
(cond
[(and (pair? a) (pair? b))
(and (equal? (car a) (car b))
(equal? (cdr a) (cdr b)))]
[(and (number? a) (number? b))
(= a b)]
[else
(eq? a b)]))

Scheme dla każdego

W tym tygodniu skończyłem czytać wprowadzenie do języka Haskell o zachęcającym tytule “Write Yourself a Scheme in 48 Hours”. W języku statycznie-typowanym, leniwym i bez śladu efektów ubocznych implementujemy język dynamiczny, ze ścisłą ewaluacją i mutacją. Ciekawe doświadczenie samo w sobie, chociaż możliwe, że zbyt wymagające dla monolingwistów. Autor nie bez przyczyny podaje Wizard Booka i Małego Schemera jako lektury uzupełniające. Bez tego rzeczywiście może być trudno.

Poradnik czyta się dość lekko, można wręcz popaść w rutynę kopiuj-wklej i przerobić całość w krótkim czasie. Warto jednak pomedytować nad samym tekstem tutoriala, a obowiązkowo już nad pojawiającymi się co jakiś czas ćwiczeniami. Dzięki nim można oswoić się z dokumentacją i poczuć czym pisanie programu w Haskellu naprawdę się je. Niestety pod koniec tekstu, właśnie wtedy, gdy pojawiają się trudniejsze tematy, jak łączenie monadów i zarządzanie stanem, ćwiczeń zaczyna brakować. Nie ma innej rady, jak zagłębić się w tekst i linijka po linijce prześledzić kod.

Ja tutaj ograniczę się do przedstawienia kilku subiektywnych spostrzerzeń na temat samego Haskella.

Rzucanie i łapanie wyjątków

Moją uwagę szczególnie zwróciły funkcje obsługi wyjątków przedstawione
w części 5.
Po raz pierwszy widząc definicję extractValue uznałem ją za błędną:

extractValue :: ThrowsError String -> String
extractValue (Right val) = val

extractValue ma typ ThrowsError String -> String, zdaje się więc uwalniać wartość z paszczy monadu. Co więcej, extractValue zdefiniowany jest tylko dla konstruktora Right, tutaj obudowującego prawidłową wartość obliczeń, podczas gdy Left obudowuje wartość typu LispError. Okazuje się, że definicja taka nie sprawia problemów, gdyż autor używa extractValue wyłącznie po wywołaniu trapError.

evalString :: String -> IO String
evalString expr = return $ extractValue $
    trapError (liftM show $ readExpr expr >>= eval)

trapError korzysta z catchError, by wszystkie ewentualne błędy w action przepuścić przez funkcję (return . show):

trapError :: ThrowsError String -> ThrowsError String
trapError action = catchError action (return . show)

to zaś jedynie zamienia błąd na ciąg znaków (show) i obudowuje go jako wartość (return). trapError nigdy więc nie zwróci wartości Left i extractValue jest bezpieczne w swojej obecnej definicji.

Przykład ten jest ciekawy, gdyż pokazuje, że można pisać funkcje, które korzystają z monadu tylko wewnętrzenie, nie “zarażając” pozostałych części systemu. Niestety nie zawsze możliwa jest bezpieczna ucieczka z monadu. catchError wyraźnie oddziela część programu, która może zgłaszać błędy od reszty i poprzez funkcję obsługi błędu (drugi argument do catchError) pozwala obliczenia błędne zamienić na prawidłowe, tak jak w przykładzie powyżej robi to złożenie return i show.

Gdy jednak spojrzymy na monad IO, sytuacja nie jest już taka różowa. Co prawda istnieje funkcja unsafePerformIO, ale nie jest ona, jak z resztą jej nazwa wskazuje, bezpieczna w użyciu. O ile bowiem funkcję zgłaszającą błąd łatwo przekształcić na taką, która błędu nie zgłasza (kto nie słyszał o łapaniu wyjątków?), to by zamienić funkcję, która skorzystała z wejścia/wyjścia na taką, która z niego nie skorzystała, potrzebowalibyśmy wehikułu czasu.

Dla ciekawych tematu proponuję sprawdzić listę funkcji wypakowujących wartości z monadów: m a -> a.

Wyjątki mają odzwierciedlenie w typach

W stylu Javowych checked exceptions.

Mogą stanowić potencjalny PITA przy rozwijaniu programu. Gdy postanowimy wprowadzić obsługę błędów w pewnej części aplikacji, czeka nas uzupełnienie definicji o typ wyjątku, a samych funkcji o wywołania return. Nie jest to skomplikowana operacja, ale niestety żmudna i czasochłonna.

kapsW

Płynne posługiwanie się Haskellem wymaga opanowania czytania wyrażeń od końca. Składanie funkcji, styl pointfree łącznie z biblioteką standardową pełną procedur wyższego rzędu powoduje, że definicje takie jak isBound są typowe dla programów w Haskellu:

isBound envRef var = readIORef envRef >>= return .
    maybe False (const True) . lookup var

Słowami samego autora:

This style of programming - relying heavily on function composition, function application, and passing functions to functions - is very common in Haskell code. It often lets you express very complicated algorithms in a single line, breaking down intermediate steps into other functions that can be combined in various ways. Unfortunately, it means that you often have to read Haskell code from right-to-left and keep careful track of the types.

Podstawowy przykład użycia IORef

Jeżeli przedstawiony w części 8 opis IORef nie był zbyt jasny, znalazłem przejrzysty przykład użycia IORef.

Tworzenie biblioteki dla PLT

Niedawno napisaną
bibliotekę do generowania identikon udostępniłem na
planecie PLT - PLTowym odpowieniku
Perlowego CPANu, czy
Pythonowego indeksu pakietów. Zadanie
przygotowania biblioteki w taki sposób, by mogła być umieszczona na planecie
nie było trudne, ale niosło ze sobą kilka niespodzianek.

Efekt końcowy można
znaleźć na stronie pakietu,
razem z dokumentacją
i pełnym kodem źródłowym.
By skorzystać z biblioteki wystarczy teraz dodać do programu jedną linijkę:

(require (planet "identicons.ss" ("mk" "identicons.plt")))

Sam proces przygotowania biblioteki jest
dobrze opisany w dokumentacji.
Mój kod był już podzielony na moduły, pozostało więc napisanie dokumentacji w
formacie Scribble i wypełnienie
pliku info.ss. Możliwe do ustawienia atrybuty dla info.ss
są ładnie opisane we wspomnianym już dokumencie,
skupię się więc na samej tylko dokumentacji.

Upraszczając, dokument Scribble pisze się tak, jak zwykły dokument tekstowy.
Jest jednak dostępny cały zestaw specjalnych znaczników umożliwiających
linkowanie do innych stron dokumentacji, zagnieżdżanie kodu Scheme, wizualne
upiększanie tekstu itp. Przede wszystkim dokument musi zaczynać się od
#lang scribble/doc, po czy zazwyczaj następuje require.
Warta zapamiętania jest składnia z for-label.
Nagłówek pliku manual.scrbl z pakietu identikon wygląda następująco:

#lang scribble/doc
@(require scribble/manual
scribble/eval
(for-label scheme/gui)
(for-label "identicons.ss"))

@title{Identicons library}

@title ustawia tytuł strony z dokumentacją. Przy dokumentowaniu
modułów po tytule następuje zazwyczaj znacznik
@defmodule.
Efektem ubocznym zwykłego @defmodule jest zaimportowanie podanego modułu,
co w przypadku dokumentowania biblioteki, której jeszcze na planecie nie ma, jest dość
niefortunne. Problem udało mi się obejść w następujący sposób:

@defmodule*/no-declare[((planet "identicons.ss" ("mk" "identicons.plt")))]
@declare-exporting["identicons.ss"]

Poza tą małą niedogodnością inne znaczniki działają bez większych niespodzianek.
Dla wygody zebrałem poniżej wszystkie znaczniki, jakich użyłem w dokumentacji identikon:

Przy okazji budowania dokumentacji natrafiłem na błąd, który niezwłocznie
zgłosiłem.
Nie minęło 5 godzin jak w repozytorium PLT pojawiła się poprawka. To się
nazywa dobry support w projekcie open-source!
Lista dyskusyjna
również działa bardzo sprawnie i wcale nierzadko można otrzymać odpowiedź
od samego Matthiasa Felleisena,
współautora m.in. wspomnianego wcześniej
“Małego Schemera”.
Wokół PLT istnieje aktywna społeczność, z którą warto się zapoznać. Do usłyszenia więc
na plt-scheme!

Implementacja identikon w PLT Scheme

Gdy w sieci href="http://www.docuverse.com/blog/donpark/2007/01/18/visual-security-9-block-ip-identification">przeczytałem
o identikonach o wiele bardziej urzekła mnie estetyka ich wyglądu
niż ich techniczne zalety. Identikony, w formie zaimplementowanej
przez Dona Parka, służyć mają jako graficzna reprezentacja tożsamości
osoby w sieci. Założenie jest takie, że kolorowe wzory są łatwiejsze
do porównania i zapamiętania przez człowieka, niż numer IP. Dodatkowo,
przepuszczając IP użytkownika przez funkcję haszującą z sekretnym
ziarnem możemy zachować właściwości identyfikujące bez potrzeby
ujawniania samego numeru IP.


Będąc zainteresowany samymi wzorami, szybko odnalazłem href="http://www.docuverse.com/blog/donpark/2007/01/19/identicon-updated-and-source-released">opis
sposobu ich generowania i zacząłem zastanawiać się nad
implementacją. Pomyślałem bowiem, że to świetna okazja na wypróbowanie
możliwości Scheme i bliższe poznanie wybranego środowiska. Padło na href="http://www.plt-scheme.org/">PLT, głównie ze względu na moją
wcześniejszą z nim styczność i dobre doświadczenia wyniesione z eksperymentów w
DrScheme.

Jak się później dowiedziałem, bloczki, z których zbudowane są
identikony, zostały początkowo href="http://www.levitated.net/daily/lev9block.html">opisane przez
Jareda Tarbella
na podstawie mającej długą historię sztuki href="http://en.wikipedia.org/wiki/Quilting">quiltu. A to z kolei
pamiętam, że gdzieś
już widziałem
.

Przede wszystkim zachęcam do ściągnięcia
i wypróbowania kodu
. Do prawidłowego działania wymaga on
PLT z serii 3.99
(lub 4.0, który ma
wyjść całkiem niedługo).
Poniżej zaś kilka komentarzy i wniosków, które nasunęły mi się podczas
implementacji.

Bibliotekę wrzuciłem na Planetę PLT, by jej użyć wystarczy więc jedna linijka kodu:

(require (planet "identicons.ss" ("mk" "identicons.plt")))

Implementacja identikon zostanie automatycznie ściągnięta i zainstalowana.

System obiektowy

System obiektowy PLT Scheme funkcjonalnie nie różni się bardzo od tych
zawartych w językach takich jak Java, Python, czy Ruby. Filozofia jest
jednak trochę inna, głównie z tej przyczyny, że system obiektowy nie
jest podstawą języka, ale jednym z wielu sposobów organizacji programu.
Po załadowaniu modułu scheme/class nie doświadczymy więc
wielkich zmian. Podstawowe typy danych, takie jak liczby, symbole, czy
ciągi znaków nie zostają wciągnięte w hierarchię klas. Prawdę mówiąc,
poza klasą
object%
i interfejsem
externalizable<%>
hierarchia ta jest całkiem pusta i zadanie wypełnienia jej leży
w rękach programisty. Po bibliotekach standardowych pękających od
podstawowych klas i obiektów jest to całkiem odświeżające doświadczenie,
muszę przyznać. W założeniu twórców PLT obiekty mają służyć do sprawnego
zamodelowania dziedziny problemu, przed jakim staje programista. A do
tego nie potrzeba bagażu historycznego jaki często niosą ze sobą hierarchie
starszych języków obiektowych.

Podstawowe konstrukcje są dość dobrze opisane w
dokumentacji,
a pomiędzy liniami można też poznać kilka konwencji. Nazwy klas kończą się
%, a nazwy interfejsów <%>. W obrębie
wyrażenia definiującego klasę
dostępnych jest kilka form specjalnych służących do definiowania atrybutów
i metod. Do komfortowej pracy wystarczy znajomość czterech z nich:

  • init-field do definiowania atrybutów, które może ustawić użytkownik podczas tworzenia obiektu
  • field do definiowania pozostałych atrybutów
  • define/public do definiowania publicznych metod
  • define do definiowania metod prywatnych

Dla przykładu przyjrzyjmy się implementacji identikony:

(define identicon%
(class object%
(init-field seed)

(field (32-bits (make-bit-stream seed))
(center-patch-shape (list-ref center-patch-shapes (32-bits 2)))
(side-patch-shape (list-ref patch-shapes (32-bits 4)))
...)

(define (get-patch-size dc)
...)

(define/public (draw dc)
...)

(super-new)))

Kod zdaje się mówić sam za siebie. seed jest atrybutem
ustawianym podczas konstrukcji obiektu. 32-bits,
center-patch-shape i side-patch-shape
są ukrytymi atrybutami przyjmującymi zadane wartości. Warte zauważenia
jest to, że 32-bits zależy od atrybutu seed,
podczas gdy samo 32-bits jest używane w następujących
po nim definicjach. get-patch-size jest metodą dostępną
tylko dla pozostałych metod tej klasy (czyli jest metodę prywatną),
a draw jest metodą publiczną. Wywołanie (super-new)
kończące definicje klasy jest potrzebne do właściwej inicjalizacji
obiektu przez nadklasę object%.

Konstrukcja obiektów jest bezbolesna. Przykładowo, by utworzyć nową identikonę
wystarczy napisać:

(make-object identicon% 1234567890)

i dostaniemy z powrotem identikonę pokazaną poniżej. By zapisać identikonę
do pliku, wystarczy wywołać na niej metodę save-to-file z podaniem
nazwy pliku i rozmiarem identikony w pikselach:

(send identicon save-to-file "1234567890.png" 200)


O ile do tego momentu nie miałem większych zastrzeżeń do systemu obiektowego
PLT to do sposobu wywoływania metod muszę się już przyczepić. :-)
System obiektowy z przekazywaniem komunikatów (ang.
message passing)
moim zdaniem idzie trochę na przekór lispowej filozofii. Common Lispowe procedury
ogólne (ang. generic procedures)
nie dość, że potężniejsze, to moim zdaniem bardziej idą z duchem Lispa. Pisanie kodu
podobnego do:

(send identicon draw 30)

wydaje się nienaturalne, umieszczając nazwę wykonywanej operacji dopiero na
trzeciej pozycji wyrażenia. Naturalną składnią dla wołania metody draw
jest następująca postać powyższego wyrażenia:

(draw identicon 30)

Co prawda, można by zacząć dodawać dla wszystkich metod publicznych
odpowiadającą im procedurę, np.:

(define (draw object . args)
(send/apply object draw args))

i może nawet powiązać ten efekt z define/public, ale wtedy
mielibyśmy już nowy system obiektowy, łamiący świętą w świecie Scheme
makrową higienę.
Oczywiście świat nie jest czarno-biały i istnieją zalety podejścia
używanego przez PLT. Jednym z niewątpliwych plusów jest fakt, że jedynym
symbolem, jaki trzeba eksportować z modułu jest nazwa klasy - nazwy
metod bowiem rozwijane są jedynie w kontekście konkretnego obiektu.
Metody “podążają za” obiektami, co w systemie z przekazywaniem komunikatów
ma sens.

W PLTowym pakiecie Swindle
jest dostępny system obiektowy bliższy Common Lispowemu CLOSowi, będzie
więc jeszcze okazja przyjrzeć się innemu podejściu.

Inną ciekawostką związaną ze standardowym PLTowym systemem obiektowym jest
to, że klasy same nie są obiektami:

(object? identicon%) ;; => #f

Nie możemy więc na nich wywoływać metod i definiować atrybutów. Nie jest to
jednak potrzebne, w PLT istnieją bowiem moduły, w których możemy zawrzeć
zarówno lokalny stan, jak i grupę procedur. O zabawach z metaklasami możemy
jednak zapomnieć (co prawdopodobnie językowi wychodzi na zdrowie ;-).

Moduły i kontrakty

Wraz z nadejściem R6RS doczekaliśmy się
standardowego
wsparcia dla modułów (bibliotek) w Scheme
, zanim jednak ten czas nastąpił
każda z implementacji musiała rozwiązać ten problem po swojemu. Moduły w PLT,
w ograniczonym zakresie jakim miałem okazję ich używać, spełniają swoją funkcję
doskonale. By obudować całą zawartość pliku w moduł wystarczy dodać na jego
początku linijkę z określeniem używanego podzbioru języka przy pomocy
#lang.
Przykładowo, by użyć języka rozszerzonego o
możliwości operowania oknami i grafiką
należy użyć:

#lang scheme/gui

Osiągamy w ten sposób efekt podobny do modułów pythonowych - każdy plik
stanowi moduł, którego nazwa wywodzi się bezpośrednio z nazwy pliku,
z pominięciem rozszerzenia. Plik identicons.scm zawiera
więc moduł o nazwie identicons.

Prócz tej jednej linijki moduł prawdopodobnie nie będzie wymagał żadnych zmian.
require
wciąż działa, a wszystkie definicje są szczelnie zamknięte dla zewnętrznego
świata. Udostępnianie symboli na zewnątrz odbywa się przy pomocy formy
specjalnej
provide,
ja jednak od pierwszego wejrzenia zakochałem się w formie
provide/contract.
Przy jej pomocy bowiem możemy nie tylko określić eksportowane symbole, ale
również określić
kontrakt,
który będzie obowiązywał pomiędzy modułem, a jego użytkownikiem. Przykładowo,
kontrakt funkcji degrees->radians zdefiniowanej w module
util wygląda następująco:

[degrees->radians (number? . -> . number?)]


Oznacza on, że degrees->radians jest funkcją, która oczekuje
liczbę jako argument i obiecuje zwrócić liczbę. Proste. Jak widać, kontrakty
mogą służyć do dynamicznego sprawdzania typów, ale ich możliwości sięgają
daleko poza. Kontrakt można zbudować z dowolnego predykatu, a te mogą
sprawdzać nie tylko typ, ale i wartości i stan obiektów przekraczających
granice kontraktu. Świetne w kontraktach jest to, że są całkowicie opcjonalne.
Nie ma więc żadnego problemu w tym, by dojrzalsze części budowanego przez nas
systemu posiadały kontrakty i jednocześnie współpracowały z nowszymi modułami,
których projekt wciąż jest niejasny - a przez to - pozbawiony kontraktów. Poza
oczywistymi względami technicznymi kontrakty posiadają również niewątpliwą
wartość dokumentacyjną. Odpowiednio dobrane nazwy predykatów mogą wiele
powiedzieć o oczekiwaniach, jakie kładziemy na argumenty funkcji
i o obietnicach jakie składamy co do wartości przez te funkcje zwracanych.

Co ciekawe, kontrakty można stosować nie tylko w kontekście modułów, ale
również
bezpośrednio do definicji,
jak i
do metod i atrybutów obiektów.

Składnia infixowa

Jak wspomina przypis na stronie dokumentacji kontraktów
PLT Scheme udostępnia specjalną
składnię z dwoma kropkami.
Jej działanie jest bardzo proste. Każde wyrażenie o postaci:

(a b . x . c d)

przekształcane jest na:

(x a b c d)

jeszcze przed interpretacją. Oprócz tego, że składnia ta jest bardzo przydatna
przy definiowaniu kontraktów, można ją (nad)użyć chociażby do pisania wyrażeń
algebraicznych. Przykładowo:

(a . * . (b . + . c))

jest tym samym co:

(* a (+ b c))

Osobiście jednak odradzałbym posuwanie się z użyciem tego składniowego lukru
zbyt daleko. ;-)

Narzędzia graficzne

Moje zainteresowanie grafiką w PLT zaczęło się od
tego wspaniałego tutoriala wstępnego.
Polecam go tym, którzy chcą zacząć przygodę z Lispem, ale nudzi ich operowanie
wyłącznie na liczbach. A w zasadzie to polecam go każdemu, tak
dobry jest. :-)

Niestety do implementacji identikon
biblioteka prezentacyjna
jest niewystarczająca. Głównym jej minusem jest brak możliwości obracania
obiektów graficznych. Indeks szybko jednak zaprowadził mnie do
metody rotate klasy dc-path%,
która to stanowi część
większej biblioteki graficznej
dostępnej wraz z PLT.
Klasa dc-path% umożliwia
narysowanie, wypełnienie i manipulacje dowolnym kształtem, złożonym z odcinków,
czy łuków. Konstrukcja takiej ścieżki jest bardzo prosta. Przykładowo, by narysować
trójkąt najpierw tworzymy nowy obiekt dc-path%:

Cały poniższy kod musi
być uruchamiany z linii poleceń mred lub w środowisku
drscheme; funkcjonalność graficzna nie jest dostępna z poziomu
mzscheme.

(define path (make-object dc-path%))

Po tym określamy punkt startowy (”punkt przyłożenia ołówka do kartki”):

(send path move-to 0 0)

i zaczynamy rysować jedną z dostępnych metod, przykładowo
line-to:

(send path line-to 100 100)
(send path line-to 0 100)
(send path line-to 0 0)

Żeby zobaczyć efekt musimy przygotować odpowiedni kontekst do rysowania
(ang. drawing context), czyli w praktyce cokolwiek, co implementuje
interfejs dc<%>.
Skorzystajmy z kodu identikon i użyjmy do tego celu zwykłej ramki (czy też
“okna”):

(define (make-frame-dc)
(let* ((frame (new frame% (label "New frame") (width 200) (height 200)))
(canvas (new canvas% (parent frame))))
(send frame show #t)
(sleep/yield 1)
(send canvas get-dc)))

Utwórzmy więc właściwy obiekt i narysujmy na nim nasz trójkąt:

(define dc (make-frame-dc))
(send dc draw-path path)

W tym momencie na ekranie powinniśmy zobaczyć zarys trójkąta. Pokolorowanie go
to tylko kwestia użycia odpowiedniego pędzla. Wybierzmy dla niego jakiś pozytywny
kolor i przerysujmy nasz trójkąt ponownie (oczywiście wszystko bez restartowania
interpretera, czy otwierania nowej ramki):

(send dc set-brush "green" 'solid)
(send dc draw-path path)

Gotowe, nasz trójkąt jest teraz zielony. A stąd już niedaleko do generowania
kolorowych identikon. Po szczegóły zapraszam do
źródeł.

Wnioski

Podsumowując, PLT jest dojrzałą i bogatą w rozszerzenia implementacją języka
Scheme. Dzięki obszernej i dokładnej dokumentacji łatwo się jej nauczyć
i korzystać z jej możliwości. Dla tych, którzy wolą zacząć od łagodniejszej niż
strony dokumentacji lektury jest blisko z PLT związana książka
“How to Design Programs”. Twórcy PLT nie
boją się eksperymentować - dystrybucja PLT pełna jest
ciekawych
minijęzyków
i odważnych
pomysłów, jest więc prawdziwą kopalnią wiedzy
dla zainteresowanych możliwościami Scheme i implementacją języków programowania ogólnie.Viagra versus levivia
Feldene
Women using viagra
Accutane
Xanax and pregnancy
Information phentermine
Indomethacin
Adipex phentermine xenical
Cefamandole
Adenosine
Lanoxin
Tramadol cod
Oxprenolol
Methylergonovine
Ibuprofen
Nicorette
Pulmonary hypertension and viagra
Probucol
Cialis drug impotence
Cialis impotence drug eli lilly co
Lortab and xanax without a prescription
Levivia viagra compared
Meropenem
Viagra herbal alternative
Saccharin
Flexeril
Colace
Buy phentermine online same day delivery
Mecamylamine
Phentermine dangers
Info on meridia
Aldara
Cheap price on phentermine
Reglan
Purchase xanax
Hydrocodone ap ap
Cialis soft
Levoxyl
Buy phentermine with no prescription
Pyridoxine
Cialis drug for impotence
Order buy phentermine online
Metronidazole
Pharmacy phentermine sister
Ipratropium
Fioricet
Clomocycline
Cheap pharmacy viagra
Pentaerythritol
Buy hydrocodone overnight
Xanax dosage
Amiodarone
10 min viagra
Voltaren
Viagra drug
Low natural resources for the drug phentermine
Phentermine side effects
Order cheap phentermine
Paxil
Lowest price on phentermine
Lexapro interaction with phentermine
Cialis price
Zithromax
Lotrel
Arava
Decamethonium
Arthrotec
Approval cialis
Cope
Levivia and viagra
Xanax for sale
Adipex diet phentermine pill prescription
Potassium
Buy cialis viagra
Pain medication tramadol
Diet ingredient phentermine pill
Dangers of phentermine heart
Terbutaline
Saturday delivery phentermine
Killer pain tramadol
Ambien rx
Cardizem
Viagra sample pack
Cheap tramadol prescriptions online
Meridia weight loss pill
Isoniazid
Cod online pharmacy phentermine sell
Phentermine in florida
Generic viagra overnight delivery
Dexbrompheniramine
Hydrocodone picture
Canadian cialis
Cialis sales uk
Lopressor
Xanax no rx
Xanax death
About phentermine
Cialis immunity
Viagra kaufen
Buy generic viagra online
Phentermine hc
Podophyllum
Ipodate
Octreotide
Real phentermine
Xanax pictures
Kaopectate
Lamivudine
Herbal alternative to viagra
Phentermine blue 30 mg
Valsartan
Anafranil
Norgestrel
Lansoprazole
Tramadol online pharmacy
Budesonide
Phentermine blogging
Mephenytoin
Ditropan
Order viagra now
Keflex
Phentermine with no prescription
Difference between cialis and viagra
Ambien overdose
Tramadol hydrochloride tablet
Cialis review
Phentermine 37.5 mg free shipping
Online pharmacies with doctor consultation for viagra
Busulfan
Phentermine prescribed online
Buying tramadol online
Levivia viagra online
Phentermine worldwide shipment
Phentermine ephedrine
Estrone
Phentermine 37.5 mg sale
Viagra commercial
Nasalcrom
Shipping overnight phentermine
Phentermine without doctor’s approval
Actonel
Phentermine credit card or cod
_cialis et levitra
How long does xanax stay in your body
Ethotoin
Where can i buy phentermine
Buy Atarax
Zidovudine
Order soma carisoprodol
Macrodantin

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)

Strumienie danych

Dzisiaj skupimy się na rozwiązaniu ćwiczeń z drugiego rozdziału Wizard Booka. Na początku zajmiemy się tworzeniem struktur danych, a później tym, w czym podobno Lisp jest najlepszy: przetwarzaniem list.

2.2

Wprawka do dalszych ćwiczeń, nie wymaga szczególnego komentarza.

(define make-segment cons)
(define start-segment car)
(define end-segment cdr)

(define make-point cons)
(define x-point car)
(define y-point cdr)

(define (midpoint-segment segment)
(make-point (average (x-point (start-segment segment))
(x-point (end-segment segment)))
(average (y-point (start-segment segment))
(y-point (end-segment segment)))))

(define (average x y)
(/ (+ x y) 2))

2.3

Poniższa implementacja zakłada, że prostokąt tworzymy podając współrzędne przeciwległych wierzchołków (punktów, utworzonych przy pomocy make-point). Warto zwrócić uwagę na kolejność definicji poszczególnych funkcji. Na początku zdefiniowałem funkcje obliczające obwód (circumference) i powierzchnię (area), korzystając z jeszcze nie istniejących definicji wysokości (height) i szerokości (width) prostokąta. Po tym przeszedłem do definicji tych dwóch brakujących funkcji, korzystając z nieistniejących jeszcze selektorów prostokąta (rectangle-p1 i rectangle-p2) i selektorów punktu (x-point i y-point). Wreszcie cztery selektory udało się zdefiniować korzystając wyłącznie z funkcji wbudowanych w język (cons, car i cdr). Wishful thinking to potężna technika, używajcie jej jednak z rozwagą.

Obwód prostokąta.
(define (circumference rectangle)
(+ (* 2 (height rectangle))
(* 2 (width rectangle))))
Powierzchnia prostokąta.
(define (area rectangle)
(* (height rectangle)
(width rectangle)))
Długość boku ułożonego wzdłuż osi X.
(define (height rectangle)
(abs (- (y-point (rectangle-p1 rectangle))
(y-point (rectangle-p2 rectangle)))))
Długość boku ułożonego wzdłuż osi Y.
(define (width rectangle)
(abs (- (x-point (rectangle-p1 rectangle))
(x-point (rectangle-p2 rectangle)))))
Konstruktory i selektory dla prostokąta.
(define make-rectangle cons)
(define rectangle-p1 car)
(define rectangle-p2 cdr)
Konstruktory i selektory dla punktu.
(define make-point cons)
(define x-point car)
(define y-point cdr)

2.4

Proste zadanie, które zaciera różnicę między kodem, a danymi.

(define (cdr z)
(z (lambda (p q) q)))

2.5

Kolejna zabawna implementacja funkcji cons, car i cdr.

(define (dividable? a b)
(= (remainder a b) 0))

(define (logn a b)
(/ (log b) (log a)))

(define (cons x y)
(* (expt 2 x)
(expt 3 y)))

(define (car x)
(if (dividable? x 3)
(car (quotient x 3))
(logn 2 x)))

(define (cdr x)
(if (dividable? x 2)
(cdr (quotient x 2))
(logn 3 x)))

2.17

Mamy zdefiniować funkcję zwracającą listę z ostatnim elementem danej listy. Niezliczone ilości napisanych do tej pory funkcji przeglądających listy powodują, że nie musimy się nawet zastanawiać nad rozwiązaniem.

(define (last-pair seq)
(if (null? (cdr seq))
seq
(last-pair (cdr seq))))

2.18

Odwrócenie listy? Nic prostszego.

(define (reverse sequence)
(define (iter sequence result)
(if (null? sequence)
result
(iter (cdr sequence)
(cons (car sequence) result))))
(iter sequence '()))

Używając funkcji fold z SRFI 1 sprawa staje się jeszcze prostsza:

(require (only (lib "1.ss" "srfi") fold))
(define (reverse sequence)
(fold cons '() sequence))

2.20

Zadanie znów proste, ale pokazuje jak definiować funkcje o dowolnej liczbie argumentów, co warto zapamiętać na przyszłość.

(require (only (lib "1.ss" "srfi") filter))

(define (same-parity first . rest)
(let ((parity (remainder first 2)))
(cons first
(filter (lambda (x) (= (remainder x 2) parity))
rest))))

2.23

(define (for-each proc seq)
(if (null? seq)
#t
(let ((result (proc (car seq))))
(for-each proc (cdr seq)))))

Być może znając już procedurę map kusiło by nas, by napisać:

(define (for-each proc seq)
(begin
(map proc seq)
#t))

Jednak według standardu R5RS procedura map nie specyfikuje kolejności wykonania mapowań poszczególnych elementów. Przy ewaluacji wyrażenia (map 1+ '(1 2 3)) mogło by się okazać, że najpierw wykonywane jest wyrażenie (1+ 2), potem (1+ 3) i na końcu dopiero (1+ 1). Wbrew pozorom właściwość ta jest bardzo przydatna. Nietrudno sobie bowiem wyobrazić implementację map przeznaczoną dla komputerów wieloprocesorowych wykonująca obliczenia równolegle. Dopóki więc unikamy efektów ubocznych (ang. side-effects) przy mapowaniu, możemy w przyszłości przenieść nasze obliczenia na wiele procesorów, wymieniając jedynie definicję funkcji map.

2.24

Mamy pokazać trzy reprezentacje wyniku ewaluacji wyrażenia (list 1 (list 2 (list 3 4))) przez interpreter. Zacznijmy od tego, co wypisze sam interpreter:

(1 (2 (3 4)))

Teraz wynik ewaluacji w postaci drzewa:

drzewo

I wreszcie postać pudełkowo-wskaźnikowa:

pudełka

2.25

Musimy tylko przy pomocy car i cdr wydobyć z podanych list liczbę 7. Tak pewnie wygląda kod spagetti w Scheme.

  • (car (cdr (car (cdr (cdr '(1 3 (5 7) 9))))))
  • (caar '((7)))
  • (cadr (cadr (cadr (cadr (cadr (cadr '(1 (2 (3 (4 (5 (6 7))))))))))))

Trochę oszukałem i użyłem funkcji caar, która jest tym samym co (car (car ...)), jak i funkcji cadr, która odpowiada kombinacji (car (cdr ...)).

2.27

Przy rozwiązaniu tego ćwiczenia pozwoliłem sobie znowu skorzystać z funkcji fold.

(require (only (lib "1.ss" "srfi") fold))

(define (deep-reverse seq)
(fold (lambda (element result)
(cons (if (pair? element)
(deep-reverse element)
element)
result))
'()
seq))

2.28

Tak przywykłem już do przydatnych funkcji z SRFI-1, że i tym razem nie mogłem się powstrzymać przed użyciem jednej z nich. Bohaterem tego zadania jest append-map.

(require (only (lib "1.ss" "srfi") append-map))

(define (fringe tree)
(if (pair? tree)
(append-map fringe tree)
(list tree)))

2.30 i 2.31

Najpierw rozwiążemy problem podnoszenia liści drzewa do kwadratu przy pomocy map, a potem wyodrębnimy z tej procedury ogólny schemat mapowania liści drzewa.

(define (square x) (* x x))

;; square-tree przy pomocy map
(define (square-tree seq)
(if (pair? seq)
(map square-tree seq)
(square seq)))

;; tree-map do mapowania lisci drzewa
(define (tree-map proc tree)
(if (pair? tree)
(map (lambda (t) (tree-map proc t)) tree)
(proc tree)))

;; square-tree przy pomocy tree-map
(define (square-tree seq)
(tree-map square seq))

2.32

Odrobina teorii mnogości jeszcze nikomu nie zaszkodziła.

(define (subsets s)
(if (null? s)
(list nil)
(let ((rest (subsets (cdr s))))
(append rest
(map (lambda (el) (cons (car s) el))
rest)))))

Działanie algorytmu najłatwiej wyjaśnić na przykładach. Jest tylko jeden zbiór zbioru pustego:

> (subsets '())
(())

Zbiór jednoelementowy ma dwa podzbiory:

> (subsets '(1))
(() (1))

Podzbiory zbioru dwuelementowego osiągamy poprzez zebranie wszystkich podzbiorów zbioru o jednoelementowego (odpowiada za to wyrażenie (rest (subsets (cdr s)))) i następnie dodanie do każdego z tych podzbiorów elementu pierwszego (wyrażenie (map (lambda (el) (cons (car s) el)) rest)).

> (subsets '(2 1))
(() (1) (2) (2 1))

Jak widać, wzięliśmy wszystkie podzbiory zbioru (1), czyli () i (1), a następnie utworzyliśmy nowe podzbiory dodając do tych poprzednich nowy element 2, osiągając (2) i (2 1).

Podobnie rzecz wygląda dla zbiorów o większej ilości elementów:

> (subsets '(3 2 1))
(() (1) (2) (2 1) (3) (3 1) (3 2) (3 2 1))

Bierzemy wszystkie podzbiory zbioru (2 1) (czyli () (1) (2) (2 1) z kroku poprzedniego) i wrzucamy na początek każdego z nich trójkę. Połączenie tych dwóch zbiorów daje nam wynik.

2.33

Ciekawe jest, że odwzorowanie (map) można wyrazić przy pomocy kumulacji. Podobnie zdefiniować można funkcje append i length.

(define (map p sequence)
(accumulate (lambda (x y) (cons (p x) y)) '() sequence))

(define (append seq1 seq2)
(accumulate cons seq2 seq1))

(define (length sequence)
(accumulate (lambda (x y) (+ y 1)) 0 sequence))

2.34

Tym razem uzupełniamy funkcję obliczającą wartość wielomianu za pomocą schematu Hornera.

(define (horner-eval x coefficient-sequence)
(accumulate (lambda (this-coeff higher-terms)
(+ (* x higher-terms) this-coeff))
0
coefficient-sequence))

2.35

Zliczanie liści w drzewie - prosta sprawa.

(define (sum seq)
(accumulate + 0 seq))

(define (count-leaves t)
(sum (map (lambda (x)
(if (pair? x)
(count-leaves x)
1))
t)))

2.36

Uzupełnić brakujący kod? Śmiesznie proste.

(define (accumulate-n op init seqs)
(if (null? (car seqs))
'()
(cons (accumulate op init (map car seqs))
(accumulate-n op init (map cdr seqs)))))

2.37

W tym zadaniu korzystamy ze standardowego map i z funkcji accumulate-n zdefiniowanej w poprzednim zadaniu.

(define (matrix-*-vector m v)
(map (lambda (x) (sum (map * x v))) m))

(define (transpose mat)
(accumulate-n cons '() mat))

(define (matrix-*-matrix m n)
(let ((cols (transpose n)))
(map (lambda (x) (matrix-*-vector cols x)) m)))

2.38

Znamy już prawostronne składanie (fold-right), teraz poznamy składanie lewostronne (fold-left). Wyniki wyrażeń są następujące:

  • (fold-right / 1 (list 1 2 3)) ; => 3/2
  • (fold-left / 1 (list 1 2 3)) ; => 1/6
  • (fold-right list '() (list 1 2 3)) ; => (1 (2 (3 ())))
  • (fold-left list '() (list 1 2 3)) ; => (((() 1) 2) 3)

W tym momencie warto jednak zatrzymać się na chwilę i porównać sobie funkcje składania przedstawione w Wizard Booku i te z SRFI 1. Na początek warto zauważyć, że funkcje fold i fold-right z SRFI 1 potrafią obsłużyć kilka list, podczas gdy proste wersje zwijania z Wizard Booka przyjmują jako ostatni argument dokładnie jedną listę. Po za tym fold-right według SRFI i według panów z MIT działa identycznie. Wywołany jako:

(fold-right op initial list)

zwróci wynik ewaluacji

(op e1 (op e2 ... (op en initial)))

gdzie e1, e2, …, en to kolejne elementy listy list.

Inaczej rzecz się ma ze zwijaniem lewostronnym. fold SRFI-owe rozwinie się w :

(op en ... (op e2 (op e1 initial)) ... )

podczas, gdy to z Wizard Booka rozwinie się do postaci zgoła odmiennej:

(op (op (op initial e1) e2) ... en)

Różnica powstaje z odmiennej kolejności przekazanych argumentów do funkcji op. Jak widać powyżej Wizard Bookowa wersja jako pierwszy argument stawia dotychczasowy wynik (w szczególności initial), zaś w przypadku SRFI na pierwszym miejscu stoi aktualny element iteracji. Dzięki temu, że ta różnica jest tak niewielka, z łatwością wyrazić można SICP-owe fold-left przy pomocy fold z SRFI 1:

(define (sicp-fold-left op initial sequence)
(fold (lambda (x y) (op y x)) initial sequence))

2.41

Ostatnie ćwiczenie, którego rozwiązanie chciałbym dzisiaj przedstawić, dotyczy generowania permutacji i wybierania z nich podzbioru spełniającego pewny warunek. Zadanie 2.41 polega na znalezieniu zbioru takich uporządkowanych trójek liczb naturalnych (bez zera), których suma ma określoną wartość. Zadanie to można jednak uogólnić, co postaram się poniżej uczynić. Na początek kilka pomocniczych definicji, które znane są wam z poprzednich wpisów:

(define nil '())

(define (accumulate op initial seq)
(if (null? seq)
initial
(op (car seq)
(accumulate op initial (cdr seq)))))

(define (sum seq)
(accumulate + 0 seq))

(define (flatmap proc seq)
(accumulate append nil (map proc seq)))

Będziemy również potrzebować funkcji remove i filter z SRFI-1:

(require (only (lib "1.ss" "srfi") remove filter))

Przyda nam się również oparta na remove funkcja remove-= usuwająca z listy liczby równe danej liczbie:

(define (remove-= num l)
(remove (lambda (x) (= x num)) l))

Przejdźmy nareszcie do definicji naszego problemu. Musimy znaleźć trójki niepowtarzających się dodatnich liczb całkowitych nie większych od N, których suma jest równa zadanej liczbie S. Znamy już więc sygnaturę naszej funkcji:

(define (uniq-triples-with-sum s n) ...)

Graficznie:

Zakładając, że potrafimy wygenerować odpowiednie trójki liczb nie większych od N, możemy zdefiniować szukaną funkcję następująco:

(define (uniq-triples-with-sum s n)
(filter (lambda (triple) (sum-is? s triple))
(uniq-triples n)))

co na schemacie możemy przedstawić jako:

Definicja predykatu sum-is? jest prosta:

(define (sum-is? n seq)
(= (sum seq) n))

uniq-triples to twardsza sztuka. Patrząc jednak na problem z odpowiedniej perspektywy można spostrzec, że zbiór unikalnych uporządkowanych trójek nie większych niż liczba N można otrzymać generując wszystkie możliwe wariacje bez powtórzeń zbioru liczb od 1 do N. Zapisując to w Scheme:

(define (uniq-triples n)
(if (< n 3)
(error "n must be greater than 3, otherwise we won't be able to generate a triple")
(permutations (enumerate n) 3)))

Schemat zaś przedstawia się następująco:

Zredukowaliśmy problem do implementacji dwóch funkcji: enumerate i permutations. Nie trzymam was więcej w niepewności - poniżej ich definicje, które kończą rozwiązanie naszego zadania.

(define (enumerate n)
(define (iter n result)
(if (zero? n)
result
(iter (- n 1) (cons n result))))
(iter n nil))

(define (permutations s k)
(if (zero? k)
(list nil)
(flatmap (lambda (x)
(map (lambda (p) (cons x p))
(permutations (remove-= x s) (- k 1))))
s)))

Skoro jednak zahaczyliśmy o kombinatorykę, szkoda byłoby nie zgłębić tematu trochę bardziej. Poniżej znajdziecie definicję permutacji:

(define (permutations-for-k=n s)
(permutations s (length s)))

Skorzystałem tutaj z powiązania permutacji i wariacji bez powtórzeń. Definicja wariacji z powtórzeniami wymaga jednak trochę więcej zachodu:

(define (permutations-with-repetition n k)
(if (zero? k)
(list nil)
(flatmap (lambda (perm-part) (map (lambda (element)
(cons element perm-part))
(enumerate n)))
(permutations-with-repetition n (- k 1)))))

Przykładowo, by wygenerować zbiór wszystkich trójek (tym razem nie unikalnych) nie większych od N, wystarczy napisać:

(define (triples n)
(permutations-with-repetition n 3))

Testowanie

Dzisiaj rozszerzymy odrobinę tematykę wykładu i zajmiemy się
pisaniem testów dla kodu, który tworzymy. W większości ćwiczeń,
gdzie za zadanie mamy stworzyć funkcję wykonującą jakieś konkretne
przekształcenie autorzy podają przykład jej użycia - przekazane
argumenty i oczekiwany wynik. My zajmiemy się przekształceniem
tych przykładów w wykonywalne specyfikacje, które automatycznie
będą sprawdzać poprawność naszych procedur.

Uwaga techniczna

Do uruchomienia przykładów znajdujących się w tym wpisie będziesz
potrzebował interpretera
mzscheme
i rozszerzenia standardu
srfi-78.
Kod umieść w pliku rational.scm, w tym samym katalogu
co plik check.scm, a na jego początku zamieść instrukcje
wczytujące niezbędne do testowania biblioteki:

(require (lib "23.ss" "srfi") (lib "42.ss" "srfi"))
(load "check.scm")

Program uruchamiaj poleceniem:

mzscheme -r rational.scm

1. Specyfikacja (nie)formalna

Na wykładzie określono interfejs liczb wymiernych poprzez funkcje
make-rat, numer i denom,
zaś oczekiwane ich działanie zostało streszczone w poniższych
dwóch wyrażeniach:

(numer (make-rat n d)) ; => n
(denom (make-rat n d)) ; => d

Mówiąc po ludzku: dla dowolnych liczb całkowitych n
i d, licznik (ang. numerator) ułamka
utworzonego przy pomocy wywołania procedury
(make-rat n d) ma być równy n, zaś
mianownik (ang. denominator) ma być równy
d.

Jako, że wykład skupia się głównie na wykorzystaniu tak uzyskanego
interfejsu ja chciałbym spojrzeć na rzecz od strony
implementatora. Wcielimy się teraz w rolę George’a, który ma
procedury make-rat, numer i
denom napisać. Jeszcze raz przyjrzyjmy się założeniom
jakie nasza implementacja ma spełniać:

  1. procedura make-rat powinna przyjmować dwa
    liczbowe argumenty i zwracać obiekt reprezentujący ułamek
  2. procedura numer powinna przyjmować jeden argument
    będący ułamkiem utworzonym przez make-rat i
    zwracać jego licznik
  3. procedura denom powinna przyjmować jeden argument
    będący ułamkiem utworzonym przez make-rat i
    zwracać jego mianownik

Mając tak dokładnie sprecyzowane wymagania można już usiąść do
implementacji. Ja proponuję jednak chwilę się z tym wstrzymać i
zastanowić się nad sposobem w jaki możemy podejść do, może
nie udowodnienia, ale przetestowania poprawności naszego
przyszłego rozwiązania. Mając pod ręką zestaw prostych testów
obejmujących gamę możliwych argumentów, podejdziemy do
implementacji o wiele pewniej. W miarę jak nasz kod stopniowo
będzie przechodził kolejne testy będziemy pewni, że kierunek w
którym zmierzamy jest właściwy. Kompletność zaś poszczególnych
testów pomoże nam sprostać wymaganiom ustalonej wcześniej
specyfikacji.

2. Pierwsze testy

Do zakodowania testów użyjemy oczywiście samego języka
Scheme, a dokładniej skorzystamy z prostej biblioteki do
testowania opisanej w
SRFI-78: Lightweight testing“.
Wciąż powtarza się tutaj słowo “testowanie”, ja jednak wolę myśleć
o tych kawałkach kodu jako o specyfikacji zapisanej w taki sposób,
że można ją wykonać. Podobnie jak w przedmowie do Wizard Booka
ujęli to autorzy, “możliwość ich wykonania przez maszyny
powinna być traktowana jako coś dodatkowego”
.

Składnia testów zdefiniowanych w SRFI-78 jest bardzo prosta.
W praktyce wystarczy znajomość składni makra check.
Jest ona następująca:

(check wyrażenie-testowane => wartość-oczekiwana)

Podczas testowania wyrażenie jest obliczane
i jego wartość jest porównywana z wartością oczekiwaną. Jeżeli
obie się różnią wyświetlany jest błąd. To wszystko. O tym jak
wykorzystać ten mechanizm w praktyce - poniżej.

Zapis specyfikacji z punktu pierwszego wygląda następująco:

(define rational (make-rat x y))
(check (numer rational) => x)
(check (denom rational) => y)

Powyższy kod jest zapisem naszej specyfikacji z jednym małym
wyjątkiem - brakuje tutaj określenia “dla dowolnego” liczbowego
argumentu. Spróbujmy uwzględnić tę kwestię w naszym teście.

(define (check-rat x y)
(define rational (make-rat x y))
(check (numer rational) => x)
(check (denom rational) => y))

(for-each (lambda (pair-of-args) (apply check-rat pair-of-args))
any-pair-of-arguments)

Pomocnicza procedura check-rat sprawdza poprawność wyników
generowanych przez procedury make-rat, numer
i denom dla zadanych argumentów x
i y. Argumenty te pochodzą z generatora
any-pair-of-arguments. Niestety zbiór liczb całkowitych
jest nieskończony, nie możemy więc wymagać, by nasz program zakończył
testowanie, jeżeli zaimplementujemy any-pair-of-arguments
jako nieskończony zbiór par (x, y). Zmuszeni jesteśmy
ograniczyć testowanie do pewnego skończonego zbioru danych testowych.
Wypróbujmy najpierw prosty dodatni ułamek 1/2:

(define any-pair-of-arguments '((1 2)))

Uruchamiamy test poprzez zwykłe wywołanie naszego skryptu - moduł
srfi-78 i jego makro check zajmują się resztą:

mzscheme -r rational.scm

Otrzymujemy błąd:

reference to undefined identifier: make-rat

Rzeczywiście, nie zdefiniowaliśmy jeszcze żadnej z wymaganych procedur.
Zróbmy to teraz, trochę jednak oszukując, bo nasze procedury będą
zawsze zwracały wartość prawda (#t):

(define (make-rat x y) #t)
(define (numer rational) #t)
(define (denom rational) #t)

Zobaczmy jak zachowa się nasz test:

(numer rational) => #t ; *** failed ***
 ; expected result: 1

(denom rational) => #t ; *** failed ***
 ; expected result: 2

Kod nie spełnia specyfikacji i nasz test to wykrywa. Dobrze,
spróbujmy go naprawić. Zmienię definicję numer
i denom:

(define (numer rational) 1)
(define (denom rational) 2)

Jaki będzie wynik testów?

(numer rational) => 1 ; correct
(denom rational) => 2 ; correct

Test uznaje nasze procedury za poprawne, chociaż my wiemy,
że nasz kod nie jest poprawny. To wyraźny znak, że nasze testy nie są
wystarczające. Dodajmy więc dwa nowe zestawy testowe:

(define any-pair-of-arguments
'((1 2) (3 4) (5 6)))

Tym razem nasz kod powoduje spodziewane błędy:

(numer rational) => 1 ; correct
(denom rational) => 2 ; correct
(numer rational) => 1 ; *** failed ***
 ; expected result: 3
(denom rational) => 2 ; *** failed ***
 ; expected result: 4
(numer rational) => 1 ; *** failed ***
 ; expected result: 5
(denom rational) => 2 ; *** failed ***
 ; expected result: 6

Żeby przejść wszystkie te przypadki testowe nie możemy już oszukiwać
i jesteśmy zmuszeni zapamiętać przekazane do make-rat
liczby. Skorzystamy z przedstawionych na wykładzie par:

(define (make-rat x y) (cons x y))
(define (numer rational) (car rational))
(define (denom rational) (cdr rational))

Gratulacje, nasz kod przechodzi wszystkie testy:

(numer rational) => 1 ; correct
(denom rational) => 2 ; correct
(numer rational) => 3 ; correct
(denom rational) => 4 ; correct
(numer rational) => 5 ; correct
(denom rational) => 6 ; correct

Moduł srfi-78 umożliwia wypisanie podsumowania wszystkich testów.
Wystarczy dodać na koniec pliku następującą linijkę:

(check-report)

3. Skracanie ułamków

W tej chwili mamy już działające procedury do tworzenia ułamków
i związane z nimi automatyczne testy. Zazwyczaj kod i testy
umieszcza się w oddzielnych plikach, wydzielmy więc z pliku
rational.scm wszystko co związane z testami i umieśćmy
w pliku rational-test.scm, pamiętając o dodaniu
dyrektywy wczytującej definicje z oryginalnego pliku
rational.scm: (load "rational.scm").
Po wykonaniu tych czynności pliki rational.scm
i rational-test.scm powinny wyglądać następująco:

;; plik rational.scm

(define (make-rat x y) (cons x y))
(define (numer rational) (car rational))
(define (denom rational) (cdr rational))

;; plik rational-test.scm

(require (lib "23.ss" "srfi") (lib "42.ss" "srfi"))
(load "check.scm")

(load "rational.scm")

(define (check-rat x y)
(define rational (make-rat x y))
(check (numer rational) => x)
(check (denom rational) => y))

(define any-pair-of-arguments
'((1 2) (3 4) (5 6)))

(for-each (lambda (pair-of-args) (apply check-rat pair-of-args))
any-pair-of-arguments)

(check-report)

Mając już gotową podstawową funkcjonalność zajmiemy się jej
stopniowym rozszerzaniem. Zaczniemy od skracania ułamków.
Mówiąc dokładnie, chcemy, by dla ułamka utworzonego przy pomocy
(make-rat 2 4), procedura numer
zwracała 1, zaś procedura denom zwracała 2
(tym samym skracając 2/4 do 1/2). Jak zwykle zacznijmy od
zdefiniowania nowego testu.

(let ((rational (make-rat 2 4)))
(check (numer rational) => 1)
(check (denom rational) => 2))

Wynik testu:

(numer rational) => 2 ; *** failed ***
 ; expected result: 1

(denom rational) => 4 ; *** failed ***
 ; expected result: 2

Czas wrócić do kodu i zaimplementować skracanie. Gdzie to skracanie
zaimplementujemy nie ma znaczenia - możemy skracać zarówno przy
tworzeniu (make-rat), jak i przy odczycie (procedury
numer i denom). Ja pójdę na łatwiznę
i przepiszę z książki skracanie przy tworzeniu. ;-)

(define (make-rat n d)
(let ((g (gcd n d)))
(cons (/ n g) (/ d g))))

Dodajmy jeszcze jeden test dla pewności:

(let ((rational (make-rat 15 25)))
(check (numer rational) => 3)
(check (denom rational) => 5))

Nasz kod wciąż przechodzi wszystkie testy, implementacja jest więc
rzeczywiście poprawna.

W naszych testach można jednak zauważyć nieprzyjemne powtórzenia.
Warto więc poświęcić chwilę na uogólnienie naszych przypadków
testowych. Zmieńmy odrobinę napisaną wcześniej pętlę
for-each tak, by zamiast listy par argumentów
przyjmowała listy argumentów i oczekiwanych wyników.

(define (check-rat arguments expected-numer expected-denom)
(define rational (apply make-rat arguments))
(check (numer rational) => expected-numer)
(check (denom rational) => expected-denom))

(for-each (lambda (args) (apply check-rat args)) test-cases)

Definicja przypadków testowych wyglądać będzie teraz następująco:

(define test-cases
(list (list '(1 2) 1 2)
(list '(3 4) 3 4)
(list '(5 6) 5 6)))

Dodanie ostatnich dwóch przypadków na skracanie jest trywialne:

(define test-cases
(list (list '(1 2) 1 2)
(list '(3 4) 3 4)
(list '(5 6) 5 6)
;; skracanie licznika z mianownikiem
(list '(2 4) 1 2)
(list '(15 25) 3 5)))

4. Normalizacja znaków

Mamy już zdefiniowane i przetestowane procedury dotyczące ułamków
w takiej formie, jak określono je na wykładzie. Teraz zajmiemy się
ich dalszym rozszerzaniem, jako źródło natchnienia traktując
ćwiczenie 2.1.
Naszym zadaniem jest takie zmodyfikowanie procedury make-rat,
by radziła sobie zarówno z dodatnimi, jak i z ujemnymi argumentami.
Dodatkowo znak całego ułamka musi być normalizowany - ułamek ujemny
ma mieć ujemny licznik i dodatni mianownik, zaś ułamek dodatni ma
mieć dodatni zarówno licznik, jak i mianownik. Zapiszmy wszystkie cztery
możliwe przypadki w postaci testów:

;; ...
;; normalizacja znakow
(list '(1 1) 1 1)
(list '(-1 1) -1 1)
(list '(1 -1) -1 1)
(list '(-1 -1) 1 1)))

Gdy uruchomimy nasze testy przekonamy się, że nasz kod przechodzi
pierwsze dwa z nich, generuje jednak błąd dla dwóch ostatnich.
Dwa ostatnie przypadki są więc tymi, które musimy naprawić. Dla
argumentów (1 -1) oczekujemy licznika równego -1
i mianownika równego 1. Dla argumentów (-1 -1)
oczekujemy licznika i mianownika równego 1. Łącząc te dwa przypadki
otrzymujemy zależność: jeżeli drugi argument make-rat
jest ujemny, odwróć znaki obu argumentów
. Ten właśnie warunek
zapiszemy w kodzie.

(define (make-rat n d)
(if (negative? d)
(make-rat (- n) (- d))
(let ((g (gcd n d)))
(cons (/ n g) (/ d g)))))

Gotowe. Wszystkie testy przechodzą bez błędów. Wygląda więc na to,
że udało nam się sprytnie zaimplementować wymaganą funkcjonalność
nie psując niczego po drodze. Pewność, że niczego nie popsuliśmy
dają nam właśnie testy - o wiele szybciej i prościej pisać je
w trakcie pisania samego kodu, niż później debugować błędy
wywołane zmianami w większym systemie. Pisanie testów jest więc
rodzajem inwestycji. Pisanie kodu zajmuje nam teraz trochę więcej
czasu, ale oszczędzamy czas (i nerwy) w przyszłości. Do tego, jak
widzieliśmy przed chwilą, testy pomagają przy samym pisaniu
programu, ułatwiając zdefiniowanie zachowania jakiego oczekujemy
od naszych procedur, a czasami nawet wskazując prostsze sposoby
rozwiązania problemów.

5. Skrajne przypadki

Nasza implementacja ułamków potrafi skracać licznik z mianownikiem
i normalizować ich znak. Brakuje teraz jeszcze tylko jednej rzeczy
- obsługi sytuacji wyjątkowych. Zakładając poprawne typy argumentów
sytuacja taka w przypadku ułamków jest tylko jedna - dzielenie przez
zero. W tej chwili make-rat nie protestuje, gdy jako
mianownik przekażemy mu zero. Procedura powinna jednak zgłosić
błąd przy pomocy standardowej procedury error. By
przetestować sytuację wyjątkową skorzystamy z makra
with-handlers,
które zajmuje się obsługą wyjątków.

(check (with-handlers ((exn? (lambda (e) 'error)))
(make-rat 1 0)) => 'error)

W powyższym wyrażeniu każdy wyjątek rzucony przez make-rat
zostanie przechwycony przez makro
with-handlers,
a wyrażenie przyjmie wartość zwróconą przez podaną funkcję (w naszym
przypadku wartością tą będzie symbol error). Jako, że
spodziewamy się wyjątku, piszemy => 'error.

Uruchomienie testu pokazuje, że spodziewany wyjątek nie został rzucony:

(with-handlers ((exn? (lambda (e) (quote error)))) (make-rat 1 0)) 
    => (1 . 0) ; *** failed ***
 ; expected result: error

Wracamy do definicji make-rat i dopisujemy przypadek
dla mianownika równego zero:

(define (make-rat n d)
(cond ((negative? d)
(make-rat (- n) (- d)))
((zero? d)
(error "Mianownik nie moze byc zerem."))
(else (let ((g (gcd n d)))
(cons (/ n g) (/ d g))))))

Gdy uruchomimy test ponownie przekonamy się, że nasze zadanie
zostało wykonane - wyjątek jest rzucany. Tym samym skutecznie
przetestowaliśmy sytuację wyjątkową.

Podsumowanie

Pisanie automatycznych testów równolegle z kodem ma wiele zalet.
Zbyt mało jest miejsca na tym blogu, by opisać je
wszystkie, zachęcam zatem do zapoznania się z tematem na własną
rekę. Technikę tę nazwano Test Driven
Development (w skrócie TDD), czyli programowanie
sterowane testami. Materiałów po polsku niestety nie ma zbyt wiele,
pozwolę wymienić sobie trzy artykuły. Każdy z nich opisuje testowanie
w innym języku programowania, zahacza jednak (mniej lub bardziej)
również o samą technikę.

Powodzenia w testowaniu!

Ćwiczenia na deser

Jedną z zalet Lispa jest to, że jest niezniszczalny. Od jego powstania trzon języka pozostaje niezmienny; minimalistyczny, ale na tyle potężny, by inne języki programowania czerpały z niego aż do teraz. Oznacza to również, że można zostawić Lispa na kilka miesięcy i po tym wrócić do niego, mając pewność, że zbyt wiele się nie zmieniło. Muszę przyznać, że pod względem dydaktycznym jest to ogromna zaleta. Tym samym wracamy do końcówki pierwszego rozdziału Wizard Booka.


Lisp machine

W miarę jak autorzy przedstawiają procedury wyższego rzędu można zrozumieć, że procedura jest czymś więcej, niż ktoś wychowany na C/C++/Javie mógłby podejrzewać. Nie jest to bowiem zwykły kawałek kodu, kontener dla ciągu instrukcji, które mamy zamiar wiele razy wywołać. Procedura nie przedstawia tylko obliczeń wykonywanych na liczbach i strukturach - jest raczej formalnym sposobem zapisu metody postępowania, która sama w sobie może korzystać nie tylko z liczbowych argumentów, ale i z innych metod. Ukazuje to wyraźnie stopnie abstrakcji, po których możemy się wspinać, definiując kolejne procedury na szczycie innych procedur. Szybko odrywamy się od pojęcia funkcji operującej na liczbach na rzecz ogólniejszej metody, sposobu postępowania, algorytmu, który możemy zapisać w postaci kodu.

Wykorzystajmy tę wiedzę w praktyce. Poniżej rozwiązanie kilku ćwiczeń rozdziału pierwszego.

Ćwiczenie 1.16

W zadaniu tym należy przekształcić podaną wcześniej definicję procedury potęgowania z formy generującej proces rekurencyjny na proces iteracyjny. Dzięki wskazówce zadanie to jest całkiem proste. Dla parzystych n liczbę b podnosimy do kwadratu, zaś dla n nieparzystych a mnożymy przez b, zachowując w ten sposób stałą wartość wyrażenia a*b^n. Poniżej pełne rozwiązanie:

(define (fast-expt a b n)
(cond ((= n 0) a)
((even? n) (fast-expt a (square b) (/ n 2)))
(else (fast-expt (* a b) b (- n 1)))))

Ćwiczenie 1.17

Tutaj autorzy proszą nas o zdefiniowanie operacji mnożenia korzystającą jedynie z dodawania, ale w taki sposób, by procedura wykonywała logarytmiczną liczbę kroków ze względu na argumenty a i b. Logarytmiczna liczba kroków w stosunku do argumentu oznacza, że przy dwukrotnym (lub w ogólności n-krotnym) wzroście argumentu liczba kroków zwiększa się o jeden. W rozwiązaniu wykorzystać mamy dwie pomocnicze funkcje double i halve, które odpowiednio podwajają i dzielą na dwa swoje argumenty:

(define (double n) (+ n n))

(define (halve n) (/ n 2))

Rozwiązanie:

(define (fast-mul a b)
(cond ((= b 0) 0)
((even? b) (double (fast-mul a (halve b))))
(else (+ a (fast-mul a (- b 1))))))

Ćwiczenie 1.30

W tym ćwiczeniu wystarczy uzupełnić kod. Mając już za sobą pewne doświadczenie w przekształcaniu procesu rekurencyjnego w iteracyjny, znalezienie prawidłowego rozwiązania nie jest żadnym wyzwaniem:

(define (sum term a next b)
(define (iter a result)
(if (> a b)
result
(iter (next a) (+ (term a) result))))
(iter a 0))

Ćwiczenie 1.31

Należy zdefiniować podobną do sum procedurę product, która zwraca iloczyn wartości funkcji w punktach zadanego zakresu. Oto wersja rekurencyjna:

(define (product term a next b)
(if (> a b)
1
(* (term a)
(product term (next a) next b))))

A to wersja iteracyjna:

(define (product term a next b)
(define (iter a result)
(if (> a b)
result
(iter (next a)
(* (term a) result))))
(iter a 1))

Na podstawie product definicja silni jest trywialna:

(define (identity x) x)
(define (inc n) (+ n 1))

(define (factorial n)
(product identity 1 inc n))

Równie łatwo jest zapisać funkcję liczącą przybliżenie liczby π na podstawie wzoru Johna Wallisa:

(define (pi-wallis n)
(define (pi-term n)
(/ (square (* 2 n))
(* (+ (* 2 n) 1)
(- (* 2 n) 1))))
(* (product pi-term 1 inc n) 2.0))

Przy dziesięciu tysiącach kroków funkcja ta przybliża π do czterech miejsc po przecinku, co nie jest wynikiem zbyt imponującym:

> (pi-wallis 10000)
3.141514118681922

Podsumowanie rozdziału pierwszego

Wpisem tym chciałbym zakończyć i podsumować pierwszy rozdział Wizard Booka, zatytułowany “Budowanie abstrakcji za pomocą procedur”. Materiał tego rozdziału został omówiony na wykładach wideo o numerach 1a, 1b i 2a. Poniżej lista pojęć, które udało nam się poznać:

  • Inżynieria oprogramowania różni się od innych dziedzin inżynierii tym, że największym ograniczeniem nie są prawa fizyki, ale nasze własne umysły. Innymi słowy, zbudować możemy wszystko to, co jesteśmy w stanie sobie wyobrazić.
  • Abstrakcja jest sposobem na radzenie sobie ze złożonością dużych systemów. Zamykanie funkcjonalności w “czarnych skrzynkach” pozwala nam skupić się na problemie, pomijając nieistotne szczegóły.
  • Procedury generują (czy też opisują) procesy. Procesy podczas wykonania mogą rozwijać się na różne sposoby - “kształt” procesu iteracyjnego jest inny niż “kształt” procesu rekurencyjnego.
  • Procedury reprezentują ogólne metody obliczeń, mogą więc przyjmować inne procedury jako argumenty i zwracać procedurę jako wynik.

Prócz tych ogólnych zagadnień poznaliśmy kawałek języka Scheme:

W moich wpisach pojawiły się również elementy wykraczające poza pierwszy rozdział Wizard Booka, a mianowicie:

Tym samym ostatecznie żegnamy się z rozdziałem pierwszym i przechodzimy do rozdziału drugiego, gdzie zajmiemy się budowaniem struktur danych.

W poszukiwaniu idealnej formy

Zaczęło się niewinnie. Otworzyłem końcowe strony pierwszego rozdziału Wizard Booka z nadzieją na rozwiązanie kilku zadań i rychłe przejście do części mówiącej o abstrakcji danych, po czym przeczytałem treść zadania 1.22 i zabrałem się do pracy. Ogólnie rzecz biorąc należało napisać procedurę search-for-primes, która szuka 3 liczb pierwszych większych od zadanego n i pokazuje czas wykorzystany na obliczenie każdej z nich. Definicje funkcji wypisujących zostały podane w ćwiczeniu, ale jako że ja zawsze wiem lepiej (i nie podoba mi się mieszanie kodu obliczającego z wyświetlającym), napisałem je po swojemu. Zacząłem od napisania ogólnej funkcji do mierzenia czasu wykoniania dowolnej funkcji:

(define (timed-function-test fun)
(let ((start-time (runtime))
(result (fun))
(end-time (runtime)))
(values result (- end-time start-time))))

Jej działanie jest proste: pobiera czas początkowy, wywołuje funkcję zapamiętując wynik, pobiera czas końcowy i zwraca dwie wartości: wartość wywołanej funkcji i czas jej wykonania. Tak, naprawdę zwraca dwie wartości, a to dzięki formie specjalnej values. Później tego wyboru pożałowałem, o wiele wygodniej użyć jest zwykłej pary, values nie dają się bowiem zagnieżdżać i ogólnie rzecz biorąc nie można z nimi zrobić nic poza natychmiastowego rozłożenia na zwracane wartości przy pomocy let-values. Wrócimy jednak do tego jeszcze później.

Do powyższej definicji timed-function-test dodać jeszcze trzeba skąd wzięło się runtime. W implementacji Scheme, której używam (mzscheme) nie ma funkcji runtime, jest za to current-inexact-milliseconds, które można z łatwością zaliasować:

(define runtime current-inexact-milliseconds)

Korzystając z procedury timed-function-test funkcję timed-prime-test zdefiniować można następująco:

(define (timed-prime-test n)
(timed-function-test (lambda () (prime? n))))

Obudowujemy funkcję prime? lambdą bez argumentów, dzięki temu właśnie funkcja timed-function-test może ją wywołać poprzez proste (fun).

1.

Mierzenie czasu mamy już opracowane, czas zająć się szukaniem samych liczb pierwszych.

(define (search-for-primes how-many start)
(define (_search-for-primes how-many start results)
(if (= how-many 0)
(reverse results)
(let-values (((primality elapsed-time)
(timed-prime-test start)))
(_search-for-primes (if primality
(- how-many 1)
how-many)
(+ start 2)
(if primality
(cons (cons start elapsed-time)
results)
results)))))
(_search-for-primes how-many
(if (even? start)
(+ start 1)
start)
'()))

Wewnątrz definiuję pomocniczą funkcję _search-for-primes, by móc zbierać liczby pierwsze na liście results, jak i po to by zawsze zaczynać poszukiwania od liczby nieparzystej (stąd forma if z predykatem even?). W każdej iteracji zwiększamy bowiem aktualną liczbę do sprawdzenia o 2, wychodząc z założenia, że żadna liczba parzysta poza 2 nie jest pierwsza i warto skupić się tylko na liczbach nieparzystych. Nie zmniejsza to złożoności, ale liczba kroków jest o połowę mniejsza. Przy pomocy formy specjalnej let-values przypisuję do zmiennych primality i elapsed-time wartości zwrócone przez timed-prime-test. Jeżeli sprawdzona liczba była pierwsza wywołujemy siebie ze zmniejszoną o jeden wartością how-many i dodaną do listy wyników results liczbą i czasem jej obliczania. Jeżeli trafiliśmy na liczbę złożoną, wywołujemy siebie z niezmienionymi wartościami how-many i results, zwiększając tylko aktualną liczbę o 2.

2.

Ot, podręcznikowy przykład wykorzystania rekursji do iteracji. Problem w tym, że kod ten jest brzydki. Przedstawia on imperatywną ideę zapisaną przy pomocy dziwacznej lispowej składni. Podobny kod w Pythonie można by zapisać następująco:

def search_for_primes(how_many, start):
result = []

if start % 2 == 0:
start += 1

while how_many > 0:
primality, elapsed_time = timed_prime_test(start)
if primality:
how_many -= 1
result.append((start, elapsed_time))
start += 2

return result

Tak, to jest o wiele czytelniejsze. Ale nie dałem za wygraną. Scheme nie ma czego zazdrościć Pythonowi, jeżeli chodzi o składnię, bo składnię może mieć taką, jaką tylko sobie wymarzymy 8-). Pobawiłem się więc trochę makrami i udało mi się przepisać funkcję search-for-primes tak by przypominała Pythonowy kod:

(define (search-for-primes how-many start)
(define result-list '())

(begin
(if (even? start)
(inc! start))
(while (> how-many 0)
(let-values (((primality elapsed-time)
(timed-prime-test start)))
(begin
(if primality
(begin
(dec! how-many)
(append-to-list! result-list
(cons start elapsed-time))))
(inc! start 2))))
result-list))

Prawdopodobnie można by za pomocą większej ilości magii jeszcze bardziej ten kod upodobnić do Pythonowego, tyle że jaki w tym sens. Moim celem nie jest nauczenie się programowania w Pythonie ze składnią Scheme, ale poznanie nowych sposobów rozwiązywania problemów. A w tym przypadku czuję, że brnę w złym kierunku. Musi istnieć bardziej naturalny dla Scheme sposób rozwiązania tego zadania.

3.

Strumień
Zacząłem więc od analizy tego, co było złego w starej wersji search-for-primes. Przede wszystkim nie podobało mi się to w jaki sposób wykonywane są kolejne iteracje. Starałem się wykorzystać zwykłą rekurencję, według zasad jakich nauczyłem się wcześniej. Chcąc uczynić wykonywany proces iteracyjnym wykorzystałem do zbierania wyników listę results. Efektem jest procedura, która nie przypomina pierwotnie postawionego problemu. Właśnie, na czym ten problem polegał? Należy wyselekcjonować M liczb pierwszych większych od zadanego N i pokazać czas wykonania procedury sprawdzającej dla każdej z nich. Selekcja, czyli filtrowanie! To jest właśnie to, czego szukałem. Ułożę całą procedurę w jeden ciąg funkcji filtrujących i mapujących, na jednym końcu wpuszczając do strumienia liczby nieparzyste, a na drugim wyciągając liczby pierwsze. Strumień - to jest odpowiedź!

By jednak móc wykorzystać tę technikę musiałem utworzyć generator - funkcję, która zachowuje stan i przy kolejnych wywołaniach zwraca (generuje) kolejne wartości. W Pythonie tworzenie generatorów jest bardzo proste:

def nieparzyste():
i = 1
while True:
yield i
i += 2

gen = nieparzyste()
print gen.next() # => 1
print gen.next() # => 3
print gen.next() # => 5

W Scheme z generatorami jeszcze się nie spotkałem. Przeszukałem więc archiwa grupy comp.lang.scheme, potem zasięgnąłem rady Google. Wreszcie w artykule Iterators: Signs of Weakness in Object-Oriented Languages znalazłem to, czego szukałem. I rozwiązanie okazało się najprostsze jak to tylko możliwe. Należy utworzyć listę z opóźnionym wartościowaniem (czy też wartościowaniem leniwym od ang. lazy evaluation). Taką listę można łatwo wykorzystać do napisania generatora: kolejne wartości listy obliczane są bowiem dopiero wtedy, gdy są potrzebne. A jak stworzyć taką leniwą listę? Przy pomocy lambda, proszę bardzo:

(define-syntax lazy-cons
(syntax-rules ()
((_ x y)
(cons x (lambda () y)))))

Makro to przekształca (lazy-cons x y) na (cons x (lambda () y)), zamykając drugą wartość w lambdzie, tym samym opóźniając jej obliczenie.
Pobranie pierwszego elementu pary następuje przez zwykłe car:

(define lazy-car car)

Uzyskanie zaś drugiego elementu wymaga wywołania lambdy:

define (lazy-cdr x)
((cdr x)))

Dzięki temu mechanizmowi uzyskujemy możliwość tworzenia nieskończonych list. Listę liczb naturalnych można otrzymać za pomocą następującej definicji:

(define (infinity-starting-at start)
(lazy-cons start (infinity-starting-at (+ start 1))))

(define natural-numbers
(infinity-starting-at 1))

Do kolejnych elementów odwołujemy się tak samo, jak do zwyczajnej listy, tyle że korzystając z “leniwych” wersji funkcji car i cdr.

> (lazy-car natural-numbers)
1
> (lazy-car (lazy-cdr natural-numbers))
2
> (lazy-car (lazy-cdr (lazy-cdr natural-numbers)))
3
> (lazy-car (lazy-cdr (lazy-cdr (lazy-cdr natural-numbers))))
4

Leniwe listy już miałem zaimplementowane, teraz przyszedł czas na leniwe wersje funkcji map i filter. Definicja pierwszej jest bardzo prosta:

define (lazy-map fun lazy-list)
(if (null? lazy-list)
'()
(lazy-cons (fun (lazy-car lazy-list))
(lazy-map fun (lazy-cdr lazy-list)))))

Każdą z wartości listy lazy-list przepuszczamy przez funkcję fun. Na miejsce starej listy otrzymujemy nową, z każdą wartością będącą wynikiem działania fun. Implementacja lazy-filter jest tylko odrobinę bardziej skomplikowana:

(define-opt (lazy-filter decide-fun lazy-list [how-many -1])
(if (or (null? lazy-list) (= how-many 0))
'()
(let ((current (lazy-car lazy-list)))
(if (decide-fun current)
(lazy-cons current
(lazy-filter decide-fun
(lazy-cdr lazy-list)
(- how-many 1)))
(lazy-filter decide-fun
(lazy-cdr lazy-list)
how-many)))))

Kilka rzeczy wymaga tutaj komentarza. Po pierwsze, użyte przeze mnie nawiasy kwadratowe służą tylko i wyłączenie zwiększeniu czytelności. Ich działanie jest dokładnie takie same jak nawiasów okrągłych. Tymczasem samo define-opt zostało zdefiniowane w SRFI 89, rozszerzeniu do standardu, które implementuje opcjonalne i kluczowe parametry dla funkcji. W tym przypadku domyślną wartością dla how-many jest -1. Tym samym funkcja lazy-filter może być wywołana z dwoma lub trzema parametrami. Jej działanie polega na przefiltrowaniu na podstawie funkcji decide-fun podanej leniwej listy, tzn. wyborze tych elementów listy, dla których funkcja zwraca wartość prawda. Opcjonalny parametr how-many każe filtrowi przerwać działanie po wyselekcjonowaniu podanej ilości elementów.

Jesteśmy już prawie gotowi. Ostatnią rzeczą, jaka jest potrzebna, jest funkcja konwertująca dowolną leniwą listę na zwykłą listę. Nie radzę jej odpalać na listach nieskończonych. ;-)

(define (lazy->list L)
(if (null? L)
L
(cons (lazy-car L) (lazy->list (lazy-cdr L)))))

Gotowe, możemy już swobodnie korzystać z leniwych list. Przykładowo, by uzyskać pierwsze dziesięć parzystych liczb, możemy napisać:

> (lazy->list (lazy-filter even? natural-numbers 10))
(2 4 6 8 10 12 14 16 18 20)

Wyselekcjonowanie 5 liczb nieparzystych, a następnie przemnożenie każdej z nich przez dwa wyglądało by następująco:

> (lazy->list (lazy-map
(lambda (x) (* x 2))
(lazy-filter odd? natural-numbers 5)))
(2 6 10 14 18)

Implementacja search-for-primes jest teraz bardzo łatwa do napisania:

;; pozbycie sie 'values' na rzecz zwyklego 'cons'
(define (timed-function-test fun)
(let ((start-time (runtime))
(result (fun))
(end-time (runtime)))
(cons result (- end-time start-time))))

(define (timed-prime-test n)
(timed-function-test (lambda () (cons (prime? n) n))))

(define (search-for-primes how-many start)
(define odd-numbers
(lazy-filter odd? (infinity-starting-at start)))
(define (decide test-result)
(caar test-result))
(define (get-rid-of-primality test-result)
(cons (cdar test-result) (cdr test-result)))
(lazy->list (lazy-map get-rid-of-primality
(lazy-filter decide
(lazy-map timed-prime-test
odd-numbers)
how-many))))

Tak, to jest już bardziej w stylu Scheme. Jak dla mnie jednak wciąż zbyt rozwlekle. Klucz do uproszczenia tego kodu już wiemy jak nazwać: abstrakcja.

4.

Jednym ze słabych punktów poprzedniej implementacji jest niejasność operacji wykonywanych na strukturze test-result. Postanowiłem więc obudować dostęp do każdej ze składowych struktury odpowiednią funkcją. Argumenty funkcji są teraz również przechowywane wśród wyników timed-function-test.

(define (make-timed-result fun-args fun-result elapsed-time)
(cons fun-args (cons fun-result elapsed-time)))

(define (get-fun-args timed-result)
(car timed-result))

(define (get-fun-result timed-result)
(cadr timed-result))

(define (get-elapsed-time timed-result)
(cddr timed-result))

(define (timed-function-test fun args)
(let ((start-time (runtime))
(result (apply fun args))
(end-time (runtime)))
(make-timed-result args result (- end-time start-time))))

Kolejną rzeczą, która prosiła się o ulepszenie był kod wyświetlający. Tutaj posunąłem się trochę dalej, dając użytkownikowi możliwość ustalenia formatu wyświetlanych danych. Dla zadanej listy wyników timed-results kolejne linie wyświetlane są według formatu output-string. W formacie można korzystać z trzech specjalnych ciągów: %a zostanie zastąpiony listą argumentów dla badanej funkcji, %r jej wynikiem, a %t czasem jej wykonania. Implementacja poniżej.

(define (cut-precision inexact digits)
(define factor (expt 10 digits))
(/ (round (* inexact factor)) factor))

(define (replace-all dictionary string)
(if (null? dictionary)
string
(letrec ((rule (car dictionary))
(from (car rule))
(to (cdr rule)))
(replace-all (cdr dictionary)
(regexp-replace* from string to)))))

(define (report-timed-result output-string timed-result)
(let ((fun-args (format "~a" (get-fun-args timed-result)))
(fun-result (format "~a" (get-fun-result timed-result)))
(elapsed-time (number->string
(cut-precision (get-elapsed-time timed-result)
4))))
(begin
(display (replace-all (list (cons "%a" fun-args)
(cons "%r" fun-result)
(cons "%t" elapsed-time))
output-string))
(newline))))

(define (display-timed-results output-string timed-results)
(if (not (null? timed-results))
(let ((first-result (car timed-results)))
(begin
(report-timed-result output-string first-result)
(display-timed-results output-string (cdr timed-results))))))

Jako, że liczenie czasu wykonania funkcji szukającej liczby pierwsze jest szczególnym przypadkiem ogólniejszego problemu, najpierw zdefiniowałem ogólną funkcję generate-statistics:

(define-opt (generate-statistics function feed decide-fun how-many
[output-string "%a->%r: %t"])
(define dtr display-timed-results)
(define tft timed-function-test)
(dtr output-string
(lazy->list (lazy-filter decide-fun
(lazy-map (lambda args
(tft function args))
feed)
how-many))))

Definicja search-for-primes wygląda wówczas najprościej jak to tylko możliwe:

(define (search-for-primes how-many start)
(define odd-numbers
(lazy-filter odd? (infinity-starting-at start)))
(define (decide timed-result)
(get-fun-result timed-result))
(generate-statistics prime? odd-numbers decide how-many "%a: %t"))

Uff, dojście do tej postaci trochę wysiłku mnie kosztowało, ale i rezultat jest nareszcie zadowalający. Dość na dziś, do zobaczenia następnym razem. :-)

pliki

Dla wnikliwych, poniżej zamieszczam linki do pełnych kodów źródłowych wszystkich przedstawionych w tym wpisie przykładów:

One day hack

W nawiązaniu do pewnego wpisu z Lispowego bloga wrzucam tutaj efekt moich dzisiejszych rozważań nad skutecznością zabezpieczania tekstu przy pomocy CSS’owej czcionki.

DeCSS

Maszyny są coraz szybsze i skuteczniejsze w wykonywaniu powierzonych im zadań. Prawdziwie myślącej maszyny możemy się nigdy nie doczekać, nie jednak z powodu ograniczonej mocy obliczeniowej. Komputery stają się coraz doskonalsze w wykonywaniu wyraźnie określonych zadań i to jest obszar ich najintensywniejszego rozwoju. Zdejmują one z ludzi potrzebę wykonywania “bezmyślnych” czynności. W miarę jednak jak systemy stają się coraz bardziej skompilowane, przymiotnik “bezmyślny” zdaje się być nie na miejscu. Do tej pory ludzie nie potrzebowali i zapewne nie będą nigdy potrzebować maszyny “do wszystkiego”. Wygodne narzędzia do konkretnych celów - tego właśnie od maszyn, jako ludzie, oczekujemy.

Mój skrypt jest naiwny, ale jest tylko dowodem na to, że nie można ukryć czegoś, co i tak jest odkryte. Skoro człowiek może odczytać napis, może to zrobić również komputer. Co więcej, zrobi to tysiące razy szybciej i z niższym procentem błędów. Stopniowo, w miarę przyrostu kolejnych warstw na styku człowieka a maszyny, coraz trudniej będzie odróżnić zmyślnego bota od żywego człowieka. Prawdę mówiąc, już w tej chwili pewna część informacji, z jakich korzystamy na co dzień jest generowana wyłącznie przez maszyny. Zmyślne serwisy opowiedzą nam o ostatnich wydarzeniach na świecie, doradzą jaka muzyka może nam się spodobać, nawet napiszą dla nas utwór muzyczny… Przez to, że technologia tak brutalnie wdziera się w nasze codzienne życie, można zrozumieć jak wiele rzeczy, które robimy, polega w dużej mierze na przestawianiu symboli. Nawet ja, pisząc ten tekst, zdaję się tylko układać słowa w odpowiednim porządku. Ktoś po drugiej stronie ekranu nie jest już w stanie stwierdzić na pewno, że to, co czyta, zostało napisane przez człowieka. I granicą nie jest naprawdę moment, w którym maszyny nauczą się mówić tak, że nie poznamy się na ich braku człowieczeństwa. Granicą jest moment, w którym przełożymy treści przekazywane nam przez maszyny nad dzieła rąk ludzkich. W miarę jak mijają lata, coraz bardziej mam wrażenie, że ta granica została już przekroczona…

Kod źródłowy skryptu, którego działanie widać na powyższym zrzucie znaleźć można tutaj.

DRY

Termin DRY (Don’t repeat yourself) określa filozofię programowania, która jest przeciwnością popularnej poniekąd metody kopiuj i wklej. Stosując ją należy wyłapywać wszystkie schematy, jakie pojawiają się w pisanym przez nas kodzie i uogólniać je. Najpopularniejszym na to sposobem jest używanie funkcji i klas. Lisp posiada dodatkowo możliwość definiowania makr, które pozwalają uogólniać kod na poziomie składni (w przeciwności do poziomu wykonywania programu, gdzie działają funkcje i klasy). O tym, jak bardzo język Scheme sprzyja stosowaniu DRY przekonałem się podczas testowania emacsowego modułu msf-abbrev. Jego działanie polega na dopełnianiu określonych przez nas nazw przez inne, zazwyczaj dłuższe wyrażenia. Dzięki przystępnemu interface’owi pisanie kodu momentami staje się tak proste jak wypełnianie formularzy. Korzystając z przykładowego dopełnienia zdefiniowanego przez autora modułu, po wpisaniu fopenx i wciśnięciu spacji, zostanie wklejony w aktualne miejsce kod podobny do następującego:

if ((f = fopen("nazwa_pliku", "r")) == NULL)
{
std::cerr << "ERROR: opening file"
<< " at " << __FILE__ << ":" << __LINE__
<< std::endl << std::flush;
exit(1);
}

Pola, którymi różnią się wywołania fopen można wypełniać jak pola formularza, przechodząc od jednego do drugiego przy pomocy klawisza Tab. Jeżeli chcecie zobaczyć moduł w akcji, na stronie projektu dostępne jest demo.

Chociaż msf-abbrev często się przydaje, stwarza poważne zagrożenie dla zasady DRY. Podany wyżej przykład pisze się bardzo prosto, jego usunięcie lub modyfikacja jest już jednak bardzo kłopotliwa. Wyobraźcie sobie bowiem, że musicie zmienić w komunikacie błędu “ERROR” na “Error”, lub też kod wyjścia z jedynki na dwójkę. Z pomocą przyjść może edytor z dobrą obsługą operacji zastępowania, jednak samo nawet określenie, w których plikach (możliwe, że rozsianych po różnych projektach) użyliśmy tych szablonów, może być bardzo czasochłonne. Tymczasem rozwiązanie dla podanego przykładu można napisać w samym C++, trzymając w ten sposób definicję w jednym centralnym punkcie.

#define fopenx(filename, mode) \
_fopenx(filename, mode, __FILE__, __LINE__)

FILE *_fopenx(char *filename, char *mode, char *file, int line) {
FILE *f = NULL;

if ((f = fopen(filename, mode)) == NULL) {
std::cerr << "ERROR: opening file"
<< " at " << file << ":" << line
<< std::endl << std::flush;
exit(1);
}

return f;
}

Używanie funkcji fopenx() jest bardzo proste:

f = fopenx("nazwa_pliku", "r");

W ten sposób nie ma potrzeby sięgać po msf-abbrev, bo znaków do wpisania nie jest wcale aż tak wiele.

Możliwości abstrakcji w Scheme są naprawdę potężne. Dzięki temu, że funkcje są pełnoprawnymi obiektami i można nimi dowolnie manipulować, z przekazywaniem i zwracaniem włącznie, mogą służyć one nie tylko do opisu konkretnych działań (jak np. obliczenie iloczynu dwóch argumentów), ale i do ogólnych sposobów rozwiązywania szerokich klas problemów (np. metoda Newtona znajdowania pierwiastków funkcji). Dzięki czystej składni w miarę przyrostu złożoności kod nie staje się brzydki. Makra są zaś furtką do czynienia rzeczy najprostszymi jak to tylko możliwe. Przykład? Proszę bardzo. Pisząc w Scheme często korzysta się z funkcji rekurencyjnych, by jednak wywołać siebie, muszą być one nazwane. Wymaga to użycia form specjalnych define lub let, co wymusza określoną kolejność wyrażeń (najpierw definicje, potem użycie). Czasami jednak chcielibyśmy mieć możliwość zdefiniowania anonimowej funkcji, która jednocześnie wywołuje samą siebie. Inaczej - chcielibyśmy połączyć siłę form lambda i define. W Scheme istnieje już forma specjalna, która tworzy nową zagnieżdżoną przestrzeń nazw, pozwalając na umieszczenie w niej nowych definicji. Jest to let. Jej składnia jest, jak każdej formy specjalnej, bardzo prosta:

(let (( )
( )
…)
)

Każdej podanej nazwie zostanie przyporządkowana odpowiadająca jej wartość, zaś całe wyrażenie let zwróci wartość wyrażenia podanego jako drugi argument. Ze zdefiniowanych nazw można korzystać tylko w ostatnim wyrażeniu. My definiujemy funkcję rekurencyjną, musimy więc mieć możliwość skorzystania z nazw jeszcze w samych wyrażeniach. Pozwalająca na to odmiana let nazywa się letrec.

Rozważmy prosty przykład silni. Dotychczas, by obliczyć silnię z dowolnej liczby musieliśmy zdefiniować obliczającą ją procedurę, a dopiero po tym z niej skorzystać.

(define (silnia x)
(if (= x 0)
1
(* x (silnia (- x 1)))))

(silnia 5)

Wykorzystując letrec możemy powyższy efekt uzyskać pisząc:

((letrec ((silnia
(lambda (x)
(if (= x 0)
1
(* x (silnia (- x 1)))))))
silnia) 5)

Ano, pięknie, jedyny mankament to wygląd. Definicja nie wygląda zbyt dobrze - słowa silnia używamy dwa razy (w definicji i następnie jako wartość wyrażenia letrec), gdy wystarczyło by raz, zaś w miejsce lambda i letrec można by wstawić pojedynczą nazwę. Spróbujmy więc to poprawić. Niestety nie da się wyabstrahować powyższego kodu do funkcji, głównie przez to, że Scheme korzysta z zasięgu leksykalnego (ang. lexical scope). Najkorzystniej byłoby, gdybyśmy mogli określone wyrażenie przekształcić w inne, jeszcze zanim nastąpi jego wykonanie. Do tego właśnie służą makra. Określmy najpierw, jak chcielibyśmy, by nasza nowa lambda wyglądała. Ja skłaniałbym się ku składni przypominającej define, tzn:

(def-lambda ( )
)

Chociaż wygląda identycznie, różni się od define tym, że zamiast związywać funkcję z podaną nazwą w aktualnej przestrzeni nazw, zwraca tę funkcję jako wartość. Nasz przykład z silnią uprościłby się więc do następującej postaci:

((def-lambda (silnia x)
(if (= x 0)
1
(* x (silnia (- x 1))))) 5)

Tak, to wygląda o wiele ładniej. Nie wgłębiając się zbytnio w składnię, definicję makra def-lambda jest następująca:

(define-syntax def-lambda
(syntax-rules ()
((_ (self . args) value)
(letrec ((self (lambda args value)))
self))))

Wyjaśnienie działania makr to temat na zupełnie inny wpis (a w zasadzie na kilka wpisów), chciałbym jednak zwrócić uwagę na sam efekt ich użycia. Dzięki wbudowanym w język możliwościom udało nam się uogólnić powtarzający się kod do zwartej i zrozumiałej postaci, która ma dodatkowo tę piękną właściwość, że niczym nie różni się od innych form specjalnych. W pewnym sensie zmieniliśmy więc sam język w taki sposób, by bardziej odpowiadał naszym potrzebom. Piękno tego rozwiązania bije na głowę to, co oferuje msf-abbrev. Dlatego, jeżeli tylko macie okazję, zamiast zmieniać edytor, proponuję zmienić język programowania.

Matematycznie

Dzisiaj same konkrety - rozwiązania dla ćwiczeń kończących rozdział 1.2.2.

Ćwiczenie 1.11

Ćwiczenie proste, biorąc pod uwagę to, co opisywałem wcześniej. Należy funkcję f(n) zapisać jako procedurę obliczającą wartości za pomocą procesu iteracyjnego i rekurencyjnego. Definicja funkcji jest następująca:

f(n)=n dla n<3 i f(n) = f(n-1)+2f(n-2)+3f(n-3) dla n>=3

Wersja rekurencyjna:

(define (f n)
(cond ((< n 3) n)
(else (+ (f (- n 1))
(* 2 (f (- n 2)))
(* 3 (f (- n 3)))))))

Wersja iteracyjna:

(define (f n)
(define (f-iter a b c count)
(cond ((= count 2) c)
(else (f-iter b c (+ c (* 2 b) (* 3 a)) (- count 1)))))
(cond ((< n 3) n)
(else (f-iter 0 1 2 n))))

Ćwiczenie 1.12

Zadanie polega na napisaniu procedury obliczającej elementy trójkąta Pascala. Nie sprecyzowano czy funkcja ma zwracać pojedyncze elementy, czy całe trójkąty, stworzyłem więc trzy funkcje: pascal-number oblicza wartość n-tej liczby w podanym wierszu trójkąta Pascala, pascal-row oblicza cały n-ty wiersz (zwracając listę liczb), zaś pascal-triangle oblicza cały trójkąt o zadanej wysokości (zwracając listę wierszy).

(define (pascal-number r n)
(if (or (= n 1)
(= n r))
1
(+ (pascal-number (- r 1) (- n 1))
(pascal-number (- r 1) n))))

(define (make-list-by-iterate iters-num function)
(define (iter i)
(if (> i iters-num)
()
(cons (function i)
(iter (+ i 1)))))
(iter 1))

(define (pascal-row n)
(make-list-by-iterate n
(lambda (x) (pascal-number n x))))

(define (pascal-triangle n)
(make-list-by-iterate n
(lambda (x) (pascal-row x))))

Warto zwrócić uwagę na funkcję pomocniczą make-list-by-iterate, która buduje listę z wartości zwracanych podanej funkcji wywoływanej kolejno z argumentem od 1 do iters-num. Budowanie n-tego wiersza (pascal-row) jest niczym więcej jak wywołaniem pascal-number n razy. Budowanie trójkąta Pascala o wysokości n (pascal-triangle) polega zaś na stworzeniu kolejnych jego wierszy, od pierwszego aż do n-tego.

Ćwiczenie 1.13

Obecność tego ćwiczenia wybitnie świadczy o tym, że Wizard Book jest książką pomocną w nauczaniu Informatyki, a nie zwykłym podręcznikiem do programowania. Autorzy proszą nas bowiem o przeprowadzenie matematycznego dowodu.

Udowodnij, że Fib(n) jest liczbą całkowitą najbliższą (phi^n)/sqrt(5), gdzie phi = (1 + sqrt(5))/2.
Wskazówka: Niech psi = (1 - sqrt(5))/2. Korzystając z definicji liczb Fibonacciego, udowodnij przez indukcję, że Fib(n) = (phi^n - psi^n)/sqrt(5).

Dzięki wskazówce rozwiązanie jest dość proste. Zanim przejdziemy do dowodu, przypomnijmy sobie definicję ciągu Fibonacciego i własności liczby phi i psi.

Fib(n) = Fib(n-1) + Fib(n-2)
phi^2 = phi + 1
psi^2 = psi + 1

Możemy już przejść do pierwszego kroku indukcyjnego. Sprawdzamy, czy teza zachodzi dla n równego zero i jeden.

Fib(0)

Fib(1)

Wyniki są poprawne, więc przechodzimy do kroku drugiego. Założenie (dla n>0):

Fib(n-1)=(phi^(n-1) - psi^(n-1))/sqrt(5)
Fib(n)=(phi^n - psi^n)/sqrt(5)

Teza:

Fib(n)=(phi^(n+1) - psi^(n+1))/sqrt(5)

Dowód:

Fib(n+1)=Fib(n)+Fib(n-1)
(phi^n - psi^n)/sqrt(5) + (phi^(n-1) - psi^(n-1))/sqrt(5)
(phi^n + phi^(n-1) - psi^n - psi^(n-1))/sqrt(5)
((phi^(n-1))(phi+1) - (psi^(n-1))(psi+1))/sqrt(5)
(phi^(n+1) - psi^(n+1))/sqrt(5)

Skoro wiemy już ile równe jest Fib(n), to by udowodnić, że jest ona najbliższą liczbą całkowitą dla (phi^n)/sqrt(5), należy dowieść nierówności:

|Fib(n)-(phi^n/sqrt(5))| <= 1/2

Ta oczywiście upraszcza się do postaci:

|(phi^n - psi^n)/sqrt(5) - (phi^n/sqrt(5))| <= 1/2
|(- psi^n)/sqrt(5)| <= 1/2

Lewa część wyrażenia dąży do zera zaczynając od 0.447 dla n równego zero, nierówność jest więc zawsze spełniona. Formalnie należałoby jeszcze zapisać na to dowód, jest on jednak trywialny, a mi już znudziło się wklepywanie tych wzorów. ;-) W ten oto sposób rozwiązaliśmy ćwiczenie 1.13.

Escape Meta Alt Control Shift

Ten wpis miał być o DrScheme, środowisku do tworzenia i testowania programów napisanych w języku Scheme. Miałem go skończyć już kilka dni temu. Tymczasem od ostatniej notki mija już tydzień, a mi wciąż ciężko zebrać się do pisania. Powód?

Emacs.

Pozwolę sobie zacytować najpierw pewnego początkującego emacsowca, którego wypowiedź najłatwiej opisze moją obecną sytuację:

“Jak ze wszystkimi rzeczami w Uniksie również ten to przypadek dalekiej podróży zaczynającej się jednym niepewnym krokiem.”

“I suppose, as with all things UNIX, its a case of the journey of a thousand miles beginning with one step.”

Do DrScheme prawdopodobnie jeszcze wrócę, posiada on bowiem ciekawe narzędzie do debugowania kodu. Wiele skrótów klawiszowych jest w DrScheme taka sama jak w Emacsie, więc ewentualne przejście będzie mało bolesne (po tym co do tej pory widziałem wątpię jednak, by ktoś chciał rezygnować z Emacsa na rzecz DrScheme).

Emacs - edytor tekstu

Opiszę więc teraz pokrótce wszystko to, co udało mi się w Emacsie okryć przez te kilka dni. Rady będą zorientowane na systemy uniksowe, ale jestem pewny, że użytkownicy Windowsów wykażą się charakterystycznym dla siebie samozaparciem i bez problemu sobie poradzą. ;-) Emacsa ściągnąć można ze strony GNU. Osobiście polecam najnowsze wersje z cvs’a. W Debianie rozbite są one na paczki emacs-snapshot, emacs-snapshot-bin-common, emacs-snapshot-common, i emacs-snapshot-el. Po wpisaniu w terminalu emacs edytor włącza się w okienku. Ja zdecydowanie wolę pracę na konsoli, więc ustawiłem sobie w moim .bashrc alias:

alias emacs='emacs-snapshot -nw'

No dobrze, wiemy jak włączyć, ale jak tego cuda używać? Przede wszystkim, zanim nawet zaczniecie, proponuję każdemu podpiąć pod Caps Lock klawisz Ctrl. Mając podstawowy klawisz pod małym lewym palcem naprawdę ułatwia edycję. Najlepiej od razu przeczytajcie sobie ten tutorial, gdzie oprócz tej rady podanych jest wiele innych, których stosowanie przekłada się na wymierne polepszenie komfortu pracy z Emacsem.

Podstawy edycji załapać możecie czytając poradnik Marcina Bielewicza. Ja opiszę Emacsa z punktu widzenia kilku dni używania ze zwróceniem uwagi na walory ułatwiające dłubanie w kodzie Scheme.

Jedną z podstawowych koncepcji Emacsa są bufory, które reprezentują pewne wirtualne przestrzenie do pracy. Przypomina to trochę ideę uniksowych terminali. Bufor może być związany z plikiem, gdy właśnie go edytujemy/przeglądamy. Ale w buforze możemy też trzymać otwartego shella, sesję ssh, interpreter pythona, czy cokolwiek innego. Nie było by w tym jednak nic ciekawego, gdyby nie fakt, że ze wszystkich buforów można korzystać przy pomocy tych samych poleceń edycyjnych. Nie ma więc żadnego problemu, by skopiować z powłoki kilka linijek, albo przeszukać okno interpretera za pomocą znajomej kombinacji C-s (notacja ta oznacza: trzymając Ctrl wciśnij s). Poprzednie/następne polecenie uzyskuje się przy pomocy kombinacji C- strzałka w górę/w dół. I tak dalej, możliwości są naprawdę niezliczone, wszystko dzięki ujednoliceniu zachowania buforów.

Pomiędzy buforami poruszać się można kolejno przy pomocy kombinacji C-strzałka w lewo/w prawo. Na raz widocznych może być kilka buforów. Okno dzieli się przy pomocy kombinacji C-x 2 (wybierz Ctrl+x, a potem 2). By zmaksymalizować aktywny bufor, należy wpisać C-x 1, by go ukryć: C-x 0. Bufor zamyka się kombinacją C-x k. Zazwyczaj przed zamknięciem bufora związanego z plikiem chcesz wszystkie swoje zmiany zachować. Służy do tego kombinacja C-x C-s.

Komenda, która wciąż ratuje mi życie, to C-g, aka keyboard-quit. Powoduje ona zaprzestanie czynności, którą właśnie wykonuje edytor. Nadal często zdarza mi się wklepać przypadkiem dowolną kombinację klawiszy, po której nie za bardzo wiem gdzie jestem. C-g zawsze wraca do stanu pierwotnego.

I w zasadzie tyle wystarczy do w miarę sprawnego korzystania z edytora. Swoje umiejętności można udoskonalić czytając wbudowany tutorial dostępny pod kombinacją klawiszy C-h t. Odważni mogą też zacząć czytać dokumentację.

Emacs - środowisko programistyczne

To, że bufory zachowują się podobnie, to tylko jedna strona medalu. Właściwości bufora determinuje również tryb (ang. mode) w jakim się on znajduje. Domyślny bufor *scratch* rozpoczyna pracę w trybie Lisp Interaction. Jest to tryb, w którym wpisane w Lispie polecenia możemy przesłać Emacsowi do wykonania. Spróbujcie na przykład wpisać:

(menu-bar-mode -1)

Mając kursor na końcu linii wybierzcie kombinację klawiszy C-x C-e. Wykona ona podane polecenie, powodując zniknięcie menu. By je przywrócić wystarczy zamienić argument funkcji na 1 i wykonać kod ponownie. To tylko prosty przykład; warto być świadomym, że do dyspozycji mamy kompletny interpreter Lispa (a dokładnie dialektu nazywanego Emacs Lispem).

Emacs posiada tryb dla Scheme (nazywa się on po prostu scheme-mode), istnieje jednak ciekawa biblioteka rozszerzająca jego możliwości. Jest to Quack, dostępny w Debianie w pakiecie quack-el. Po instalacji wystarczy dodać jedną linijkę do pliku ~/.emacs i jesteśmy gotowi do pracy:

(require 'quack)

Otwieramy Emacsa, naszym oczom ukazuje się standardowy ekran z przywitaniem. Wciskamy C-x b i wpisujemy test, tworząc w ten sposób nowy bufor o nazwie test. Jako, że chcemy pisać program w języku Scheme, uruchomimy w buforze odpowiedni tryb: M-x scheme-mode. Quack automatycznie podłącza się pod ten tryb, dopisana do ~/.emacs pojedyncza linijka zajęła się wszystkimi ustawieniami. Możemy zacząć pisać. Warto zwrócić uwagę na kolorowanie składni, automatyczne wstawianie wcięć i skaczący kursor dopasowujący nawiasy. Po napisaniu kodu zapewne chcielibyśmy go przetestować. Interpreter Scheme uruchamiamy kombinacją C-c C-q r (lub też M-x run-scheme). By upewnić się, że interpreter działa poprawnie, możemy wpisać dowolne poprawne wyrażenie:

> (+ (* 2 3) 4)
10

No dobrze, ale nie chcemy przecież przepisywać w interpreterze całego kodu, który właśnie napisaliśmy w oknie edycyjnym. Nie ma problemu, Emacs świetnie radzi sobie z kopiowaniem tekstu pomiędzy oknami. Przechodzimy do okna edycji kombinacją C-x o. Zaznaczamy cały bufor kombinacją C-x h, a kopiujemy go do interpetera przy pomocy C-c C-r. Gotowe. Załóżmy, że w oknie edycyjnym mieliśmy rekurencyjną definicję silni.

(define (silnia x)
(if (= x 0)
1
(* x (silnia (- x 1)))))

Przechodząc do okna intepretera możemy już z właśnie wykonanego kodu skorzystać. Piszemy:

(silnia 5)

I otrzymujemy poprawny wynik:

120

Oczywiście to nie koniec atrakcji. Co powiecie na szybki dostęp do pełnej dokumentacji języka Scheme? Po wybraniu M-x quack-view-manual i wciśnięciu Tab przedstawione wam zostaną nazwy wszystkim manuali, jakie są dostępne online. Wystarczy wpisać nazwę dowolnego z nich i potwierdzić, by Emacs otworzył przeglądarkę na odpowiedniej stronie. U mnie włącza się Mozilla, co nie jest najszczęśliwszym rozwiązaniem. Ale Emacs nie byłby nazywany złośliwie systemem operacyjnym, gdyby nie pozwalał na uruchomienie w buforze przeglądarki. Ja używam w3m, której emacsowy pakiet w Debianie to w3m-el. By zmienić używaną przez Quack przeglądarkę trzeba przejść do bufora *scratch* i wpisać następującą linijkę:

(setq quack-browse-url-browser-function 'w3m-browse-url)

Wykonujemy ją zwyczajowo przy pomocy C-x C-e. Jeżeli ponownie otworzymy dowolny manual, zobaczymy go już bezpośrednio w Emacsie. No dobrze, ale możemy czytać dokumentację i bez Emacsa. Quack oferuje nam coś więcej. Jeżeli chcecie uzyskać dokumentację dla dowolnej funkcji, wystarczy, że wpiszecie jej nazwę i wykonacie kombinację C-c C-q k. Jest jednak błąd, nie wiem nawet czy w module w3m, czy w quacku (skłaniałbym się ku pierwszemu, bo z innymi przeglądarkami nie ma problemów), który powoduje, że przeglądarka nie skacze do opisu funkcji. Po dłuższych bojach udało mi się rozwiązać problem. Dodajcie poniższe linijki do swojego pliku ~/.emacs, a w3m będzie już działać poprawnie:

(defun substitute-hex-values (url)
(if (string-match "%25" url)
(replace-match "%" nil nil url)
url))

(defun my-w3m-browse-url (url &optional new-session)
(when (stringp url)
(w3m-browse-url (substitute-hex-values url) new-session)))

(setq quack-browse-url-browser-function 'my-w3m-browse-url)

Jak widzicie, możliwości Emacsa są całkiem spore. Pozwolę sobie wspomnieć o jeszcze jednej często używanej kombinacji klawiszy, jaką jest M-/. Dopełnia ona właśnie pisane słowo na podstawie innych słów w tym buforze (a także w innych buforach). Przykładowo, jeżeli zdefiniowaliście wcześniej funkcję funkcja-o-długiej-nazwie, to wystarczy, wpisać kilka pierwszych liter i użyć kombinacji M-/, by Emacs oszczędził nam dalszego pisania.

Jeżeli czujecie niedosyt, zapraszam do eksperymentowania z Emacsem. Możecie też przejrzeć tutoriale o Scheme w Emacsie: tutaj i tutaj.

.emacs

Każde polecenie, które wykonywaliśmy w buforze *scratch* możemy zapisać do pliku .emacs, dzięki czemu zostanie ono wykonane zaraz po uruchomieniu edytora. Opiszę poniżej kilka fragmentów mojego pliku .emacs, które mogą wam się przydać.


(column-number-mode 1)

Powoduje, że w pasku statusu widoczny jest nie tylko numer linii, ale i numer kolumny.


(setq inhibit-startup-message t)

Dzięki temu po włączeniu Emacsa nie pojawia się ekran z przywitaniem.


(setq make-backup-files nil)

Wyłącza tworzenie kopii zapasowych edytowanych plików. Bez tego w krótkim czasie katalogi zapełniają się masą plików z tyldą na końcu.


(setq truncate-lines t)

(defun point-of-beginning-of-bottom-line ()
(save-excursion
(move-to-window-line -1)
(point)))

(defun point-of-beginning-of-line ()
(save-excursion
(beginning-of-line)
(point)))

(defun next-one-line () (interactive)
(if (= (point-of-beginning-of-bottom-line)
(point-of-beginning-of-line))
(progn (scroll-up 1)
(next-line 1))
(next-line 1)))

(defun point-of-beginning-of-top-line ()
(save-excursion
(move-to-window-line 0)
(point)))

(defun previous-one-line () (interactive)
(if (= (point-of-beginning-of-top-line)
(point-of-beginning-of-line))
(progn (scroll-down 1)
(previous-line 1))
(previous-line 1)))

(global-set-key (kbd "“) ‘next-one-line)
(global-set-key (kbd ““) ‘previous-one-line)

(defun scroll-down-by-1 () (interactive)
(scroll-down 1))
(defun scroll-up-by-1 () (interactive)
(scroll-up 1))
(global-set-key “\C-n” ’scroll-up-by-1)
(global-set-key “\C-p” ’scroll-down-by-1)

Kod pożyczony stąd. Powoduje on płynne przewijanie ekranu przy korzystaniu ze strzałek. Ja dodałem obsługę klawiszy C-n i C-p, które powodują przewinięcie ekranu w górę/w dół bez zmieniania pozycji kursora.


(setq quack-run-scheme-always-prompts-p nil)

Wpisanie w scheme-mode kombinacji C-c C-q r powoduje natychmiastowe uruchomienie interpretera Scheme bez pytania o jego nazwę.


(setq quack-pretty-lambda-p 1)

Jeżeli posiadacie odpowiedni terminal z odpowiednią czcionką zamiast słowa lambda widoczna będzie grecka litera.

Jeżeli znacie jakąś funkcję Emacsa, która przydaje się przy edycji kodu, zachęcam do opisania jej w komentarzach.

Walcząc z czasem

W ćwiczeniu 1.10 mamy okazję zapoznać się z funkcją Ackermanna, a raczej jej odmianą przygotowaną przez autorów Wizard Booka. Definicja prawdziwej funkcji Ackermanna jest trochę inna, obie wersje mają jednak tę właściwość, że rosną bardzo szybko.

(define (A x y)
(cond ((= y 0) 0)
((= x 0) (* 2 y))
((= y 1) 2)
(else (A (- x 1)
(A x (- y 1))))))

Pierwsze polecenie każe obliczyć wartość trzech wyrażeń: (A 1 10), (A 2 4) i (A 3 3). Rozwinięcia są proste, ale dość żmudne przy obliczeniach.

  • (A 1 10)
    (A 0 (A 1 9))
    (A 0 (A 0 (A 1 8)))
    (A 0 (A 0 (A 0 (A 1 7))))
    ; ...
    (A 0 (A 0 (A 0 (A 0 (A 0 (A 0 (A 0 (A 0 (A 0 (A 1 1))))))))))
    (A 0 (A 0 (A 0 (A 0 (A 0 (A 0 (A 0 (A 0 (A 0 2)))))))))
    (A 0 (A 0 (A 0 (A 0 (A 0 (A 0 (A 0 (A 0 4))))))))
    ; ...
    (A 0 512)
    1024
    
  • (A 2 4)
    (A 1 (A 2 3))
    (A 1 (A 1 (A 2 2)))
    (A 1 (A 1 (A 1 (A 2 1))))
    (A 1 (A 1 (A 1 2)))
    (A 1 (A 1 (A 0 (A 1 1))))
    (A 1 (A 1 (A 0 2)))
    (A 1 (A 1 4))
    (A 1 (A 0 (A 1 3)))
    (A 1 (A 0 (A 0 (A 1 2))))
    (A 1 (A 0 (A 0 (A 0 (A 1 1)))))
    (A 1 (A 0 (A 0 (A 0 2))))
    (A 1 (A 0 (A 0 4)))
    (A 1 (A 0 8))
    (A 1 16)
    (A 0 (A 1 15))
    (A 0 (A 0 (A 1 14)))
    ; ...
    (A 0 32768)
    65536
    
  • (A 3 3)
    (A 2 (A 3 2))
    (A 2 (A 2 (A 3 1)))
    (A 2 (A 2 2))
    (A 2 (A 1 (A 2 1)))
    (A 2 (A 1 2))
    (A 2 (A 0 (A 1 1)))
    (A 2 (A 0 2))
    (A 2 4)
    ; to juz obliczylismy w poprzednim przykladzie, wiec...
    65536
    

Druga część zadania polega na podaniu zwartej definicji matematycznej funkcji (A 0 n), (A 1 n) i (A 2 n). Definicja funkcji Ackermanna mówi o tym, że (A x y) to (A (- x 1) ... przyłożone y-razy do 2 (pomijając przypadek, gdy y jest równe 0). (A 0 n) jest równe 2n z definicji (drugi predykat wyrażenia cond). (A 1 n) to 2n przyłożone (n-1)-razy do 2 lub 0, gdy n jest zerem. Definicja (A 1 n) wygląda więc następująco:

A(1,n) = 2^n gdy n>0 lub 0 gdy n=0

W podobny sposób dochodzimy do definicji (A 2 n):

A(2,n) = 2^(A(2,n-1)) gdy n>1 lub 2 gdy n=1 lub 0 gdy n=0

Na tym kończy się zadanie, ja jednak zacząłem się zastanawiać nad możliwością przyspieszenia obliczeń. Obliczenie (A 2 5) zajmuje u mnie około 4.5 sekundy, co pomimo wielkości wyniku (19729 cyfr :-), jest wynikiem całkiem słabym. Pierwszą myślą było oczywiście przepisanie definicji rekurencyjnej na iteracyjną. Najpierw stworzyłem pomocniczą funkcję iter-fun, która symuluje rekurencyjne złożenie (f (f (f ... (f start)))))):

(define (iter-fun f num start)
(if (= num 0)
start
(iter-fun f (- num 1) (f start))))

Wykorzystując ją przepisałem funkcję Ackermanna do następującej postaci:

(define (A x y)
(cond ((= y 0) 0)
((= x 0) (* 2 y))
(else (iter-fun (lambda (i) (A (- x 1) i))
(- y 1)
2))))

Z nową definicją na czasie obliczenia (A 2 5) zyskałem tylko około pół sekundy. To wciąż dawało jednak wynik w okolicach 3.9 sekundy. Dla porównania, ten sam algorytm zapisany w Pythonie wykonuje się u mnie przez 1.2 sekundy.

def iter_fun(f, num, start):
for i in xrange(num):
start = f(start)
return start

def A(x, y):
if y == 0:
return 0
elif x == 0:
return 2*y
else:
return iter_fun(lambda i: A(x-1, i), y-1, 2)

Więc jednak z tym kodem nie jest tak źle. Można jednak trochę oszukać i wykorzystując matematyczne definicję funkcji A(0,n) i A(1,n) zapisać kod w taki sposób:

(define (A x y)
(define (A0 n)
(* 2 n))
(define (A1 n)
(if (= n 0)
0
(expt 2 n)))
(cond ((= y 0) 0)
((= x 0) (A0 y))
((= x 1) (A1 y))
(else (iter-fun (lambda (i) (A (- x 1) i))
(- y 1)
2))))

Ten kod generuje poprawną wartość wyrażenia (A 2 5) w niezmierzalnie małym czasie. Testując go dla dużych wartości (A 1 n) można przekonać się o tym, że MzScheme ma lepiej napisaną arytmetykę dużych liczb od Pythona (a co najmniej ich potęgowanie). To, co mnie jednak najbardziej zastanowiło, to różnica w wydajności pomiędzy wersjami 2.3 i 2.4 Pythona. Oto bezpośrednie wyniki uzyskane z interpreterów MzScheme 301, Pythona 2.3 i Pythona 2.4:

MzScheme 301

> (time (expt 2 100000000))
cpu time: 568 real time: 570 gc time: 420

Python 2.3

Python 2.3.5 (#2, Mar  6 2006, 10:12:24)
[GCC 4.0.3 20060304 (prerelease) (Debian 4.0.2-10)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import timeit
>>> a = timeit.Timer('2**100000000')
>>> a.timeit(1)
5.5458259582519531

Python 2.4

Python 2.4.2 (#2, Nov 20 2005, 17:04:48)
[GCC 4.0.3 20051111 (prerelease) (Debian 4.0.2-4)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import timeit
>>> a = timeit.Timer('2**100000000')
>>> a.timeit(1)
13.069931030273438

Pythonowy moduł timeit zwraca czas w sekundach, a funkcja MzScheme time w milisekundach. Różnica więc jest wyraźnie widoczna, dlaczego jednak nowsza wersja Pythona działa ponad dwa razy wolniej od wersji wcześniejszej?

Czasoprzestrzeń

Na wykładzie 1b pan Sussman wprowadza nas w tematykę złożoności obliczeniowej. Mimo tego, że jest to domena informatyki teoretycznej, robi to na tyle przystępnie, że nawet najbardziej oporni są w stanie coś z tego wynieść. To, na co chciałbym zwrócić uwagę, to sposób w jaki język Scheme ułatwia dosłowne pokazanie rozwoju procesu podczas wykonania. Przykład użyty na wykładzie znaleźć można w książce jako ćwiczenie 1.9. Pełna jego treść podana jest na końcu części 1.2.1, ja przytoczę tylko odpowiednie kody źródłowe. Zadanie polega na opisaniu procesu generowanego przez każdą z procedur. Pierwszy kod źródłowy wygląda następująco:

(define (+ a b)
(if (= a 0)
b
(+ (dec a) (inc b))))

Procedury inc i dec odpowiednio zwiększają i zmniejszają swój argument o jeden. Ta definicja dodawania mówi więc, że sumą a i b jest b jeżeli a jest równe zero, w przeciwnym razie jest to suma a-1 i b+1. Stary problem jest więc zastępowany przez nowy problem. Proces kolejnych redukcji problemu do problemów je zastępujących jest przedstawiony na obrazku poniżej. Wykonywana operacja to (+ 3 4).

Iteracja

Jak wyraźnie widać, ta technika upraszczania problemu, chociaż korzysta z definicji rekurenycjnej (funkcja + wywołuje samą siebie) generuje proces iteracyjny. Dzieje się tak dlatego, bo w momencie wywołania (+ (dec a) (inc b)) iterpreter wie, że może zastąpić, “nadpisać” oryginalne wywołanie tym nowym. A ma taką możliwość, bo wynik tego wywołania jest jednocześnie wynikiem oryginalnego wywołania. Dlatego (+ 3 4) znaczy to samo co (+ 2 5), co znaczy to samo co (+ 1 6), itd. aż do momentu, gdy problem uprości się do postaci, w której jesteśmy w stanie udzielić jednoznacznej odpowiedzi (w tym przypadku nastąpi to, gdy a dojdzie do zera). Inaczej jednak jest dla drugiego kodu.

(define (+ a b)
(if (= a 0)
b
(inc (+ (dec a) b))))

Tutaj definiujemy sumę a i b jako zwiększoną o jeden sumę a-1 i b. Kształt procesu generowanego przez tę procedurę jest zupełnie inny.

Rekurencja

W tym przypadku nie zastępujemy starego problemu nowym problemem, ale definiujemy dla niego zupełnie nowy podproblem, którego rozwiązanie pomoże nam w rozwiązaniu. Musimy więc ze zwróceniem wyniku poczekać, aż interpreter obliczy (+ (dec a) b). Dopiero, gdy ten podproblem zostanie rozwiązany, będziemy mogli zwiększyć zwrócony przez wywołaną podprocedurę wynik o jeden i ogłosić nasze rozwiązanie. Fakt, że musimy pamiętać o wykonaniu dodatkowych operacji, powoduje, że interpreter nie może zastąpić starego problemu nowym, jak to uprzednio się odbywało. Co więcej, dla każdego wywołania naszej sumy musi pamiętać, w jakie miejsce wrócić. I to jest przyczyną liniowej pamięciowej złożoności tego procesu. Liniowy proces rekurencyjny nie tylko generuje liczbę kroków liniowo zależną od danych wejściowych (stąd liniowa złożoność czasowa), ale i potrzebuje dodatkowej pamięci dla każdego wywołania rekurencyjnego.

Jeżeli więc chcemy, by nasze programy były jak najszybsze i jak najmniejsze powinniśmy, gdzie to jest możliwe, zastępować procesy rekurencyjne iteracyjnymi. Uważni czytelnicy zapewne pamiętają, jak dla zabawy definiowałem funkcję sum-of-squares obliczającą sumę kwadratów wszystkich swoich elementów. Dla przypomnienia, kod wyglądał następująco:

(define (sum-of-squares L)
(if (null? L)
0
(+ (square (car L)) (sum-of-squares (cdr L)))))

Jako, że języki, na których uczyłem się programować wpoiły mi iterację jako najnaturalniejszy z modeli obliczeń, nie trudno mi było wyobrazić sobie iteracyjny sposób obliczania tej funkcji. Przykładowo w Pythonie zapisałbym to tak:

def sum_of_squares(L):
sum = 0
for el in L:
sum += el**2
return sum

Czyli, ogólnie rzecz biorąc, w każdym momencie iteracji przechowuję dotychczas obliczoną sumę elementów w specjalnej zmiennej sum. W Scheme do dyspozycji mamy tylko definicje rekurencyjne, więc rozwiązanie jest odrobinę dłuższe:

(define (sum-of-squares L)
(define (sos L sum)
(if (null? L)
sum
(sos (cdr L) (+ sum (square (car L))))))
(sos L 0))

Zdefiniowałem pomocniczą funkcję sos, która jako drugi argument przechowuje sumę, którą należy dodać do ostatecznego wyniku. Iterację zaczynamy od sumy równej zero, stąd wartością wywołania (sum-of-squares L) jest (sos L 0). Proces jest iteracyjny, bo za każdym razem stare wywołanie funkcji sos jest zastępowane przez nowe. Nie ma dodatkowych obliczeń do wykonania, ani miejsca powrotu do zapamiętania. Nowa definicja procedury sum-of-squares generuje więc proces iteracyjny.

Jaka funkcja, każdy widzi

Scheme nie bez przyczyny jest nazywany językiem funkcyjnym. Funkcje są podstawowym budulcem każdego programu, a ich interpretacja i wykonanie nadają kształt procesom. W związku z tym funkcje posiadają w Scheme odpowiednie przywileje. Wspomniane są one zarówno w wykładzie 2a, jak i pod koniec pierwszego rozdziału Wizard Booka. Oto one:

  • Funkcje mogą być nazwane

    Wspomniałem już o dwóch sposobach nazywania procedur. Jeden z nich to oczywiście:

    (define ( )
    )

    Drugi sposób bazuje na formie specjalnej lambda zwracającej anonimową procedurę.

    (define
    (lambda () ))

  • Funkcje mogą być argumentami innych funkcji

    Z tej właściwości skorzystałem przy definiowaniu funkcji list-op. Przyjmowała ona jako jedyny swój argument operator taki jak + lub -.

  • Funkcje mogą być zwracane przez inne funkcje

    I tutaj ponownie muszę wspomnieć funkcję list-op. Zwracała ona bowiem funkcję implementującą pisemne działanie na ciągach cyfr. Ciekawe zastosowanie tej zasady poznać można w ćwiczeniu 1.4:

    (define (a-plus-abs-b a b)
    ((if (> b 0) + -) a b))

    Operatorem kombinacji ((if (> b 0) + -) a b) jest kombinacja (if (> b 0) + -). Stosując dobrze nam znany model obliczeń można z łatwością przewidzieć zachowanie funkcji a-plus-abs-b. Mianowicie operatorem stanie się + jeżeli b będzie większe od zera. W przeciwnym wypadku operatorem kombinacji będzie -. Funkcja jest więc odpowiednikiem matematycznego zapisu: a + |b| (gdzie pionowe kreski reprezentują wartość bezwzględną).

  • Funkcje mogą być elementami struktur danych

    Z tego jeszcze nie korzystałem, ale z pewnością w drugim rozdziale znajdzie się wiele okazji do zastosowania tej właściwości.

Już na samym początku autorzy Wizard Booka przedstawiają dwa modele obliczeń wartości funkcji: normalną i stosowaną kolejność obliczania. Wykorzystując kolejność normalną najpierw rozwijamy całe wyrażenie do momentu aż będzie zawierać tylko wartości pierwotne (jak liczby i funkcje wbudowane). Kolejność stosowana, bliższa sposobowi, w jaki działają interpretery, sukcesywnie rozwija i oblicza wartości kolejnych czynników kombinacji. Chociaż wszystkie omawiane przez nas do tej pory programy działają dokładnie tak samo dla obu modeli obliczeń, w ćwiczeniu 1.5 autorzy podają ciekawy przykład procedury zachowującej się odmiennie przy użyciu normalnej i stosowanej kolejności obliczania. Kod wygląda tak:

(define (p) (p))

(define (test x y)
(if (= x 0)
0
y))

(test 0 (p))

W przypadku normalnej kolejności obliczania rozwinięcie ostatniej linijki wygląda następująco:

(if (= 0 0)
0
(p))

Wywołanie funkcji test jest zastępowane przez formę specjalną if. To wyrażenie zaś, w związku ze spełnionym warunkiem, upraszczane jest do wartości następnika, czyli do zera. Całe wyrażenie zwróci więc wartość zero. Inaczej jednak zachowa się interpreter implementujący stosowaną kolejność obliczania. W tym przypadku zasady obliczania każą rozwinąć wszystkie elementy kombinacji jeszcze zanim wywołamy procedurę. Próba rozwinięcia (p) spowoduje jednak nieskończoną pętlę i w efekcie program nigdy się nie zakończy. I tak właśnie stanie się jeżeli spróbujemy uruchomić powyższy kod w interpreterze DrScheme.

Jak widać z ostatniego przykładu, by móc pisać (i czytać) kod w języku Scheme trzeba znać nie tylko metodę interpretacji kombinacji, ale i specyficzną składnię form specjalnych. Formy specjalne, chociaż wyglądają jak funkcje, mają własne zasady obliczania wyrażeń. Najlepiej nam znaną formą specjalną jest define. Gdy piszemy (define x 42) nie jest obliczana wartość zmiennej x, co miało by miejsce, gdyby define było zwykłą funkcją. Bardziej wyrafinowany przykład znaleźć można w ćwiczeniu 1.6, którego treść pozwolę sobie w całości przytoczyć.

Liz P. Haker nie rozumie, dlaczego if musi być formą specjalną: “Dlaczego nie mogę zdefiniować jej jako zwykłej procedury za pomocą cond?”. Przyjaciółka Liz, Ewa Lu Ator, twierdzi, że rzeczywiście można tak zrobić, i definiuje nową wersję if:

(define (new-if predykat nastepnik alternatywa)
(cond (predykat nastepnik)
(else alternatywa)))

Ewa pokazuje Liz program:


(new-if (= 2 3) 0 5)
5
(new-if (= 1 1) 0 5)
0

Zachwycona Liz stosuje new-if w programie obliczającym pierwiastki kwadratowe:

(define (sqrt-iter guess x)
(new-if (good-enough? guess x)
guess
(sqrt-iter (improve guess x)
x)))

Co się stanie, gdy Liz spróbuje użyć tego programu do obliczenia pierwiastka kwadratowego? Odpowiedź uzasadnij.

Wystarczy wykonać jeden cykl podstawienia, by zauważyć jak działać będzie cała procedura. Interpreter napotykając na kombinację zawierającą new-if zastosuje zwykłe zasady obliczania, bo new-if nie jest formą specjalną, ale procedurą. Przed wywołaniem obliczy więc kolejne argumenty procedury: kombinację (good-enough? guess x), wartość guess i kombinację (sqrt-iter (improve guess x) x). Obliczenie tej ostatniej wymagać będzie oczywiście wywołanie funkcji sqrt-iter. A obliczenie jej jest równoważne z obliczeniem wartości kombinacji z operatorem new-if. To więc doprowadzi do rozwinięcia wspomnianych już trzech argumentów. I tak w kółko, program wpadnie w nieskończoną pętlę.

Skoro już jesteśmy przy ćwiczeniach, przedstawię jeszcze swoje rozwiązania ćwiczeń 1.7 i 1.8. Ćwiczenie 1.7 polega na ulepszeniu funkcji obliczającej pierwiastek kwadratowy w taki sposób, by była bardziej dokładna dla małych wartości argumentu.

(define (sqrt-iter guess lastguess x)
(if (good-enough? guess lastguess)
guess
(sqrt-iter (improve guess x) guess
x)))

(define (good-enough? guess lastguess)
(< (abs (- guess lastguess)) 0.0001))

(define (sqrt x)
(sqrt-iter 1.0 0.0 x))

; pozostale funkcje pozostaja niezmienione

W ćwiczeniu 1.8 należy zaimplementować metodę Newtona służącą do obliczania pierwiastków sześciennych. Rozwiązanie:

(define (sqrt x)
(define (sqrt-iter guess lastguess)
(if (good-enough? guess lastguess)
guess
(sqrt-iter (improve guess) guess)))
(define (improve guess)
(/ (+ (/ x (* guess guess)) (* 2 guess)) 3))
(define (good-enough? guess lastguess)
(< (abs (- guess lastguess)) 0.0001))
(sqrt-iter 1.0 0.0))

Do funkcji powrócimy jeszcze wielokrotnie, na dzisiaj jednak to już wszystko.

Uczymy się dodawać

Przeglądając archiwa pewnego lispowego bloga natrafiłem na ciekawe zadanko. Do tego bloga pewnie jeszcze wrócimy, a tymczasem zajmijmy się samym zadaniem:

Zdefiniuj rekurencyjne funkcje LIST+ i (*) LIST- wykonujące operacje “pisemnego” dodawania i odejmowania liczb reprezentowanych przez listy cyfr dziesiętnych, np.:

(LIST+ '(1 2 3) '(6 8)) ==> (1 9 1)

(*) (LIST- '(1 9 1) '(6 8)) ==> (1 2 3)

Zadanie nie jest trudne, ale zanim przedstawię swoje rozwiązanie, kilka słów dotyczących definiowania funkcji w Scheme. Do tej pory wykorzystywałem następujący model:

(define ( )
)

Często jednak zdarza się, że podczas pisania jednej funkcji piszemy dla niej funkcje pomocnicze. Na potrzeby funkcji merge-sort powstały np. right-half, split, czy slice. Definiowanie każdej funkcji w globalnej przestrzeni nazw jest wygodne podczas eksperymentowania, bo można dowolnie zmieniać i przenosić definicje. Jeżeli jednak nasza funkcja jest częścią większego programu, dobrym zwyczajem jest ukrywać wszystkie szczegóły, udostępniając na zewnątrz tylko prosty i zwarty interface. Technikę tę nazywa się black-box abstraction, jako że dany kawałek kodu możemy traktować jako “czarną skrzynkę” - interesuje nas tylko to, jakie wartości przyjmuje na wejściu i jakie wartości zwraca. W związku z tym, że możemy każdą z tych “czarnych skrzynek” oddzielnie przetestować i wymienić w razie potrzeby, ich stosowanie znacznie ułatwia rozwijanie i debugowanie kodu. W Scheme procedury, oprócz kombinacji zwracających wartość, mogą zawierać serię pomocniczych definicji. Wzór definicji funkcji wygląda więc teraz następująco:

(define ( )

)

W taki właśnie sposób zdefiniowałem ogólną metodę list-op, która wykonuje działania “słupkami”, od jedności do najbardziej znaczących cyfr zadanych liczb.

(define (list-op operator)
(define (list-parse L1 L2 carry)
(define (safe-car L)
(if (null? L)
0
(car L)))

(define (safe-cdr L)
(if (null? L)
()
(cdr L)))

(define partial-result
(+ (operator (safe-car L1) (safe-car L2)) carry))

(define current-digit
(modulo partial-result 10))

(define new-carry
(if (< partial-result 0)
(- (quotient partial-result 10) 1)
(quotient partial-result 10)))

(cons current-digit
(if (and (null? L1)
(null? L2))
()
(list-parse (safe-cdr L1)
(safe-cdr L2)
new-carry))))

(lambda (L1 L2)
(cut-zeros (reverse (list-parse (reverse L1)
(reverse L2)
0)))))

(define (cut-zeros L)
(if (or (null? L)
(not (zero? (car L))))
L
(cut-zeros (cdr L))))

(define list+ (list-op +))
(define list- (list-op -))

Prócz tego, że funkcja lisp-op wykorzystuje black-box abstraction, jest ona również tzw. higher-order function, tzn. funkcją, która przyjmuje na wejściu funkcję i zwraca jako wartość inną funkcję. Funkcje te szczegółowo przedstawia pan Sussman w wykładzie 2a.

Funkcję cut-zeros zostawiłem na zewnątrz, bo przyda mi się ona do zdefiniowania funkcji mnożącej dwie zadane liczby reprezentowane jako listy cyfr. Implementację uczyniłem najprostszą na ile to było możliwe. Skoro potrafimy już dodawać i odejmować, mnożenie zdefiniować można następująco:

(define (list* L1 L2)
(if (null? (cut-zeros L2))
()
(list+ L1 (list* L1 (list- L2 '(1))))))

Jak widać, pomnożyć liczbę a przez b to znaczy pomnożyć a przez b-1 i dodać do wyniku a. Jeżeli zaś b jest zerem, to wynikiem mnożenia jest lista pusta (u nas będąca synonimem zera). Implementację dzielenia pozostawiam jako ćwiczenie dla czytelnika. ;-)

Sortowanie

W poprzednim wpisie rozwiązywałem zadanie 1.3 z Wizard Booka. Polegało ono na napisaniu funkcji o trzech argumentach, która zwraca sumę kwadratów dwóch większych z nich. Zastanawiałem się również nad zadaniem uogólnionym, tzn. o funkcji przyjmującą dowolną ilość argumentów, która zwraca sumę kwadratów n największych z nich. Nie potrafiłem jednak wówczas wyobrazić sobie implementacji sortowania listy. Okazało się jednak, że znowu nie myślałem rekurencyjnie.

Przyjąłem bowiem, że jeżeli mam zaimplementować sortowanie, to powinienem zacząć od jednej z prostszych metod, jak np. sortowanie bąbelkowe (ang. bubble sort). Mimo usilnych prób nie potrafiłem jednak stworzyć niczego, co byłoby przejrzyste i proste. Zagnieżdżone iteracje, indeksowanie tablic i zamiana elementów - elementy idealnie pasujące do programowania imperatywnego w świecie zamkniętych nawiasów wyglądają co najmniej dziwnie.

Wreszcie dzisiaj, w trakcie podróży pociągiem, przypomniałem sobie o rekurencyjnych metodach sortowania. W przeciągu godziny na stronach notesu udało mi się zapisać w Scheme implementację algorytmu sortowania przez scalanie (ang. merge sort). Do szczęścia potrzebne były mi w zasadzie dwie rzeczy. Pierwsza to łączenie dwóch posortowanych list w jedną posortowaną. Implementacja okazała się całkiem prosta. Przede wszystkim, jeżeli któraś z list jest pusta, to wynik równy jest drugiej liście. W przeciwnym wypadku budujemy nową listę. Na jej szczyt wkładamy mniejszy z pierwszych elementów obu list, a resztę budujemy rekurencyjnie wywołując siebie. Lista, z której zdjęto pierwszy element zostaje oczywiście go w tym wywołaniu pozbawiona.

(define (merge L1 L2)
(cond ((null? L1) L2)
((null? L2) L1)
((< (car L1) (car L2)) (cons (car L1)
(merge L2
(cdr L1))))
(else (cons (car L2)
(merge L1
(cdr L2))))))

Operator cons tworzy parę z dwóch zadanych obiektów. Jego zagnieżdżone wywołania mogą służyć do tworzenia list. Np. lista (1 2 3) może być skonstruowana za pomocą wywołania (cons 1 (cons 2 (cons 3 ()))).

Należy się też słowo sprostowania. Ostatnim razem do sprawdzania, czy lista jest pusta zdefiniowałem własną funkcję empty?. Dzisiaj w otchłaniach dokumentacji dowiedziałem się o funkcji null?, która robi dokładnie to samo.

Poprawność działania funkcji merge możemy łatwo sprawdzić.

(merge '(1 2 3 4 5) '(2 4 6))
(1 2 2 3 4 4 5 6)

Przy pomocy apostrofu w zapisie '(1 2 3 4 5) informujemy interpreter, by nie wykonywał następującej po nim kombinacji. W tym przypadku jest to zwięźlejszy sposób na zapisanie (list 1 2 3 4).

Drugą rzeczą kluczową przy implementacji merge sort jest podział listy na dwie części. To zadanie uogólniłem do funkcji zwracającej dowolny kawałek listy zawarty pomiędzy zadanymi indeksami (czyli coś na wzór pythonowej metody slice).

; (head L): zwroc liste bez ostatniego elementu
(define (head L)
(if (= (length L) 1)
()
(cons (car L)
(head (cdr L)))))

(define (slice L start end)
(if (= start 0)
(if (> end (- (length L) 1))
L
(slice (head L) start end))
(slice (cdr L) (- start 1) (- end 1))))

Przykładowo więc dla listy (7 11 13 17 19 23) użycie

(slice L 0 2)

zwróci (7 11) (czyli element pierwszy i drugi), zaś

(slice L 2 5)

zwróci (13 17 19) (elementy od trzeciego do szóstego).

Na podstawie tej funkcji z łatwością możemy już podzielić listę na dwie części.

(define (split L index)
(list (slice L 0 index)
(slice L index (length L))))

Funkcja split dzieli listę w miejscu wskazanym przez indeks i zwraca listę, której pierwszym elementem jest lewa część zadanej listy, a drugim elementem prawa część. W merge sort będziemy dzielić listę dokładnie w połowie, dlatego warto zdefiniować do tego oddzielną funkcję:

(define (split-half L)
(split L (quotient (length L) 2)))

Lewą i prawą część listy uzyskujemy więc w ten sposób:

(define (left-half L)
(car (split-half L)))

(define (right-half L)
(cadr (split-half L)))

(cadr L) jest skrótowym zapisem dla kombinacji (car (cdr L)).
I to już wszystko, co jest potrzebne do zapisania algorytmu merge sort.

(define (merge-sort L)
(if (< (length L) 2)
L
(merge (merge-sort (left-half L))
(merge-sort (right-half L)))))

Czy to nie było proste? ;-)

Ostateczne rozwiązanie uogólnionego zadania 1.3 wygląda więc następująco:

; (max L n): zwroc n najwiekszych elementow z listy L
(define (max L n)
(slice (reverse (merge-sort L)) 0 n))

(define (square x)
(* x x))

(define (sum-of-squares L)
(if (null? L)
0
(+ (square (car L))
(sum-of-squares (cdr L)))))

(define (fun L n)
(sum-of-squares (max L n)))

Nawias za nawiasem

Definiowanie funkcji w Scheme jest rzeczą naprawdę piękną. Podczas czytania pierwszych przykładów kodu, jak i później, podczas pisania własnych kawałków rozwiązujących zadane problemy, towarzyszyło mi uczucie panowania nad środowiskiem wykonania programu. Swoboda wyrażania myśli przy pomocy prostych konstrukcji dawała (i wciąż daje) mi ogromną satysfakcję. Dzisiaj dopiero uświadomiłem sobie skąd to wrażenie się bierze.

Po pierwsze, zdefiniowane przez nas funkcje używa się dokładnie w ten sam sposób, jak te predefiniowane w używanym przez nas środowisku. Weźmy na przykład prostą definicję funkcji podnoszącej liczbę do dowolnego naturalnego wykładnika.

(define (** a b)
(if (= b 0)
1
(* a (** a (- b 1)))))

Definiujemy funkcję o nazwie ** (tak, to nie pomyłka) przyjmującą dwa argumenty. Jeżeli drugi argument jest zerem, zwracamy jeden (bo cokolwiek do potęgi zerowej to jeden), w przeciwnym wypadku wynikiem jest a przemnożone przez a do potęgi (b-1). Bardzo proste, a jednocześnie otwierające oczy na prawdziwe wykorzystanie rekurencji. Ale do tego jeszcze wrócimy później. Symbol podwójnej gwiazdki spodoba się zapewne programistom Pythona, ale niektórzy woleliby np. ^. Nie ma żadnego problemu.

(define (^ a b)
(if (= b 0)
1
(* a (^ a (- b 1)))))

W tej postaci funkcja wciąż działa poprawnie:

(^ 2 128)
340282366920938463463374607431768211456

Nic nie stoi oczywiście na przeszkodzie, by przedefiniować funkcje wbudowane. Co więcej, można nawet przeładować definicję form specjalnych! Co powiecie na to?

(define (define a)
(* a a))

Właśnie zdefiniowaliśmy funkcję o nazwie define, która podnosi swój argument do kwadratu. Oczywiście tym sposobem pozbawiliśmy się sposobu na używanie wbudowanego define, potencjalnie psując całą resztę kodu, która zapewne na domyślnym zachowaniu define polega. Ale z drugiej strony nikt nie powiedział, że programujemy na poważnie. ;-)

Drugim czynnikiem, który powoduje, że tak bardzo polubiłem nawiasy jest piękno jakie tkwi w prostocie składni Scheme. Ta prostota naprawdę zachęca i ośmiela do programowania. I o ile już słyszałem o programowaniu bottom-up i wydawało mi się, że rozumiem to pojęcie, dopiero po doświadczeniu lekkości i swobody pisania w Scheme poczułem na własnej skórze co ono oznacza. Chociaż lubię programować, Scheme powoduje, że nie mogę przestać definiować, wyszczególniać, abstrahować i tak aż do osiągnięcia ideału… Paul Graham znowu miał rację.

Nie trzeba daleko szukać, by przekonać się o tym, co napisałem. Rozwiążmy wspólnie ćwiczenie 1.3. Jego treść brzmi następująco:

Zdefiniuj procedurę o trzech argumentach będących liczbami, której wynikiem jest suma kwadratów dwóch większych argumentów.

Zanim zdefiniujemy zadaną funkcję, potrzebne nam będzie kilka funkcji pomocniczych. Potrzebujemy z pewnością funkcji znajdującej dwa większe elementy spośród trzech zadanych argumentów. Ja zdefiniowałem ją tak:

(define (max-two a b c)
(if (< a b)
(list b (if (< a c)
c
a))
(list a (if (< b c)
c
b))))

W kodzie wykorzystałem funkcję wbudowaną list, która ze wszystkich podanych argumentów tworzy listę. Kod jest prosty, ale dla tych, którzy jeszcze nie przyzwyczaili się do nawiasów, oto wersja w Pythonie:

def max_two(a, b, c):
if a < b:
if a < c:
return [b, c]
else:
return [b, a]
else:
if b < c:
return [a, c]
else:
return [a, b]

Nie jest on szczególnie pythonowy, ale obrazuje ideę. Za pomocą dwóch warunków znajdujemy najmniejszy element spośród trzech i zwracamy listę złożoną z pozostałych dwóch. Drugi warunek można jednak uprościć. Potrzebna nam będzie funkcja zwracającą element większy spośród dwóch zadanych. Nazwiemy ją greater.

(define (greater a b)
(if (> a b)
a
b))

(define (max-two a b c)
(if (< a b)
(list b
(greater a c))
(list a
(greater c b))))

Tak, teraz to wygląda o wiele lepiej. Problem w ogólnym przypadku (dla dowolnej ilości liczb) sprowadza się do posortowania listy i wybrania kilku elementów z początku/końca. Na tę chwilę jest to zadanie wykraczające poza moje możliwości programowania w Scheme. Ale obiecuję, że do tematu jeszcze wrócę.

Gdybyśmy teraz spróbowali stworzyć funkcję rozwiązującą podane zadanie, zauważylibyśmy, że czegoś nam brakuje.

(define (fun a b c)
(+ (square ...?

Problemem jest funkcja max-two, która zwraca listę. Wiemy, że lista jest dwuelementowa, musimy jeszcze dobrać się do kolejnych wartości. Scheme nie ma dla nas indeksowania, ale specyficzne operatory car i cdr. car zwraca pierwszy element listy, zaś cdr listę z usuniętym pierwszym elementem. Inaczej mówiąc, przy pomocy tych dwóch funkcji potrafimy rozłożyć listę na głowę i ogon. Jesteśmy już coraz bliżej rozwiązania.

(define (fun a b c)
(+ (square (car (max-two a b c)))
(square (car (cdr (max-two a b c))))))

Od razu widać brzydotę tego rozwiązania, bo musimy dwa razy wywołać funkcję max-two. W tym momencie istnieje pokusa, by obliczyć listę dwóch największych elementów i związać ją z jakąś lokalną nazwą we wnętrzu funkcji. Jako argumenty car i cdr wykorzystalibyśmy wtedy zawczasu obliczoną wartość. W Pythonie byłoby to całkiem naturalne:

def fun(a, b, c):
# przechowanie wartosci w tymczasowej zmiennej
L = max_two(a, b, c)
return square(L[0]) + square(L[1])

Jednak to rozwiązanie jest mało lispowe (o ile można mówić o wyczuciu stylu Lispa w tak wczesnym okresie nauki). Na usta (a raczej pod palce) ciśnie się więc definicja nowej funkcji.

(define (sum-of-squares L)
(+ (square (car L))
(square (car (cdr L)))))

(define (fun a b c)
(sum-of-squares (max-two a b c)))

Czyniąc listę argumentem funkcji zwalniamy interpreter z obowiązku dwukrotnego obliczania jej wartości (chociaż to też zależy od sposobu działania interpretera, co mam nadzieję opisać następnym razem). Oczywiście do szczęścia brakuje nam jeszcze funkcji square, obliczającej kwadrat podanego argumentu.

(define (square a)
(* a a))

I w ten oto sposób otrzymaliśmy działającą funkcję, obliczającą sumę kwadratów dwóch większych argumentów. Po napisaniu tego kodu pomyślałem, że ciekawie byłoby uogólnić funkcję sum-of-squares, tak by obliczała sumę kwadratów wszystkich elementów listy, bez względu na to ile by ich nie było. W związku z tym, że funkcja ta przetwarza listę, najbardziej naturalnie będzie zapisać jej definicję rekurencyjnie. Jak w każdej funkcji rekurencyjnej potrzebujemy więc warunku zakończenia rekursji. W tym przypadku będzie to lista pusta. Suma kwadratów listy pustej wyniesie oczywiście zero. Listy posiadające przynajmniej jeden element będą zaś rozwijane rekurencyjnie, poprzez ucinanie kolejnych głów operatorem car. Z resztą nie ma co tłumaczyć, kod jest naprawdę prosty. Oto on:

(define (empty? L)
(equal? L ()))

(define (sum-of-squares L)
(if (empty? L)
0
(+ (square (car L)) (sum-of-squares (cdr L)))))

Wyjaśnienia wymaga jeszcze tylko funkcja equal?. Zwraca ona wartość prawda, gdy podane jej dwa argumenty są równe. Na jej podstawie zdefiniowaliśmy funkcję empty? określającą czy lista jest pusta. Możliwość używania znaków takich jak wykrzyknik czy znak zapytania jest bardzo przydatną cechą Scheme. Zalety są wyraźnie widoczne.

W tym miejscu w zasadzie można by pokusić się o implementację ogólnych funkcji takich jak map, czy reduce, ale w ten sposób nie zostało by nic na później. ;-) Na dzisiaj to wszystko. Do usłyszenia.

Stając na ramionach gigantów

Wszystko zaczęło się od małego komputerka przyniesionego pewnego pięknego dnia przez mojego tatę do domu. Miał zewnętrzny odtwarzacz do kaset i masywny zasilacz. Wszystko, co tworzyło jednostkę centralną ukryte zostało w obudowie połączonej z klawiaturą. Podłączało się tę magiczną skrzyneczkę do telewizora i po włączeniu na niebieskim tle ukazywał się znak zachęty.

Commodore 64

Te piękne czasy, gdy do szczęścia wystarczało 64Kb RAMu i nie przeszkadzał brak twardego dysku, zdają się być bardzo odległe. Mój tata programy pisał w grubych zeszytach, które później razem skrzętnie przepisywaliśmy i zapisywaliśmy na zwykłych kasetach magnetofonowych. Chociaż wszystko to dzisiaj wydaje się śmieszne, wówczas byłem z możliwości tego sprzętu naprawdę zadowolony. C=64 miał prawdziwy user manual, grafikę opartą o sprite’y, nawet kartę dźwiękową! Nikomu nie przeszkadzało numerowanie linii w BASICu, czy potrzeba strojenia głowicy magnetofonu. Pomimo ograniczeń była to wówczas dla mnie technologia kosmiczna.

Musiało jednak przepłynąć wiele wody w rzece, bym był w stanie zrozumieć, że C=64 było tylko szczytem góry lodowej. Na przełomie lat 60-tych i 70-tych Ritchie i Thompson stworzyli język C i Uniksa. Koniec lat 80-tych to czas upowszechnienia się komputerów IBM PC. Przyłączające się do rządowego Arpanetu amerykańskie uczelnie kładą podwaliny Internetu. Polska dołączyła do globalnej wioski dopiero w roku 1991. W tym samym czasie Linus Torvalds upublicznia pierwszą wersję swojego systemu operacyjnego, który wraz z projektem GNU utworzy darmowy i dostępny dla każdego system, zmieniając tym samym zasady panujące na rynku informatycznym. W 1998 roku rusza google.com, która wkrótce staje się podstawowym sposobem wyszukiwania informacji w Internecie. Dostęp do informacji jeszcze nigdy w historii ludzkości nie był tak prosty.

Po dłuższej przerwie do komputerów wróciłem dopiero, gdy w naszym domu pojawił się pecet. Początkowo nieśmiało poznając system, próbowałem jednocześnie pisania programów w Turbo Pascalu i Delphi. Jednak rozwinąłem swoje skrzydła dopiero, gdy na dysku zagościł Red Hat. Nie potrzeba było wiele czasu bym przyswoił sobie C, shella i Perla. Jeżeli było trzeba pisałem też w assemblerze. Niecały rok temu zainteresowałem się Pythonem i ten język jak żaden inny odmienił moje spojrzenie na programowanie. Wciąż jednak czegoś mi brakowało. Języki które poznawałem w pewien sposób były podobne do siebie. Moje doświadczenia wyniesione z długiego okresu programowania w C powodowały, że myślałem imperatywnie. Tą zmienną zwiększ o dwa, wywołaj tamte trzy funkcje i zwróć wartość siedem. Oczywiście znałem już wtedy Paula Grahama i czytałem jego teksty zachwalające Lispa, jednak nigdy nie znalazłem dłuższej chwili czasu, by przyjrzeć się dobrze temu językowi.

Pewnego dnia natrafiłem na artykuł opisujący materiały do nauki języka Scheme (jednego z dialektów Lispa), dostępne za darmo z MIT. Do ściągnięcia jest zarówno książka, jak i wykłady w formacie wideo. Po przeczytaniu pierwszego akapitu książki już wiedziałem, jakiemu językowi poświęcę czas przez najbliższe miesiące.

Procesy obliczeniowe to abstrakcje zamieszkujące komputery. W trakcie swojego życia korzystają z innych abstrakcyjnych obiektów zwanych danymi. Życie procesu przebiega zgodnie z zestawem reguł zwanym programem. Ludzie tworzą programy, aby sterować procesami. W rezultacie za pomocą naszych reguł ujarzmiamy moce drzemiące w komputerze.

Obejrzenie pierwszych wykładów tylko wzmocniło moje przekonanie. Scheme jest językiem, który po prostu muszę poznać. Wkrótce w uczelnianej księgarni zakupiłem polską wersję drugiego wydania Wizard Booka, zacząłem czytać, rozwiązywać przykłady, wracając od czasu do czasu do świetnych nagrań wykładów. I w pewnym momencie narodziła się w mojej głowie myśl, by tę ścieżkę nauki w pewien sposób zarejestrować, utrwalić, ułatwiając zainteresowanym zagłębienie się w świat języków funkcjonalnych. Świat dziwaczny i trudny do objęcia umysłem, ale zadziwiająco piękny i pobudzający wyobraźnię. I to jest właśnie moment narodzin tego bloga. Mam zamiar opisywać na jego stronach kolejne etapy poznawania Scheme, idąc rozdział po rozdziale Wizard Booka. Nie będę jednak powielał zawartych w książce treści, skupię się bardziej na interesujących różnicach w stosunku do znanych mi języków programowania i na rozwiązywaniu wymienionych na końcu każdej części zadań. Chociaż zaczynam moją podróż samotnie, z radością zabiorę ze sobą wszystkich zainteresowanych bezdrożami programowania. Zachęcam do komentowania i życzę przyjemnej lektury.

« Wcześniejsze wpisy · Nowsze wpisy »