Kto powiedział, że dzieci mają się uczyć Pascala? Owszem, język ten ma wiele zalet i nie jest zbyt skomplikowany. A gdyby tak… LISP? 🙂
Tutaj znajdziemy tutorial w wersji HTML, a tu – PDF do wydruku.

LISP dla gimnazjalistów

Część I: Podstawy
LISP. Język pierwszych hakerów. Jeden z najstarszych, które przetrwały do dziś. Język sztucznej inteligencji i systemów operacyjnych. Język nawiasów i poleceń o niejasnej etymologii. LISt Processing. LISP.
Co można zrobić w Lispie? Zanim przejdziemy do wykonywania konkretnych zadań, poznamy podstawowe elementy języka. Jako interpretera użyjemy CLISP, łatwego w obsłudze (edycja linii readline) i efektywnego.

Proste typy danych

Najprostszymi elementami są symbole. Jeśli wprowadzimy je w interpreterze, zostanie dokonana ich ewaluacja:

  [1]> 1  1  [2]> 2  2  [3]> 3  3  [4]> 0  0  [5]> -3  -3  [6]>  

Jak widać liczby ewaluują się do samych siebie. Podobnie jest z literałami:
[17]> „Fu” „Fu” [1]> 'Fu’ FU [2]>
Dodatkowo mamy dwa symbole specjalne na oznaczenie prawdy i fałszu:

  [1]> t  T  [2]> nil  NIL  [3]>  

Podobnie jak w innych popularnych językach, zmiennym można przypisywać wartości (setq):

  [1]> (setq a 1)  1  [2]> a  1  

Jaki z tego pożytek? Ano na razie żaden. Poznaliśmy pierwszy z dwóch podstawowych typów danych („obiektów”) w Lispie. Ten mniej istotny: atomy. Drugim, o wiele ważniejszym, są (niespodzianka ;)) listy. Listy, składające się z atomów i innych list, umieszczamy w nawiasach. Przykładem listy może być np. (1 2 3 4). Albo („Janek”,”Olgierd”,”Grigorij”). Albo (x y z). Są to proste, nieskomplikowane typy list. Zauważmy, że jeśli zechcemy poddać ewaluacji taką surową listę, nic z tego nie wyjdzie:

  [1]> (3 1 4 1 5 9)    *** - EVAL: 3 is not a function name  

Dlaczego? Zaraz się dowiemy 🙂


Funkcje

Same dane nie zdadzą nam się na wiele. Aby coś z nimi zrobić, potrzebujemy funkcji. Możemy na przykład spróbować dodać dwa atomy:

  [1]> (+ 3 1)  4  

Brawo! Wykonaliśmy pierwszą funkcję w Lispie 🙂 Co na początku się może wydać dziwne, a później bardzo pomocne, wyrażenie jest ewaluowane od lewej do prawej. To znaczy najpierw interpreter dowiaduje się, że ma dodawać, następnie pobiera elementy do dodania. Z innymi działaniami jest podobnie:

  [1]> (* 2 3 4)  24  

Oczywiście operacje można mieszać:

  [1]> (+ 1 (* 3 4))  13  

Wiele funkcji służy do manipulacji listami: reverse odwraca kolejność elementów listy, length zwraca długość listy, append łączy ze sobą dwie listy:

  [1]> (reverse `("Hans","Bruner"))  ("Bruner" "Hans")  [2]> (length `("Hans","Bruner"))  2  [3]> (append `("Hans","Bruner") `("WOLF"))  ("Hans" "Bruner" "WOLF")  

Zauważyliście ten niepozorny ` przed listą? Zapobiega on ewaluacji listy (w przeciwnym wypadku interpreter przyjąłby, że pierwszy element listy jest funkcją, a pozostałe jej argumentami). Stąd też możemy napisać:

  [1]> `(a b c)  (A B C)  

ale już nie:

  [1]> (a b c)    *** - EVAL: the function A is undefined  1. Break [3]>  

