Projekty w AlexScript
AlexScript to język zaprojektowany do pisania prawdziwych programów, nie tylko zabawkowych przykładów — i właśnie po to istnieją te trzy projekty, żeby to pokazać. Każdy z nich to napisana w całości w AlexScript aplikacja licząca po kilka tysięcy linii, wykorzystująca OOP, moduły, asynchroniczność, obsługę wyjątków i bibliotekę standardową. Są użyteczne same w sobie (interpreter, framework webowy, klient HTTP) i jednocześnie pełnią rolę praktycznych przykładów tego, jak AlexScript radzi sobie poza poziomem tutoriala.
Zajrzyj do kodu źródłowego, jeśli ciekawi Cię, jak organizować wieloplikowy projekt w AlexScript — wszystkie trzy korzystają z modułów, dzielą kod na osobne pliki według odpowiedzialności i trzymają się spójnych konwencji nazewniczych. Każdy projekt ma link do swojego repozytorium na końcu sekcji.
BASIC — interpreter BASIC-a w stylu Altaira
asbasic to kompletny interpreter języka BASIC napisany w AlexScript — z numerami linii, GOTO, GOSUB/RETURN, FOR/NEXT, DEF FN, DATA/READ/RESTORE, całym wyposażeniem. Uruchamiasz go z terminala, dostajesz znak zachęty >, wpisujesz programy z numerami linii i odpalasz je komendą RUN. Programy zapisują się i wczytują z plików tekstowych. Klimat prosto z instrukcji obsługi Altaira.
Projekt pokazuje przy okazji, że AlexScript radzi sobie z zadaniami, do których ludzie zwykle sięgają po Ruby albo Pythona: tokenizer, parser, interpreter i interaktywna powłoka — wszystko w przejrzystej strukturze modułów.
Co obsługuje
Pełen zestaw klasycznych instrukcji BASIC-a:
- Wyjście i wejście —
PRINTz separatorami przecinkowymi i średnikowymi orazTAB(n),INPUTz opcjonalnym promptem i odczytem wielu wartości naraz. - Przypisanie —
LET(lub bez słowa kluczowego), zmienne skalarne, tablice (A(I)) i tablice wielowymiarowe (GRID(I, J)). - Sterowanie przepływem —
IF / THEN / ELSE,GOTO,FOR / NEXTz opcjonalnymSTEP,GOSUB/RETURN,END. - Tablice —
DIMz 0-indeksowaniem włącznym:DIM A(10)daje 11 slotówA(0)..A(10). Tablice wielowymiarowe są wspierane. - Funkcje użytkownika —
DEF FN NAZWA(args) = wyrazeniedla jednolinijkowych funkcji, łącznie z wywołaniami rekurencyjnymi i parametrami przesłaniającymi globalne na czas wywołania. - Tablice danych — deklaracje
DATAzbierane z dowolnego miejsca w programie podczas wstępnego przebiegu, konsumowane sekwencyjnie przezREAD, przewijalne komendąRESTORE. - Komentarze —
REMw osobnej linii albo na końcu, plus:do łączenia wielu instrukcji w jednej linii.
Funkcje wbudowane pokrywają to, czego można się spodziewać: ABS, SGN, SQR, INT, LOG, EXP, rodzina funkcji trygonometrycznych, RND do liczb losowych, LEN, STR$, CHR$, VAL, LEFT$/RIGHT$/MID$ do operacji na napisach.
Komendy REPL
| Komenda | Działanie |
|---|---|
RUN |
uruchom zapisany program |
LIST |
wyświetl zapisany program |
NEW |
wyczyść zapisany program |
CLEAR |
wyczyść ekran |
SAVE "plik" |
zapisz program na dysku |
LOAD "plik" |
wczytaj program z dysku |
HELP |
pokaż listę komend |
QUIT |
wyjście |
Linia zaczynająca się od liczby trafia do bufora programu; samo wpisanie liczby usuwa odpowiednią linię; cokolwiek innego jest traktowane jako natychmiastowe polecenie i wykonywane na miejscu.
Uruchamianie programu
Uruchom interpreter:
alexscript basic.as
AlexScript Altair Basic 0.0.1
Type 'HELP' for available commands
>
Wpisz program (zauważ numery linii — tak właśnie BASIC go przechowuje):
> 10 PRINT "Calculate factorial"
> 20 INPUT "Enter a number: "; N
> 30 LET FACT = 1
> 40 FOR I = 1 TO N
> 50 LET FACT = FACT * I
> 60 NEXT I
> 70 PRINT N; "! ="; FACT
> RUN
Calculate factorial
Enter a number: ? 5
5 ! = 120
DATA i READ działają tak, jak można się tego spodziewać po klasycznym BASIC-u:
10 DATA "Alice", 30, "Bob", 25
20 READ NAME$, AGE
30 PRINT NAME$, AGE
40 READ NAME$, AGE
50 PRINT NAME$, AGE
60 RESTORE
70 READ NAME$
80 PRINT NAME$
Ten kod wypisze najpierw dane Alice, potem Boba, a po tym, jak RESTORE przewinie wskaźnik danych — znów dane Alice.
Funkcje zdefiniowane przez użytkownika dobrze się komponują:
10 DEF FN SQUARE(X) = X * X
20 DEF FN HYP(A, B) = SQR(FN SQUARE(A) + FN SQUARE(B))
30 PRINT FN HYP(3, 4)
Wypisze: 5.
Notki architektoniczne
Kod jest podzielony na sześć plików AlexScript: basic.as (punkt wejścia), repl.as (powłoka interaktywna), interpreter.as (wykonywanie instrukcji), parser.as (parsowanie wyrażeń i dispatch funkcji wbudowanych), lexer.as (tokenizer) i runtime.as (współdzielony stan interpretera).
Kilka konkretnych decyzji wartych odnotowania:
- Klasa
Runtimetrzyma cały zmienny stan interpretera — bufor programu, zmienne, tablice, stosy FOR/GOSUB, pulę DATA, flagi. Nie ma żadnych globalnych zmiennych krążących między modułami. Każdy komponent przyjmuje instancjęRuntimei na niej operuje. Sprawia to, że kod jest znacznie łatwiejszy do ogarnięcia niż byłby odpowiednik interpretera BASIC-a oparty na stanie modułowym. - Posortowany indeks linii. Równoległa, posortowana tablica liczb całkowitych — numerów linii — żyje obok bufora programu, dzięki czemu
RUNiGOTOnie płacą za sortowanie w każdej iteracji. - Dwuprzebiegowe wykonanie programu. Pierwszy przebieg skanuje deklaracje
DATAi buduje pulę danych; drugi uruchamia program normalnie. LinieDATAsą no-opami w trybie normalnym — i właśnie dlatego mogą pojawić się w dowolnym miejscu kodu źródłowego. - Lekser oparty na indeksie. Zamiast modyfikować tablicę znaków (typowy idiom Ruby’ego), lekser trzyma kursor
positionna napisie źródłowym. Ta sama semantyka, zero alokacji na token.
Repozytorium
Zubr — mikro framework webowy
zubr to framework HTTP w stylu Sinatry dla AlexScript. Tworzysz serwer, rejestrujesz trasy, opcjonalnie dodajesz middleware i wywołujesz start(). Reszta — parsowanie HTTP/1.1, routing, dekodowanie ciała żądania, budowanie odpowiedzi i cykl życia połączenia — należy do frameworka.
Od ręki pokrywa to, czego potrzebuje każdy realny serwis webowy — JSON API, statyczne pliki, sesje, ciasteczka, CORS, ograniczanie częstości żądań — bez żadnych zewnętrznych zależności. Korzysta wyłącznie z bibliotek standardowych dołączonych do AlexScript (socket, json, czas, digest, securerandom, plik, http).
Co obsługuje
Najważniejsze możliwości:
- Serwer HTTP/1.1 z keep-alive, konfigurowalnymi limitami czasu i graceful shutdown.
- Routing — ścieżki statyczne (
/users), parametryczne (/users/:id), wieloznacznikowe (/static/*) oraz pełne wzorce regex. - Łańcuch middleware — komponowalny pipeline z wbudowanym loggerem, CORS-em, rate limiterem i podpisanymi sesjami.
- Parsowanie ciała żądania automatycznie wybierane na podstawie
Content-Type— JSON pojawia się jako obiekt AlexScript,form-urlencodedjako hash z polami, cokolwiek innego jako surowy napis. - API ciasteczek z pełnym wsparciem atrybutów:
HttpOnly,Secure,SameSite,Max-Age,Path,Domain. - Sesje — ciasteczka podpisywane HMAC z magazynem in-memory, konfigurowalna nazwa ciasteczka i TTL.
- Serwowanie plików statycznych z ETag, Last-Modified, warunkowym GET (304 Not Modified) i ochroną przed atakami typu path traversal.
- Strumieniowane odpowiedzi zużywające stałą pamięć niezależnie od rozmiaru pliku.
- Negocjacja zawartości — parsowanie nagłówka
Accept:z wartościami q i wieloznacznikami typów. - Obsługa metod HTTP — automatyczne wsparcie
HEAD,405 Method Not Allowedz poprawnym nagłówkiemAllow:dla nieobsługiwanych metod.
Hello world
Minimalny serwer Zubra:
import("../lib/zubr")
niech serwer = Zubr::Serwer.nowy(8080)
serwer.get("/", fn(zad) {
zwroc Zubr::Odpowiedz.tekst(200, "Hello, World!\n")
})
serwer.start()
To cały plik. Jego uruchomienie startuje serwer HTTP na porcie 8080, który na GET / odpowiada zwykłym tekstem.
Routing i odpowiedzi JSON
Segmenty parametryczne używają składni :nazwa — wyłapana wartość trafia do zad.parametry():
serwer.get("/json", fn(zad) {
zwroc Zubr::Odpowiedz.json(200, {
"message": "Hello",
"timestamp": Czas.stempel()
})
})
serwer.get("/users/:id", fn(zad) {
zwroc Zubr::Odpowiedz.json(200, {
"id": zad.parametry()["id"]
})
})
serwer.post("/echo", fn(zad) {
niech dane = zad.dane()
jesli dane == nic to dane = {}
zwroc Zubr::Odpowiedz.json(200, dane)
})
Handler to funkcja, która przyjmuje żądanie (zad, skrót od „zadanie”) i zwraca odpowiedź (Odpowiedz). Klasa Odpowiedz ma fabryki na każdą typową sytuację — tekst, json, html, plik, przekieruj, brak_zawartosci (204 No Content). Każdy handler Zubra zwraca jedną z nich.
Middleware
Middleware rejestruje się przez serwer.middleware(...). Standardowe klasy middleware są dołączone do frameworka:
serwer.middleware(Zubr::Middleware::Log::standardowy())
serwer.middleware(Zubr::Middleware::CORS::pozwol("*"))
serwer.middleware(Zubr::Middleware::RateLimit::na_ip(60, 60))
serwer.middleware(Zubr::Middleware::Sesja::standardowa("change-me-please-make-this-long"))
Kolejność ma znaczenie — pierwszy zarejestrowany działa najbardziej zewnętrznie, ostatni najbardziej wewnętrznie (najbliżej handlera). Middleware sesji zwykle powinien być na końcu, bo handlery chcą go odczytywać przez zad.sesja().
API sesji jest proste:
serwer.post("/login", fn(zad) {
zad.sesja().ustaw("user_id", 42)
zwroc Zubr::Odpowiedz.tekst(200, "Logged in\n")
})
serwer.get("/me", fn(zad) {
niech uid = zad.sesja().pobierz("user_id")
jesli uid == nic to zwroc Zubr::Odpowiedz.tekst(401, "Login required\n")
zwroc Zubr::Odpowiedz.tekst(200, "User " + uid.napis() + "\n")
})
Ciasteczka są podpisywane HMAC z użyciem sekretu, który przekazujesz do middleware. Manipulacja po stronie klienta jest niemożliwa bez znajomości tego klucza.
Realny przykład: API notatek
Żeby pokazać, jak to wygląda w rzeczywistych aplikacjach — fragment przykładowej aplikacji notes, moduł obsługujący CRUD na notatkach przypisanych do użytkownika:
modul App {
modul Trasy {
modul Notatki {
funkcja zarejestruj(serwer) {
serwer.get("/api/notatki",
App::AppMiddleware::wymagaj_loginu(fn(zad) {
zwroc obsluz_liste(zad)
})
)
serwer.post("/api/notatki",
App::AppMiddleware::wymagaj_loginu(fn(zad) {
zwroc obsluz_utworz(zad)
})
)
# ... PUT, DELETE, GET-po-id rejestrują się analogicznie
}
prywatna funkcja obsluz_liste(zad) {
niech uid = zad.sesja().pobierz("uzytkownik_id")
niech mag = App::Modele::magazyn()
niech notatki = mag.notatki_uzytkownika(uid, "", "")
# ... budowanie obiektu odpowiedzi
zwroc Zubr::Odpowiedz.json(200, { "notatki": tablica })
}
}
}
}
Ten styl — moduł na grupę tras, funkcja zarejestruj(serwer), która podpina trasy do serwera, prywatne funkcje obsługujące poszczególne metody HTTP — dobrze się skaluje i pozostaje czytelny w miarę rozrastania się aplikacji. Pełna wersja przykładu notes ma osobne moduły dla tras, modeli, walidatorów i middleware.
Wydajność
Benchmarki na MacBooku Pro z 2024 roku:
| Workload | Przepustowość | p50 | p99 |
|---|---|---|---|
| Hello world, 10 połączeń | 2 150 req/s | 0,97 ms | 354 ms |
| Hello world, 50 połączeń | 1 590 req/s | 0,95 ms | 1,39 s |
| Routing + 3 middleware | 1 540 req/s | 1,2 ms | 14 ms |
| Plik statyczny 50 MB, 10 równolegle | 1,97 GB/s | — | 282 ms |
| Plik statyczny 50 MB, ciągłe obciążenie | stałe ~100 MB RSS | — | — |
Przepustowość jest ograniczona głównie przez interpreter AlexScript i GVL Ruby’ego, ale dla obciążeń, do jakich AlexScript faktycznie się dziś wdraża — wewnętrznych API, prototypów, małych usług — zapasu jest aż nadto.
Architektura
Zubr jest podzielony między lib/ (rdzeń frameworka: parser, router, builder odpowiedzi, handler połączeń, sama klasa Serwer) i middleware/ (dołączone moduły middleware). Serwer używa dispatchu wątek-na-połączenie — każde zaakceptowane połączenie TCP biegnie w osobnym wątku Ruby, parsuje żądanie, przepuszcza je przez łańcuch middleware i wypisuje odpowiedź.
Jest to prostsze i bardziej niezawodne niż podejście oparte na fiberach asynchronicznych przy obecnym stanie runtime’u AlexScript — kosztem ograniczenia przepustowości do tego, co pozwala GVL Ruby’ego. W praktyce sprawdza się dobrze — dołączona aplikacja demo notes chodzi stabilnie pod ciągłym obciążeniem z izolacją danych między użytkownikami.
Status: beta. API jest stabilne, ale implementacja jeszcze świeża.
Repozytorium
Posel — wysokopoziomowy klient HTTP
posel jest dla natywnej biblioteki Http tym, czym Axios dla fetch albo HTTParty dla Ruby’owego Net::HTTP: wysokopoziomowym opakowaniem, które dorzuca to, czego naprawdę potrzebują rzeczywiste aplikacje — konfigurowalne instancje klienta z domyślnymi nagłówkami, interceptory żądań i odpowiedzi, otypowaną hierarchię wyjątków oraz API asynchroniczne wygodne do równoległej pracy.
Do jednorazowych żądań możesz wywoływać moduł bezpośrednio. Do czegokolwiek bardziej rozbudowanego budujesz Posel::Klient skonfigurowany z bazowym URL-em, domyślnymi nagłówkami, limitem czasu i tak dalej.
import("./posel/posel")
# Jednorazowe żądanie
niech user = Posel::get_json("https://jsonplaceholder.typicode.com/users/1")
pokazl user["name"]
# Skonfigurowany klient
niech api = Posel::Klient.nowy({
"bazowy_url": "https://api.example.com/v1",
"naglowki": { "Authorization": "Bearer xyz123" },
"limit_czasu": 10
})
niech users = api.get_json("/users")
Co obsługuje
- Wszystkie metody HTTP —
get,post,put,patch,delete,head,options. - Wygodne metody JSON —
get_json,post_jsonitd., z automatyczną serializacją, parsowaniem i nagłówkami. - Konfigurowalne instancje klienta z domyślnymi nagłówkami, bazowym URL-em, parametrami zapytania, limitem czasu i limitem przekierowań — każde nadpisywalne dla pojedynczego wywołania.
- Otypowana hierarchia wyjątków — łap
BladNieZnalezionodla 404,BladHttpKlientadla każdego 4xx,BladPosladla wszystkiego, co pochodzi z posła. - Interceptory żądań i odpowiedzi — łańcuch middleware w stylu Axiosa (FIFO dla żądań, LIFO dla odpowiedzi).
- Asynchroniczne warianty każdej metody — prawdziwe równoległe I/O przez
uruchom_rownolegleiObietnica.wszystkie. - Fasada modułowa dla jednorazowych żądań:
Posel::get(...)całkowicie pomija budowanie klienta.
Konfiguracja
Posel::Klient budujemy z hasha konfiguracyjnego:
| Klucz | Typ | Domyślnie | Opis |
|---|---|---|---|
bazowy_url |
napis | nic |
Doklejany do ścieżek względnych. Pełne URL-e w wywołaniach go nadpisują. |
naglowki |
hash | {} |
Domyślne nagłówki. Łączone z nagłówkami per-call (per-call wygrywa). |
parametry |
hash | {} |
Domyślne parametry query. Łączone z parametrami per-call. |
limit_czasu |
całkowita | 30 |
Limit czasu w sekundach. |
max_przekierowan |
całkowita | 5 |
Maksymalna liczba przekierowań do podążania. |
rzucaj_bledy |
bool | prawda |
Jeśli prawda, rzuca otypowane wyjątki na 4xx/5xx. |
Każda metoda przyjmuje też opcjonalny ostatni hash opcje, który nadpisuje konfigurację klienta dla tego jednego wywołania:
api.get("/users", {
"naglowki": { "X-Request-ID": "abc" },
"parametry": { "limit": "10", "offset": "0" },
"limit_czasu": 5,
"rzucaj_bledy": falsz
})
Obiekt odpowiedzi
Metody nie-JSON-owe zwracają Posel::Odpowiedz:
niech odp = api.get("/users/1")
odp.status() # 200
odp.cialo() # surowe ciało odpowiedzi
odp.json() # sparsowany JSON, leniwie buforowany
odp.naglowek("content-type") # nieczułe na wielkość liter
odp.naglowki() # wszystkie nagłówki, klucze małymi literami
odp.czy_sukces() # 2xx
odp.czy_blad_klienta() # 4xx
odp.czy_blad_serwera() # 5xx
odp.czy_blad() # 4xx lub 5xx
Warianty *_json pomijają wrapper i zwracają sparsowane ciało bezpośrednio:
niech user = api.get_json("/users/1") # już hash, nie Odpowiedz
pokazl user["name"]
Otypowane wyjątki
Kiedy rzucaj_bledy jest włączone (domyślnie), błędy HTTP stają się otypowanymi wyjątkami AlexScript. Hierarchia pozwala łapać szeroko albo wąsko:
WyjatekPodstawowy
└── BladPosla # wszystko z posła
├── BladSieci # connection refused, DNS, reset
├── BladTimeoutu # żądanie przekroczyło limit czasu
├── BladSerializacji # parsowanie JSON-a nie powiodło się
└── BladHttp # status >= 400
├── BladHttpKlienta # 4xx
│ ├── BladZleZapytanie # 400
│ ├── BladNieautoryzowany # 401
│ ├── BladBrakDostepu # 403
│ ├── BladNieZnaleziono # 404
│ ├── BladKonfliktu # 409
│ └── BladPrzeciazenia # 429
└── BladHttpSerwera # 5xx
├── BladWewnetrzny # 500
├── BladBramy # 502
├── BladNiedostepny # 503
└── BladTimeoutuBramy # 504
W praktyce:
proba {
api.get_json("/users/9999")
} zlap (e : Posel::BladNieZnaleziono) {
pokazl "User not found"
} zlap (e : Posel::BladHttpKlienta) {
pokazl "Other 4xx: " + e["wiadomosc"]
} zlap (e : Posel::BladSieci) {
pokazl "Network problem: " + e["wiadomosc"]
} zlap (e : Posel::BladPosla) {
pokazl "Anything else from posel"
}
Jeśli wolisz samodzielnie zerknąć na odpowiedź, ustaw rzucaj_bledy: falsz na poziomie wywołania i sprawdź odp.czy_blad().
Interceptory
Interceptory to „middleware” klienta HTTP — lambdy, które widzą każde przepływające żądanie i odpowiedź. Przydają się do logowania, dorzucania nagłówków uwierzytelniających, automatycznego odświeżania tokena na 401, mierzenia czasu żądań, identyfikatorów żądań i tym podobnych spraw.
# Logger
api.dodaj_interceptor_zapytania(fn(zap) {
pokazl "→ " + zap.metoda() + " " + zap.url()
zwroc zap
})
api.dodaj_interceptor_odpowiedzi(fn(odp) {
pokazl "← " + odp.status() + " " + odp.zapytanie().url()
zwroc odp
})
Bardziej rozbudowany przykład — pomiar czasu żądania z użyciem slotu meta() do komunikacji między interceptorami:
api.dodaj_interceptor_zapytania(fn(zap) {
zap.ustaw_meta("start", Czas.teraz().timestamp_f())
zwroc zap
})
api.dodaj_interceptor_odpowiedzi(fn(odp) {
niech ms = (Czas.teraz().timestamp_f() - odp.zapytanie().meta("start")) * 1000
pokazl odp.zapytanie().url() + " took " + ms + "ms"
zwroc odp
})
Interceptory żądań biegną w kolejności rejestracji (FIFO); interceptory odpowiedzi biegną w odwrotnej kolejności (LIFO), więc ostatnio zarejestrowany jest najbliżej sieci.
Asynchroniczność i prawdziwa równoległość
Każda synchroniczna metoda ma asynchroniczny odpowiednik z sufiksem _async, który zwraca Obietnice:
asynchroniczna funkcja main() {
niech user = czekaj api.get_json_async("/users/1")
pokazl user["name"]
}
uruchom(main)
Warto zapamiętać jedną rzecz: czekaj na kilku obietnicach po kolei to wciąż sekwencyjne wykonanie — każde czekaj blokuje bieżący fiber, dopóki ta jedna obietnica się nie rozstrzygnie. Żeby uruchomić żądania współbieżnie, owiń każde w uruchom_rownolegle i połącz wynikowe obietnice przez Obietnica.wszystkie:
asynchroniczna funkcja pobierz_wszystkich() {
niech a = uruchom_rownolegle(fn() { czekaj api.get_json_async("/users/1") })
niech b = uruchom_rownolegle(fn() { czekaj api.get_json_async("/users/2") })
niech c = uruchom_rownolegle(fn() { czekaj api.get_json_async("/users/3") })
niech wyniki = czekaj Obietnica.wszystkie([a, b, c])
zwroc wyniki
}
uruchom(pobierz_wszystkich)
Teraz trzy żądania biegną współbieżnie — całkowity czas to mniej więcej tyle, ile zajmuje najwolniejsze pojedyncze żądanie, a nie suma wszystkich. Scheduler fiberów AlexScript zawiesza każde żądanie na I/O socketu i pozwala innym posuwać się dalej.
Wyjątki asynchroniczne działają tak samo jak synchroniczne: czekaj ponownie rzuca powód odrzucenia jako wyjątek AlexScript, więc otypowana hierarchia wyjątków ma zastosowanie również w kodzie asynchronicznym.
Architektura
Kod jest zorganizowany jako mały moduł:
posel/
├── posel.as # punkt wejścia + fasada modułu
├── klient.as # klasa Klient — metody sync + async, interceptory
├── zapytanie.as # Zapytanie — mutowalny obiekt żądania w pipeline
├── odpowiedz.as # Odpowiedz — wrapper odpowiedzi z leniwym JSON-em
├── pipeline.as # runner łańcucha interceptorów
├── url.as # helpery do łączenia URL-i i query string
└── bledy.as # hierarchia wyjątków
Żądanie przepływa przez pipeline tak: klient buduje Zapytanie z konfiguracji i opcji per-call, interceptory żądań biegną w kolejności FIFO, wykonywane jest właściwe wywołanie Http::* (błędy sieciowe są tłumaczone na otypowane wyjątki), surowa odpowiedź zostaje opakowana w Odpowiedz, interceptory odpowiedzi biegną w kolejności LIFO, i wreszcie — jeśli rzucaj_bledy jest włączone i status to ≥ 400 — rzucany jest pasujący BladHttp....
Dlaczego Zapytanie jest klasą, a nie hashem? Interceptory muszą je modyfikować (nagłówki, parametry zapytania, ciało, nawet URL). Klasa z nazwanymi akcesorami sprawia, że interceptory są czytelne, a literówki są wyłapywane w miejscu wywołania, a nie gdzieś w środku produkcyjnej pętli ponownych prób.
Repozytorium
Algorytmy i struktury danych
Główne repozytorium AlexScript zawiera katalog examples/ z małymi, samowystarczalnymi programami — klasycznymi algorytmami, strukturami danych i wzorcami projektowymi zaimplementowanymi w idiomatycznym AlexScript. Warto je przeczytać z dwóch powodów: jako sprawdzenie, jak cechy języka komponują się w prawdziwym kodzie (rekurencja, domknięcia, klasy, generyczność po duck-typingu), oraz jako punkt wyjścia, jeśli wolisz skopiować działającą implementację, zamiast pisać własną od zera.
Kolekcja pokrywa standardowy zakres podręcznika algorytmów — sortowanie, wyszukiwanie, hashowanie, podstawowe przeszukiwanie grafów — plus garść obiektowych przykładów ilustrujących typowe wzorce projektowe. Żaden z nich nie jest kodem frameworkowym; to zwykłe pliki .as, które możesz uruchomić bezpośrednio.
Poniżej kilka reprezentatywnych próbek.
Sortowanie bąbelkowe
Czysta implementacja pokazująca indeksowanie tablicy, zagnieżdżone numeryczne pętle dla i klasyczną trzylinijkową zamianę:
funkcja bubble_sort(tablica) {
niech n = tablica.dlg
dla niech k = 0; n - 1; 1 {
dla niech j = 0; n - k - 1; 1 {
jesli tablica[j] > tablica[j + 1] {
niech tmp = tablica[j]
tablica[j] = tablica[j + 1]
tablica[j + 1] = tmp
}
}
}
zwroc tablica
}
Zauważ, że tablica[j], tablica[j + 1] = tablica[j + 1], tablica[j] byłoby zwięźlejsze w niektórych językach — AlexScript nie ma przypisania wielokrotnego, więc jawna zamiana przez tmp jest idiomatyczna.
Wyszukiwanie binarne
Standardowa iteracja połowiąca przestrzeń wyszukiwania na posortowanej tablicy. Obliczenie środka zabezpiecza przed dryfem między typem całkowitym a zmiennoprzecinkowym, sprawdzając typ i w razie potrzeby zaokrąglając:
funkcja binary_search(tablica, szukana) {
niech lewy = 0
niech prawy = tablica.dlg - 1
dopoki lewy <= prawy {
niech srodek = (lewy + prawy) / 2
srodek = srodek.typ() == 'calkowita' ? srodek : srodek.zaokragl()
jesli tablica[srodek] == szukana {
zwroc srodek
} albojesli tablica[srodek] < szukana {
lewy = srodek + 1
} albo {
prawy = srodek - 1
}
}
zwroc -1
}
Zwraca -1 dla „nie znaleziono” — konwencjonalna wartość specjalna oznaczająca brak.
Wzorzec Singleton
Podręcznikowy Singleton, ilustrujący statyczne pola klasy, strzeżony konstruktor i akcesor pobierz(), który leniwie tworzy instancję przy pierwszym użyciu:
klasa KonfiguracjaAplikacji {
statyczny niech instancja = nic
statyczny niech zainicjalizowana = falsz
funkcja konstruktor() {
jesli KonfiguracjaAplikacji.zainicjalizowana {
rzuc "Nie można tworzyć instancji Singleton! Użyj KonfiguracjaAplikacji.pobierz()"
}
niech @ustawienia = {}
niech @wersja = "1.0.0"
niech @ustawienia["baza_danych"] = "localhost"
niech @ustawienia["port"] = 5432
niech @ustawienia["timeout"] = 30
}
statyczny funkcja pobierz() {
jesli KonfiguracjaAplikacji.instancja == nic {
niech KonfiguracjaAplikacji.zainicjalizowana = prawda
niech KonfiguracjaAplikacji.instancja = KonfiguracjaAplikacji.nowy()
niech KonfiguracjaAplikacji.zainicjalizowana = falsz
}
zwroc KonfiguracjaAplikacji.instancja
}
funkcja ustaw(klucz, wartosc) {
@ustawienia[klucz] = wartosc
}
funkcja pobierz_ustawienie(klucz) {
zwroc @ustawienia[klucz]
}
funkcja wyswietl_konfiguracje() {
pokazl "Konfiguracja aplikacji v" + @wersja + ":"
dla klucz, wartosc w @ustawienia {
pokazl " " + klucz + " = " + wartosc
}
}
}
Bezpośrednia konstrukcja jest zablokowana — tylko KonfiguracjaAplikacji.pobierz() zwraca instancję (zawsze tę samą). Cały trik, dzięki któremu to działa, polega na fladze zainicjalizowana: konstruktor sprawdza flagę i odmawia uruchomienia, ale pobierz() przełącza ją na prawdę tuż przed swoim jedynym legalnym wywołaniem nowy(), a potem przełącza z powrotem.
W poniższym katalogu znajdziesz więcej: różne sortowania (przez wstawianie, przez wybieranie, quicksort, merge sort), warianty wyszukiwania, listy łączone i stosy, hashowanie, plus garść innych wzorców GoF obok Singletona. Przejrzyj katalog bezpośrednio pod adresem github.com/N3BCKN/alexscript/tree/master/examples, żeby zobaczyć, co jest dostępne.