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.