Jak widzimy, interpreter przejął się swoja rola i zabrał się do ewaluacji listy, co nie było naszą intencją. Tak więc ten niepozorny ` stanie się naszym dobrym przyjacielem, pozwalającym wskazać interpreterowi właściwą drogę.
Funkcji jest oczywiście *znacznie* więcej, omówiliśmy tylko kilka z nich. Na resztę przyjdzie czas później 🙂


Pierwsza własna funkcja

No dobrze, koniec teorii. Przyszedł czas na napisanie pierwszej własnej funkcji. Nasze programy będą się składały głównie z funkcji, więc umiejętność ich pisania ma fundamentalne znaczenie.
Funkcje w Lispie tworzy się bardzo prosto: na początku umieszczamy słowo kluczowe defun, następnie nazwę funkcji wraz z listą argumentów, wreszcie – samą funkcję. Skomplikowane? Wcale. Rozważmy prosta funkcję dodającą dwie liczby:

  [1]> (defun dodaj(x y) (+ x y))  DODAJ  

Jest to konstrukcja analogiczna do znanej z C:
int dodaj(int x, int y) { return x+y; };
Przeanalizujmy ją jeszcze raz: po defun mamy listę argumentów (istnieje tu pewna swoboda, do której wrócimy później ), w naszym przypadku jest to (x y). Potem musi nastąpić ciało funkcji – przetwarzanie argumentów naszej funkcji: (+ x y).
Jesli nie popełnilismy żadnego błędu, powinnismy otrzymać nadzwyczaj użyteczną 😉 funkcję:

  [1]> (dodaj 8 3)  11  

Dla rozgrzewki równie uzyteczna funkcja do obliczania obwodu koła o zadanym promieniu:

  [14]> (defun obwod(r) (* 2 pi r))  OBWOD  [15]> (obwod 4)  25.132741228718345908L0  [16]>  

„Maaaaaamo, ja chę 'Hello world'”. Spokojnie, przyjdzie i na to czas.


Standardowe wejście i wyjście

W Lispie wiele rzeczy można zrobić na wiele sposóbów. Najprościej jest użyć funkcji format:

  [1]> (format t "Gdzie błądzisz, rycerzu?")  Gdzie błądzisz, rycerzu?  NIL  

Funkcja format jest bardzo elastyczna, przypomina trochę printf z C. Czasem można też użyć princ. Do nowej linii przechodzimy z terpri.
Z kolei do odczytu służy funkcja read:

  [2]> (setq a (read))  Och  OCH  [3]> a  OCH  

Czas na pierwszy program, który coś robi. Utwórzmy plik o nastepującej zawartości:

  (setq dolar 3.9)    (defun nazlote(x)    (* dolar x)  )    (princ "Ile masz dolarów? ")  (setq iledol (read))  (princ "W takim razie masz ")  (princ (nazlote iledol))  (princ " złotych")  (terpri)  

Jak sie nietrudno domyślić, program działa mniej więcej w ten sposób:

  [arturs@darkstar lisp]$ ./dolar.lisp  Ile masz dolarów? 130  W takim razie masz 507.0 złotych  

Jeśli dodatkowo pierwszą linijką skryptu będzie

  #!/usr/bin/env clisp  

…a skrypt vędzie miał atrybut +x, będziemy mogli uruchomić go z linii poleceń tak jak zwykły program.


A co jeśli…?

Przejdziemy teraz do obsługi warunków. Najprostsze są te oparte o if (jeśli):

  [1]> (setq ANIA "Fajna laska")  "Fajna laska"  [2]> ANIA  "Fajna laska"  [3]> (if (equal ANIA "Fajna laska") (princ "Tak,to prawda"))  Tak,to prawda  "Tak,to prawda"  

W powyższym przykładzie zmiennej ANIA przypisujemy wartość „Fajna laska”. Upewniamy się, że wartość rzeczywiście została przypisana. Wreszcie następuje sama konstrukcja warunkowa. Po if mamy dwa bloki: pierwszy to warunek, który jest sprawdzany, drugi to polecenie wykonywane jesli warunek zostanie spełniony. W naszym przypadku sprawdzany jest warunek (equal ANIA „Fajna laska”), tzn. „Czy zmienna ANIA jest równa (equal) Fajna laska”. Jesli tak, program wykonuje kolejny blok, czyli – w naszym przypadku – pisze (princ) „Tak, to prawda”.
Inny przykład:

  [1]> (setq pieski 3)  3  [2]> (if (> pieski 2) (princ "Mamy więcej niż 2 pieski"))  Mamy więcej niż 2 pieski  "Mamy więcej niż 2 pieski"  

Równie intuicyjna jest instrukcja cond:

  [1]> (setq a 1)  1  [2]> (cond  ((> a 1) (princ "a > 1"))  ((< a 1) (princ "a < 1"))  ((= a 1) (princ "a = 1"))  (t      (princ "Ups, coś nie tak..."))  )  a = 1  "a = 1"  

Jak widzimy, cond jest czymś w rodzaju złożonego if: składa się z wielu par; jeśli warunek okreslony w pierwszej części pary jest spełniony, wykonywany jest drugi element pary.
A co robi to t na końcu? Otóż t (prawda) zawsze jest prawdziwe, dlatego warunek ten jest zawsze prawdziwy, a następujące potem wyrażenie jest ewaluowane jeśli nie został spełniony którykolwiek z wcześniejszych warunków. Jest to więc coś w rodzaju "else" ("jeśli nie") w innych językach.


Pętle

W Lispie istnieje wiele typów pętli. Jesli chcemy coś zrobić z elementami listy, najprościej jest użyć konstrukcji z loop:

  [1]> (setq Wykonawcy `("Eminem" "MC Hammer" "Warszawski deszcz"))  ("Eminem" "MC Hammer" "Warszawski deszcz")  [2]> (loop for i in Wykonawcy do  (princ i)  (terpri)  )  Eminem  MC Hammer  Warszawski deszcz  NIL  

