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:

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:

Repozytorium

github.com/N3BCKN/asbasic

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:

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

github.com/N3BCKN/zubr

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

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

github.com/N3BCKN/posel

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.

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.