Duże programy

Duże programy wymagają nałożenia jakiejś organizacji, żeby móc nad nimi zapanować.

Wymagają też ułatwień i konwencji notacyjnych, dzięki którym nie trzeba pamiętać nieistotnych szczegółów.

Desygnatory

Desygnator to nie obiekt Lispu, lecz pojęcie. Chodzi o to, żeby wszędzie tam, gdzie to jest możliwe, dać swobodę określania argumentu. Najłatwiej to zobaczyć na przykładzie.

Jeśli w opisie funkcji mowa o tym, że argument ma być desygnatorem listy, oznacza to, że ma być

A przy okazji: atom to dowolny obiekt Lispu nie będący parą. Pojęcie dość stare, kiedyś ważne, teraz nieco straciło na znaczeniu. Tradycyjnie, wektor jest atomem!

Definicja desygnatora może być rekurencyjna, na przykład desygnator pakietu (o pakietach za chwilę) może być

Z kolei desygnator napisu może być

Tak więc, żeby dostać standardowy pakiet Common Lispu, można użyć dowolnego z poniższych wyrażeń

(find-package 'cl)
(find-package :cl)
(find-package #:cl)
(find-package "CL")
Lisp standardowo nie rozróżnia dużych i małych liter w języku, natomiast wewnętrznie wszystkie symbole (czyli identyfikatory) zapisywane są dużymi literami. Istnieją opcje pozwalające to zmieniać.

Konwencje notacyjne

Nazwy zmiennych globalnych otacza się znakami gwiazdki, np. *moja-baza*. W Lispie mozna również mieć stałe globalne, definiowane przez

(defparameter +rozmiar-stosu+ 30)

Nie są to jednak prawdziwe stałe (nie można ich użyć jako selektorów w case), lecz zmienne z zabronioną modyfikacją. Ich nazwy otacza się znakami '+', ale ta konwencja znacznie rzadziej jest używana.

Predykaty (funkcje zwracające wartości boolowskie) tradycyjnie miały na końcu 'p' lub '-p'. Tak jest dla niektórych funkcji stadardowych, na przykład consp lub numberp, ale na przykład atom nie ma nic (owszem, kiedyś niektóre klawiatury nie miały znaku zapytania). W nowszych systemach preferuje się umieszczenie na końcu nazwy znaku zapytania, tak jak w Scheme czy Dylanie.

W innych dialektach Lispu dla funkcji mających efekty uboczne używa się przyrostka '!' (bang), ale w Common Lispu słabo się to przyjmuje. Takie funkcje raczej mają na początku literę 'n', na przykład nconc czy nreverse. Ale delete i sort już nie (uważajcie, sort jest destrukcyjny). I tak pomału doszliśmy do przestrzeni nazw. Każdy by chciał, żeby to jego funkcja nazywała się ,,find-solution''. Trzeba więc wprowadzić przestrzenie nazw. W Lispie nazywają się pakietami.

Pakiety

Żeby można było użyć symbolu (powiedzmy jako nazwy zmiennej czy funkcji), musi on znajdować się w jakimś pakiecie. To nie obiekty należą do pakietów, tylko ich nazwy. Ta sama funkcja zdefiniowana lambda-wyrażeniem może mieć różne nazwy w różnych pakietach.

Najważniejszym pakietem jest pakiet o nazwie "COMMON-LISP", zawierający wszystkie nazwy standardowe (a więc i obiekty też). Normalnie nie wolno go modyfikować pod groźbą utraty zgodności ze standardem.

Jakiś pakiet jest zawsze pakietem bieżącym. Jest to pakiet, który jest wartością zmiennej globalnej *package*, początkowo jest to :cl-user. Zmieniamy go go wyrażeniem in-package, na przykład

(in-package :common-lisp)

Nie należy jednak tego robić, zwłaszcza podczas pracy interakcyjnej. Interpreter próbowałby każdy nowy symbol umieścić w tym pakiecie. Zamiast tego normalnie interpreter pracuje w pakiecie :common-lisp-user (pamiętacie jeszcze o desygnatorach -- liczy się tylko nazwa jako desygnator napisu).

Dlaczego jednak będąc w tym pakiecie mamy dostęp do symboli z pakietu :common-lisp? Każdy pakiet określa, jakie symbole eksportuje. Podczas tworzenia (definiowania) nowego pakietu można podać, z jakich pakietów ma dziedziczyć. Symbole z tych pakietów są w nim wówczas dostępne.

Uwaga: operacja ta nie jest przechodnia, to znaczy jeśli A dziedziczy z B, a B dziedziczy z C, to w A nie będzie widać symboli zewnętrznych z pakietu C. Musimy je ponownie wyeksportować, ale z B.

Definiowanie pakietu

Pakiet jest normalnym obiektem Lispu, można go utworzyć funkcją make-package

Funkcja

(make-package nazwa-pakietu [:nicknames lista-nazw]
                                   [:use lista-pakietów])
tworzy nowy pakiet o podanej nazwie.

Parametr :use podaje pakiety, których używa nowy pakiet = dziedziczy ich symbole eksportowane. Domyślnie jest to tylko pakiet common-lisp. Parametr :nicknames to lista alternatywnych nazw dl pakietu, często krótszych, na przykład cl-user dla common-lisp-user.

Ponieważ tworzenie istniejącego pakietu jest błędem, najlepiej postępować tak, jak w poniższym przykładzie.

* (unless (find-package :foo)
    (make-package :foo))  ;tworzymy pakiet foo

Zwykle jednak korzysta się z konstrukcji defpackage, wyposażonej w szereg opcji. Poniżej uproszczona postać

(defpackage nazwa
  (:nicknames synonim ...) 
  (:use pakiet ...)
  (:export symbol ...)
  ...)

Nickname to synonim nazwy pakietu, na przykład dla :common-lisp-user używa się zwykle skróconej nazwy :cl-user, dla :common-lisp zaś :cl.

Pora na przykład

(defpackage :asocjacyjna-baza-danych
  (:nicknames :abd) 
  (:use :cl :socket)
  (:export fetch store save-database))

Pakietu :cl-user raczej nie należy importować do pakietów aplikacji, ponieważ jego zawartość zależy od implementacji.

Umiemy już definiować pakiety, więc możemy rozważyć następujący problem. Napisaliśmy kilka pożytecznych funkcji i chcielibyśmy je udostępnić innym. Są na pliku "moje.lisp". Jednak jeśli ktoś załaduje je przez

(load "moje.lisp")
to zostaną one zdefiniowane w tym pakiecie, który był bieżący przy wywołaniu load. Dlatego zaleca się, żeby każdy plik rozpoczynał się linią określającą pakiet
(in-package :abd)
co ustawia wartość zmiennej globalnej *package* na podany pakiet. W efekcie pakiet ten staje się pakietem bieżącym.

Pakiet bieżący jest zmieniany tylko na czas ładowania pliku. Po załadowaniu pliku wracamy automagicznie do poprzedniego pakietu.

Funkcja import umieszcza symbole w podanym pakiecie (domyślnie bieżącym).

(import symbole [pakiet])
Stają się one jego symbolami wewnętrznymi. Można się do nich (w podanym pakiecie) odwoływać bez prefiksu pakietu. Pierwszy argument jest zwykle listą symboli, ale może być pojedynczym symbolem.

Jeśli któryś symbol jest już w podanym pakiecie, nic się nie dzieje. Natomiast jeśli inny symbol (pochodzący z innego pakietu) o tej samej nazwie jest zawarty w pakiecie, następuje sygnalizacja błędu.

* (unless (find-package :foo) (make-package :foo))
#<PACKAGE "FOO">

* (intern "F1" (find-package :foo))
FOO::F1
NIL

* (intern "F2" :foo)
FOO::F2
NIL

* (find-symbol "F1" (find-package :foo))
FOO::F1
:INTERNAL

* (export 'foo::F1 (find-package :foo))
T

* (find-symbol "F1" (find-package :foo))
F1
:EXTERNAL

;; Importujemy do bieżącego pakietu.
* (import 'foo:f1)
T

* (find-symbol "F1")
F1
:INTERNAL

Funkcja

(package-used-by-list pakiet)
zwraca listę pakietów zależnych od podanego pakietu (używających go).
* (package-used-by-list :sb-ext)
(#<PACKAGE "SB-BSD-SOCKETS-INTERNAL"> #<PACKAGE "SB-X86-64-ASM">
 #<PACKAGE "SB-EVAL"> #<PACKAGE "SB-WALKER"> #<PACKAGE "SB-VM">
 #<PACKAGE "SB-UNIX"> #<PACKAGE "SB-SYS"> #<PACKAGE "SB-PROFILE">
 #<PACKAGE "SB-PRETTY"> #<PACKAGE "SB-PCL"> #<PACKAGE "SB-REGALLOC">
 #<PACKAGE "SB-KERNEL"> #<PACKAGE "SB-GRAY"> #<PACKAGE "SB-FORMAT">
 #<PACKAGE "SB-IMPL"> #<PACKAGE "SB-FASL"> #<PACKAGE "SB-DISASSEM">
 #<PACKAGE "SB-DI"> #<PACKAGE "SB-DEBUG"> #<PACKAGE "SB-C">
 #<PACKAGE "SB-BIGNUM"> #<PACKAGE "SB-ASSEM"> #<PACKAGE "SB-ALIEN">
 #<PACKAGE "COMMON-LISP-USER">)

Funkcje do-external-symbols i do-symbols iterują po wszystkich symbolach pakietu. Natomiast funkcja

(list-all-packages)
podaje listę wszystkich istniejących pakietów.

Dostęp do pakietów

Dziedziczenie to nie jedyny sposób na dostęp do symboli pakietu. Można to zrobić w każdej chwili, poprzedzając nazwę symbolu nazwą pakietu.

(abd:fetch '(jest tam kto))
Symbole jest, tam i kto będą z bieżącego pakietu. Natomiast fetch z pakietu abd.

Jeśli nie chcemy co chwila prefiksować symboli, można użyć

(use-package :abd)
Powoduje to doraźne zaimportowanie całego pakietu abd.

Czasami chcemy zaimportować pakiet, ale występuje konflikt symboli: w naszym pakiecie mamy taki sam symbol jak w pakiecie importowanym. W takich sytuacjach można zamaskować importowanie wybranych symboli.

Funkcja

(shadow symbole [pakiet])
umieszcza symbole na liście shadowing-symbols pakietu. Jeśli któregoś symbolu nie ma pakiecie, jest on tworzony w nim.

Dla symboli na liście shadowing-symbols wszelkie konflikty nazw podczas importu są rozwiązywane automatycznie: importowane symbole są ignorowane.

* (lisp-implementation-version)
"1.4.5"

* (intern "LISP-IMPLEMENTATION-VERSION" :foo)
FOO::LISP-IMPLEMENTATION-VERSION
NIL

* (shadow 'lisp-implementation-version :foo)
T

* (in-package :foo)
#

* (lisp-implementation-version)
; in: LISP-IMPLEMENTATION-VERSION
;     (FOO::LISP-IMPLEMENTATION-VERSION)
; 
; caught COMMON-LISP:STYLE-WARNING:
;   undefined function: LISP-IMPLEMENTATION-VERSION
; 
; compilation unit finished
;   Undefined function:
;     LISP-IMPLEMENTATION-VERSION
;   caught 1 STYLE-WARNING condition

debugger invoked on a COMMON-LISP:UNDEFINED-FUNCTION in thread
#:
  The function FOO::LISP-IMPLEMENTATION-VERSION is undefined.

Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL.

restarts (invokable by number or by possibly-abbreviated name):
  0: [CONTINUE      ] Retry calling LISP-IMPLEMENTATION-VERSION.
  1: [USE-VALUE     ] Call specified function.
  2: [RETURN-VALUE  ] Return specified values.
  3: [RETURN-NOTHING] Return zero values.
  4: [ABORT         ] Exit debugger, returning to top level.

("undefined function")
0] 4

* (cl:lisp-implementation-version)
"1.4.5"

Jeśli natomiast chcemy maskować podczas importu istniejące symbole, to należy użyć funkcji

(shadowing-import symbole [pakiet])
Ta funkcja preferuje symbole importowane. Jeśli napotkamy symbol wewnętrzny o tej samej nazwie, będzie on usunięty z pakietu.

Nowe symbole trafią też na listę shadowing-symbols.

Własny pakiet

No to spróbujmy zrobić własny pakiet. Plik queue.lisp zawiera prostą definicję kolejek FIFO. Chcemy nią uszczęśliwić świat, więc trzeba to obudować pakietem. W tym przypadku zapewne definicja pakietu znalazłaby się na początku pliku, jednak przy większych bibliotekach umieszcza się ją osobno. I my też tak zrobimy.

Dwie zalety osobnej definicji pakietu

W pliku queue-package.lisp proponowana definicja.

Zwróćcie uwagę na oryginalny zapis eksportów. Liczy się tylko nazwa symbolu, więc pozornie można by napisać

  (:export make-queue empty? attach-to-queue first-elem remove-first))
ale wtedy w bieżącym pakiecie (tym, w którym pracujemy) powstałyby takie symbole. Przy najbliższym
(use-package :fifo)
powstałby konflikt: próba zaimportowania symboli nazywających się tak samo jak symbole lokalne. A tego robić nie wolno. Można wprawdzie zrobić tak, żeby nowe symbole przesłoniły stare, ale to przerost formy nad treścią.

Zamiast tego

W tym ostatnim przypadku potrzebne będzie dodatkowe niezależne wywołanie wewnątrz pakietu fifo-queue, po uprzednim przejściu do tego pakietu.

(in-package :fifo)

(export '(make-queue empty? attach-to-queue first-elem remove-first))

Niektórzy wolą umieszczać takie wywołanie na początku każdego z plików, podając za każdym razem tylko symbole z tego pliku. W przypadku metod w wielu plikach będą z tym kłopoty, bo nie wiadomo gdzie umieścić eksport. Porządny program powinien wprawdzie najpierw zdefiniować funkcję generyczną, ale kto dziś pisze porządne programy, skoro pierwsza napotkana definicja metody powoduje automatyczne zdefiniowanie funkcji generycznej, o ile takowa nie istnieje.

Ładowanie plików

Najprościej w głównym pliku umieścić szereg wywołań funkcji load dla pozostałych plików. Trzeba jednak dać ścieżki absolutne w nazwach albo uzależnić je od głównego pliku (merge-pathnames). Prędzej czy później ktoś zechce załadować ten plik z innego katalogu, a Lisp domyślnie ładuje z katalogu bieżącego.

W trakcie ładowania pliku wartością dynamicznej zmiennej *load-pathname* staje się nazwa ładowanego pliku (jej globalna wartość to nil). Podobną rolę pełni zmienna *load-truename*.

Można na przykład wydrukować nazwę ładowanego pliku

(eval-when (load) (print *load-pathname*))
(eval-when (load) (print *load-truename*))

Funkcja load tworzy również nowe wiązanie dynamiczne zmiennej *package*. Dzięki temu jeśli w pliku nie użyto in-package, to ładowanie odbywa się w zewnętrznym pakiecie. A jeśli użyto, to ładujemy w nowym pakiecie, ale potem wracamy do starego.

Działa, ale trochę mało wyrafinowane. Na przykład nie zapobiega załadowaniu czegoś dwa razy. Pora na inne rozwiązania.

Moduły i biblioteki

Pakiety obsługują przestrzenie nazw. Ale bardzo często programy są podzielone na niezależnie ładowane części, nierzadko pochodzące od innych twórców. W językach niskopoziomowych używa się terminu biblioteki. W Lispie jakoś nie, chyba że przy łączeniu z innymi językami programowania.

Początkowo używano terminu moduł. Jest to luźno zdefiniowane pojęcie i oznacza podsystem składający się z jednego lub więcej plików, który może być niezależnie załadowany.

Zmienna globalna *modules* zawiera listę nazw modułów załadowanych do bieżącego obrazu Lispu (,,świata Lispu''). Pozwala unikać wielokrotnego ładowania modułu.

Do obsługi modułów służa funkcje require i provide

Po wywołaniu provide podana nazwa modułu jest dodawana do zmiennej *modules*.

Po wywołaniu require szuka się podanej nazwy na liscie modułów. Jeśli jej tam nie ma, to moduł jest ładowany, ale jego nazwa nie jest dodawana do listy modułów -- to trzeba zrobić przez provide.

Mechanizm ten, choć standardowy, jest obecnie rzadko używany, zwłaszcza że bogatsze mechanizmy używające defsystem (np. make i asdf) przejmują te funkcje. Ale warto o nim wiedzieć.

defsystem

Konstrukcja defsystem powstała dawno temu na Lisp Maszynach. Mniej więcej odpowiada Makefile, które znacie z Unixa. Oryginalny defsystem był bardzo mocny, pozwalał na przykład powiedzieć ,,należy załadować plik A zanim się skompiluje plik B'' (zapewne A zawierał makra).

Obecnie istnieją dwa narzędzia używające defsystem. Starszy make używa plików z rozszerzeniem .system, bardziej popularny obecnie ASDF (dostępny w prawie każdej implementacji) używa plików .asd. W składni i możliwościach są drobne różnice. Pomówimy o ASDF.

Chcąc go użyć do naszego programu, po pierwsze musimy napisać plik definicyjny, na przykład pgsql.asd.

(in-package :cl-user)

(defsystem :pgsql
  :serial t
  :components ((:file "pg-package")
               (:file "bind-libpq")
               (:file "sql")))

Podajemy najpierw nazwę systemu/biblioteki (:pgsql). Po niej idą opcje. Opcja :serial pozwala zaoszczędzić na pisaniu, jeśli kolejny składnik zależy od porzednich. Najważniejsza jest opcja :components.

Podaje się, z jakich komponentów składa się system. Mogą to być moduły (:module, tutaj brak) albo pliki. Struktura jest hierarchiczna, moduły mogą zawierać kolejne moduły, ale jako liście występują pliki.

ASDF sprawdzi przy ładowaniu , czy system jest załadowany oraz czy jego komponenty są aktualne. Następnie doładuje to, co trzeba. Ładujemy przez

(asdf:load-system nazwa)
na przykład
(asdf:load-system :pgsql)

Oczywiście trzeba wcześniej załadować ASDF, np.

(require :asdf)

Zamiast tego można na początku pliku napisać

(require :pgsql)

No tak, ale skąd ASDF wie, gdzie jest plik z definicją. Najprościej załadować go ręcznie

(load "/home/zbyszek/lisp/pgsql.asd")

ASDF będzie poszukiwał plików w tym katalogu, z którego załadował plik z definicjami.

Inny sposób to dodać ścieżke do katalogu z plikiem definicyjnym do globalnej zmiennej asdf:*central-registry*, na przykład

(push "/home/zbyszek/lisp/" asdf:*central-registry*)

Przy wielu bibiotekach powoduje to rozrost tej listy, więc lepiej utworzyć osobny katalog i w nim trzymać linki symboliczne do wszystkich definicji bibliotek (,,link farm'').

Trzeci sposób lubiany przez obecnych maintainerów to skomplikowana struktura plików konfiguracyjnych. Niestety nie wszystkie wersje systemow operacyjnych chcą do niej pasować, poza tym opłaca się tylko przy wielu bibliotekach.

Cudze biblioteki

W sieci znajduje się mnóstwo ogólnych i specjalizowanych bibliotek Common Lispu. Spróbujemy poszukać jakiejś biblioteki do gniazdek. Można by użyć Google, ale po co. Główne informatorium CL to w tej chwili CLiki. Jedna z dwóch zalecanych bibliotek to USOCKET, jej strona to common-lisp.net/project/usocket. Wchodzimy w releases i ściagamy najnowszą wersję.

Po rozpakowaniu oglądamy plik usocket.asd

(in-package :asdf)

(defsystem usocket
    :name "usocket (client)"
    :author "Erik Enge & Erik Huelsmann"
    :maintainer "Chun Tian (binghe) & Hans Huebner"
    :version "0.7.0.1"
    :licence "MIT"
    :description "Universal socket library for Common Lisp"
    :depends-on (#+(or sbcl ecl) :sb-bsd-sockets
                 :split-sequence)
    :components ((:file "package")
		 (:module "vendor" :depends-on ("package")
		  :components (#+mcl (:file "kqueue")
			       #+mcl (:file "OpenTransportUDP")))
		 (:file "usocket" :depends-on ("vendor"))
		 (:file "condition" :depends-on ("usocket"))
		 (:module "backend" :depends-on ("condition")
		  :components (#+abcl		(:file "abcl")
			       #+(or allegro cormanlisp)
						(:file "allegro")
			       #+clisp		(:file "clisp")
			       #+clozure	(:file "clozure" :depends-on ("openmcl"))
			       #+cmu		(:file "cmucl")
			       #+ecl		(:file "ecl" :depends-on ("sbcl"))
			       #+lispworks	(:file "lispworks")
			       #+mcl		(:file "mcl")
			       #+mocl		(:file "mocl")
			       #+openmcl	(:file "openmcl")
			       #+(or ecl sbcl)	(:file "sbcl")
			       #+scl		(:file "scl")))
		 (:file "option" :depends-on ("backend"))))

(defmethod perform ((op test-op) (c (eql (find-system :usocket))))
  (oos 'load-op :usocket-server)
  (oos 'load-op :usocket-test)
  (oos 'test-op :usocket-test))

i stwierdzamy, że potrzebna jest biblioteka split-sequence (reszty nie musimy na razie rozumieć, ważne jest tylko :depends-on). Ściągamy ją w podobny sposób (zapewne z cl-utilities).

Może to być męczące, gdy zależności jest więcej, dlatego lepiej użyć Quicklispa.

Mamy wszystko, możemy próbować. Przykładowy plik z serwerem TCP nie przypomina tego z sieci, ale robi to samo. Na początku widać (require "usocket"). Potem definiujemy własny pakiet. No to testujmy

* (require :usocket)
NIL
* (load "tcp-echo-server")
T
* (echo:echo-server 3344)
Got message...
Got message...
  C-c C-c

Jako klienta odpaliłem najpierw

zbyszek@red04:~/public_html/lisp/lab$ telnet localhost 3344
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
to ja
You said: to ja
Connection closed by foreign host.

Drugi klient był już w Lispie

* (require :usocket)
NIL
* (load "tcp-echo-server")
T
* (echo:echo-send "to znowu ja" 3344)
"You said: to znowu ja"

Quicklisp

Mechanizm Quicklisp to centralne repozytorium modułów i bibliotek Common Lispu. Dodatkowe informacje na CLiki

Zaczynamy od instalacji. Ja robię to ściągając "quicklisp.lisp" do katalogu ~/quicklisp, ale można gdziekolwiek, po czym w SBCL

* (load "quicklisp.lisp")
...
* (quicklisp-quickstart:install)
...

Uwaga: Quicklisp ma własną wersję ASDF i tak ma podobno być.

Potem można już używać go, na początku sesji robiąc na przykład

(load "~/quicklisp/setup.lisp")

Recepty:

Prefiks ql: to nazwa pakietu Quicklispa, można by zrobić (use-package :cl), ale właściwie po co.

Co pewien czas być może warto zrobić (ql:update-dist "quicklisp") przed ql:quickload, żeby zaktualizować biblioteki.

Spróbujmy użyć Quicklispa do zgłębienia tematu gniazdek w Lispie (tych sieciowych)

 * (ql:system-apropos "socket")
#<SYSTEM clack-socket / clack-20191007-git / quicklisp 2020-04-27>
#<SYSTEM clsql-postgresql-socket / clsql-20160208-git / quicklisp 2020-04-27>
#<SYSTEM clsql-postgresql-socket3 / clsql-20160208-git / quicklisp 2020-04-27>
#<SYSTEM fast-websocket / fast-websocket-20190813-git / quicklisp 2020-04-27>
#<SYSTEM fast-websocket-test / fast-websocket-20190813-git / quicklisp 2020-04-27>
#<SYSTEM hu.dwim.web-server.websocket / hu.dwim.web-server-20200427-darcs / quicklisp 2020-04-27>
#<SYSTEM hunchensocket / hunchensocket-20180711-git / quicklisp 2020-04-27>
#<SYSTEM hunchensocket-tests / hunchensocket-20180711-git / quicklisp 2020-04-27>
#<SYSTEM iolib/sockets / iolib-v0.8.3 / quicklisp 2020-04-27>
#<SYSTEM iolib/trivial-sockets / iolib-v0.8.3 / quicklisp 2020-04-27>
#<SYSTEM trivial-sockets / trivial-sockets-20190107-git / quicklisp 2020-04-27>
#<SYSTEM usocket / usocket-0.8.3 / quicklisp 2020-04-27>
#<SYSTEM usocket-server / usocket-0.8.3 / quicklisp 2020-04-27>
#<SYSTEM usocket-test / usocket-0.8.3 / quicklisp 2020-04-27>
#<SYSTEM websocket-driver / websocket-driver-20190107-git / quicklisp 2020-04-27>
#<SYSTEM websocket-driver-base / websocket-driver-20190107-git / quicklisp 2020-04-27>
#<SYSTEM websocket-driver-client / websocket-driver-20190107-git / quicklisp 2020-04-27>
#<SYSTEM websocket-driver-server / websocket-driver-20190107-git / quicklisp 2020-04-27>

Sporo tego... Ale nas interesuje usocket.

* (ql:quickload "usocket")
To load "usocket":
  Load 1 ASDF system:
    usocket
; Loading "usocket"

("usocket")
* (load "~/tcp-echo-server")
T
* (echo:echo-server 3344)
Got message...
Got message...

Lekka zmiana w drugim kliencie

* (load "~/quicklisp/setup")
T
* (require :usocket)
NIL
* (load "tcp-echo-server")
T
* (echo:echo-send "to znowu ja" 3344)
"You said: to znowu ja"

Nie ustawiłem automatycznego odpalania Quicklispa, więc robię to ręcznie.