Wypróbujmy naszą znajomość na prostym skrypcie:

  #!/usr/bin/env clisp    (setq rzeczowniki `(mysz śnieg jabłko marchew chorągiew taboret))  (setq przyslowki `(nieśmiało gwałtownie szybko uporczywie wściekle))  (setq czasowniki `(je widzi dotyka niszczy ściska rąbie połyka))    (defun sp () (format t " "))    (loop for i in rzeczowniki do      (princ i)      (sp)      (princ (nth (random 4) przyslowki))      (sp)      (princ (nth (random 3) czasowniki))      (sp)      (princ (nth (random 3) rzeczowniki))      (terpri)  )  

Na początku definiujemy trzy listy: z czasownikami, przysłówkami i czasownikami. Następnie - z powodu wrodzonego lenistwa - definiujemy funkcję, która będzie nam wstawiała spację. Wreszcie mamy główną pętlę: loop for i in rzeczowniki do znaczy mniej więcej: "dla każdego i należącego do listy 'rzeczowniki' rób co następuje:" ...i dalej mamy zestaw czynności do wykonania. Wiemy już, co robi (princ i) i (sp) - ale co, u licha, robi to nth i random? otóż nth zwraca n-ty element listy. Jeśli więc mamy listę (a b c d e f), wtedy (nth 0 lista) zwróci nam a, (nth 1 lista) - b, itd. Z kolei (random liczba) zwraca liczbę losową z zakresu (0-liczba). Niestety, liczba ta jest taka sama przy każdym uruchomieniu interpretera.
Jak więc działa nasz skrypt? Tworzy stek bezsensownych zdań:

  [arturs@darkstar lisp]$ ./wordz.lisp  MYSZ NIEŚMIAŁO DOTYKA ŚNIEG  ŚNIEG SZYBKO JE JABŁKO  JABŁKO UPORCZYWIE JE MYSZ  MARCHEW SZYBKO JE JABŁKO  CHORĄGIEW GWAŁTOWNIE WIDZI MYSZ  TABORET NIEŚMIAŁO JE MYSZ  

Rekursja

Rekursja w przypadku funkcji polega na odwoływaniu się do siebie samej. Modelowym przykładem jest obliczanie silni. Jak pamiętamy, silnia(!) stanowi iloczyn kolejnych liczb naturalnych aż do danej liczby:

  1! = 1  2! = 1 * 2 = 2  3! = 1 * 2 * 3 = 6  

...i tak dalej. Tak więc lispowa funkcja obliczająca silnię powinna sprawdzić, czy liczba nie jest przypadkiem większa od 1, a jeśli tak, pomnożyć ją przez... silnię tej liczby zmniejszonej o jeden! Brzmi skomplikowanie?

  (defun silnia(x)  (if (> x 1)      (* x (silnia (- x 1)) )      1      )  )    (princ (silnia 4))  

Skąd ta jedynka na końcu? Jak pamiętamy, tzrecia część bloku if jest wykonywana wtedy, kiedy warunek określony w pierwszej nie zostanie spełniony. Wypróbujmy - to naprawdę działa 🙂


Mój pierwszy serwer!

Spokojnie, ten program nie będzie zbyt użyteczny ;). Załóżmy, że chcemy, by każdy łączący się z naszym komputerem na porcie 6666 mógł się dowiedzieć którego dzisiaj mamy. Do pobierania daty systemowej służy m.in. funkcja get-decoded-time; jej rezultat umieścimy w liście:

  (multiple-value-list (get-decoded-time))  

Nasz program "wiem, którego dzisiaj mamy" moglibyśmy więc zapisać tak:

  #!/usr/bin/env clisp    (setq dzien (nth 3 (multiple-value-list (get-decoded-time) )))  (setq miesiac (nth 4 (multiple-value-list (get-decoded-time) )))  (setq rok (nth 5 (multiple-value-list (get-decoded-time) )))    (format t "DZIŚ MAMY ~S ~S ~S ROKU. ~%" dzien miesiac rok)  

...ale nie zostawimy go w tej postaci, ponieważ wynik w postaci "DZIŚ MAMY 4 7 2001 ROKU." nie brzmi zbyt ładnie. Spróbujemy dodać listę miesięcy:

  #!/usr/bin/env clisp    (setq dzien (nth 3 (multiple-value-list (get-decoded-time) )))  (setq miesiac-n (nth 4 (multiple-value-list (get-decoded-time) )))  (setq rok (nth 5 (multiple-value-list (get-decoded-time) )))    (setq miesiace `(stycznia lutego marca kwietnia maja czerwca        lipca sierpnia września października listopada grudnia))    (setq miesiac (nth (- miesiac-n 1) miesiace))    (format t "DZIŚ MAMY ~S ~S ~S ROKU. ~%" dzien miesiac rok)    

W rezultacie powinniśmy otrzymać krzykliwy rezultat:

  [arturs@darkstar lisp]$ ./data.lisp  DZIŚ MAMY 4 LIPCA 2001 ROKU.  

Rzecz jasna, wcale nie musimy krzyczeć. Każdy z miesięcy mozemy ująć w cudzysłowie, zaś drugie ~S zamienić na ~A (skrót od ASCII).
Ale gdzie ten serwer? Powoli, wszystko po kolei. Posłuzymy się starą sztuczką z inet. Umieścimy następującą linijkę w /etc/inetd.conf:

  6666    stream  tcp     nowait  root    /usr/sbin/tcpd /sbin/data.lisp  

Teraz wystarczy tylko zrestartować inetd i możemy przetestować nasz serwer w działaniu ;).

  [arturs@darkstar arturs]$ telnet localhost 6666  Trying 127.0.0.1...  Connected to localhost.localdomain.  Escape character is '^]'.  Dziś mamy 4 lipca 2001 roku.  Connection closed by foreign host.  

Nie, pod Windows się nie uda.


Koniec części pierwszej.
Archiwalny news dodany przez użytkownika: arturs.
Kliknij tutaj by zobaczyć archiwalne komentarze.

Oznaczone jako → 
Share →