Witaj w AlexScript

AlexScript to ogólnego przeznaczenia, dynamicznie typowany, interpretowany język programowania z polskojęzyczną składnią. Tam, gdzie większość języków zapisuje swoje słowa kluczowe po angielsku (if, else, class, function), AlexScript zapisuje je po polsku (jesli, albo, klasa, funkcja). Semantyka będzie jednak znajoma każdemu, kto używał Ruby’ego, Pythona albo JavaScriptu: funkcje pierwszej klasy, klasy z dziedziczeniem pojedynczym, moduły z mixinami, wyjątki, domknięcia, interaktywny REPL, async/await i wbudowany debugger.

Ten przewodnik prowadzi Cię od pierwszego pokazl "Hello" aż przez klasy, moduły, wyjątki i programowanie asynchroniczne. Każda sekcja buduje na poprzedniej, więc jeśli czytasz od początku do końca, nigdy nie powinieneś trafić na pojęcie, którego wcześniej nie widziałeś. Jeśli znasz już inny język skryptowy, możesz przemknąć przez wczesne sekcje i zanurkować tam, gdzie coś przykuje Twoją uwagę.

Kilka konwencji używanych w całym przewodniku. Pliki źródłowe AlexScript używają rozszerzenia .as. Przykłady kodu pokazane są w blokach alexscript; wyjście, jeśli jest pokazane, idzie po przykładzie. Komentarze w kodzie zaczynają się od # dla pojedynczej linii albo /* ... */ dla komentarza blokowego. Komunikaty błędów produkowane przez AlexScript w czasie wykonania — w tym wszystkie cytowane w tym tutorialu — są po polsku, ponieważ sam język jest polskocentryczny.

Uruchamianie AlexScript

Żeby uruchomić program AlexScript, zapisz go do pliku z rozszerzeniem .as i przekaż plik do interpretera:

alexscript hello.as

Pierwsza linia wyjścia będzie potwierdzeniem, że plik został wczytany — na razie możesz ją zignorować. Po tym Twój program się uruchamia.

Możesz też przekazać jednolinijkowca bezpośrednio jako argument napisowy, co przydaje się do szybkich eksperymentów:

alexscript 'pokazl "test"'

REPL

Jeśli odpalisz interpreter bez żadnego argumentu plikowego, wpadasz do interaktywnego REPL-a (Read-Eval-Print Loop):

alexscript

Wewnątrz REPL-a każde wyrażenie, które wpiszesz, jest natychmiast obliczane, a wynik wypisywany. REPL trzyma też stan między liniami — zmienne, które zadeklarujesz, są dostępne, dopóki nie zresetujesz:

> niech x = 10
> niech y = 20
> x + y
=> 30

Wynik ostatniego wyrażenia jest automatycznie przypisywany do zmiennej-podkreślenia _, więc możesz łańcuchować od poprzedniej odpowiedzi:

> 5 * 7
=> 35
> _ + 100
=> 135

REPL rozpoznaje mały zestaw specjalnych komend, które nie są kodem AlexScript:

REPL to świetne miejsce, żeby wypróbowywać cechy języka w trakcie czytania tego przewodnika. Jeśli nie jesteś pewien, jak coś się zachowuje — wpisz to i zobacz.

Witaj, Świecie

Tradycyjny pierwszy program w AlexScript:

pokazl "Hello, World"

Zapisz go do hello.as i uruchom. Słowo kluczowe pokazl wypisuje swój argument, a po nim znak nowej linii. Jest też pokaz, które wypisuje bez znaku nowej linii:

pokaz "Hello, "
pokaz "World"
pokazl ""

Zarówno pokaz, jak i pokazl przyjmują dowolną wartość — napisy, liczby, wartości logiczne, tablice, obiekty, nawet instancje Twoich własnych klas. AlexScript sformatuje każdą wartość w sensowny sposób:

pokazl 42
pokazl 3.14
pokazl prawda
pokazl [1, 2, 3]
pokazl { "imie": "Anna", "wiek": 30 }

Zmienne

Zmienne deklaruje się słowem kluczowym niech. AlexScript jest dynamicznie typowany: nie zapisujesz typu jawnie — jest on wywodzony z wartości, którą przypisujesz.

niech x = 5
niech pi = 3.14
niech imie = "Alex"
niech aktywny = prawda
niech nieznany = nic

Po zadeklarowaniu możesz przypisać zmiennej nową wartość bez niech:

niech licznik = 0
licznik = licznik + 1
licznik = licznik + 1
pokazl licznik    # 2

Rozróżnienie między niech zmienna = ... (deklaruje nową zmienną) a zmienna = ... (modyfikuje istniejącą) jest w AlexScript fundamentalne. Wrócimy do tego w sekcji Zasięg zmiennych.

Stałe

Identyfikatory zapisane w całości wielkimi literami są traktowane jako stałe. Raz przypisana stała nie może być przypisana ponownie — każda taka próba rzuca błąd uruchomieniowy.

niech MAX_USERS = 100
niech PI = 3.14159

# To zawiedzie:
# MAX_USERS = 200
# BladWykonania: Nie mozna zmienic wartosci stalej MAX_USERS

Stałe są umownie używane do wartości, które nigdy nie powinny się zmieniać w czasie wykonania — ograniczeń konfiguracji, stałych matematycznych, kodów błędów i tym podobnych.

Stała może trzymać dowolny rodzaj wartości: liczbę, napis, tablicę, obiekt, nawet funkcję. Reguła „stałości” dotyczy przypisania nazwy, a nie głębokiej niezmienności wartości stojącej za nazwą. Tablica przypisana do stałej nadal może mieć modyfikowane swoje elementy przez metody takie jak dodaj — wiązanie jest stałe, zawartość już nie.

Zmienne globalne

Domyślnie zmienne zadeklarowane przez niech wewnątrz funkcji albo bloku są lokalne dla tego zasięgu. Użyj globalna niech na poziomie najwyższym (albo wewnątrz funkcji, żeby zadeklarować globalną stamtąd), żeby uczynić zmienną dostępną z dowolnego zasięgu:

globalna niech LICZNIK_GLOBALNY = 0

funkcja zwieksz() {
    LICZNIK_GLOBALNY = LICZNIK_GLOBALNY + 1
}

zwieksz()
zwieksz()
pokazl LICZNIK_GLOBALNY    # 2

Używaj globalnych oszczędnie. Sprawiają, że kod jest trudniejszy do zrozumienia i trudniejszy do testowania. Zwykle lepszym wyborem jest przekazywanie wartości jako argumentów funkcji albo przechowywanie ich w obiektach.

Zasięg zmiennych

AlexScript używa zasięgu leksykalnego (blokowego). Zmienna zadeklarowana wewnątrz bloku { ... } jest widoczna tylko w obrębie tego bloku:

jesli prawda {
    niech wewnetrzna = 1
    pokazl wewnetrzna    # 1
}

# pokazl wewnetrzna   # BladNazwy — wewnetrzna nie jest tu widoczna

Dotyczy to każdego rodzaju bloku — gałęzi jesli, pętli dopoki, pętli dla, ciał funkcji i tak dalej. Zmienne zadeklarowane wewnątrz ciała nie wyciekają na zewnątrz:

dla niech indeks = 0; 3; 1 {
    niech temp = indeks * 2
}

# pokazl temp     # BladNazwy — temp było ograniczone do ciała pętli

Zasięgi wewnętrzne mogą czytać zmienne z zasięgów zewnętrznych, ale niech zawsze tworzy nowe wiązanie w bieżącym zasięgu:

niech x = 10

jesli prawda {
    niech x = x + 1   # tworzy nowe lokalne x; zewnętrzne x niezmienione
    pokazl x          # 11
}

pokazl x              # 10

Żeby zmodyfikować zmienną zewnętrzną, przypisz bez niech:

niech x = 10

jesli prawda {
    x = x + 1         # mutuje zewnętrzne x
}

pokazl x              # 11

To rozróżnienie — niech zmienna = ... deklaruje nową zmienną, samo zmienna = ... modyfikuje istniejącą — to jedno z najczęstszych źródeł zamieszania w AlexScript. Przeczytaj to dwa razy i zapamiętaj.

Słowa zarezerwowane

Mały zestaw identyfikatorów nie może być używany jako nazwy zmiennych, ponieważ są słowami kluczowymi albo operatorami języka. Pełna lista:

niech, globalna, jesli, albo, albojesli, to, prawda, falsz, i, lub, dopoki, petla, dla, w, funkcja, nic, zakoncz, nastepny, pokaz, pokazl, zwroc, wyjscie, wczytaj, import, proba, zlap, wkoncu, rzuc, klasa, super, sam, statyczna, prywatne, abstrakcyjna, modul, dolacz, debug, fn, asynchroniczna, czekaj, istnieje.

To szczególnie zaskakuje przy bardzo krótkich. Nie możesz nazwać zmiennej i — to operator logicznego AND. Nie możesz nazwać zmiennej w — to słowo kluczowe przynależności w pętlach dla. Nazwanie zmiennej to też zawiedzie, ponieważ to jest słowem kluczowym dla wbudowanego warunku.

Jeśli naprawdę chcesz jednoliterowego licznika pętli, użyj j, k, n albo idx. Jeśli chcesz nazwać zmienną „in”, przeliteruj inaczej — na przykład wejscie. Interpreter jest tolerancyjny co do tego, jak nazywasz rzeczy, ale nie może pozwolić Ci na ponowne użycie nazw, które coś dla niego znaczą.

Typy danych

AlexScript ma mały, regularny zestaw typów wbudowanych. Każda wartość należy dokładnie do jednego z nich.

Logiczna

Typ logiczny ma dwie wartości: prawda i falsz. Wartości logiczne produkowane są przez operatory porównania (==, <, > itd.) i operatory logiczne (i, lub, !).

niech aktywny = prawda
niech zakonczony = falsz
pokazl aktywny i !zakonczony    # prawda

Calkowita

Liczby całkowite o dowolnej precyzji. Nie ma ustalonej górnej granicy — liczby całkowite AlexScript rosną tak, jak pozwala pamięć.

niech maly = 42
niech duzy = 1000000000000000000
niech ujemny = -7
pokazl duzy * duzy

Zmiennoprzecinkowa

Liczby z kropką dziesiętną. Reprezentowane przez 64-bitowe liczby zmiennoprzecinkowe.

niech pi = 3.14159
niech temperatura = -2.5
pokazl pi * 2.0

Mieszanie liczb całkowitych i zmiennoprzecinkowych w arytmetyce daje wynik zmiennoprzecinkowy.

Napis

Sekwencje znaków zapisane w cudzysłowach. Napisy AlexScript wspierają sekwencje ucieczki (\n, \t, \", \\) i konkatenację napisów przez +, ale najergonomiczniejszy sposób osadzania wartości to interpolacja napisów — zobacz sekcję Napisy.

niech imie = "Anna"
niech powitanie = "Cześć, #{imie}!"
pokazl powitanie    # Cześć, Anna!

Nic

Pojedyncza wartość nic reprezentuje „brak wartości” albo „nieobecność”. To, co dostajesz z funkcji, która nie zwraca jawnie, z dostępu do brakującego klucza obiektu i ze zmiennych, które inicjujesz bez sensownej wartości na razie.

niech wynik = nic

jesli wynik == nic {
    pokazl "brak wyniku"
}

Tablica

Uporządkowane, indeksowane od zera kolekcje, które mogą trzymać wartości dowolnych typów — w tym mieszankę typów w tej samej tablicy.

niech liczby = [1, 2, 3, 4, 5]
niech mieszane = [1, "dwa", 3.0, prawda, nic]
niech pusta = []

pokazl liczby[0]      # 1
pokazl mieszane[1]    # dwa

Tablice są wyposażone w bogaty zestaw wbudowanych metod — dlg, dodaj, usun, mapuj, filtruj, sortuj i wiele innych — szczegółowo opisanych w sekcji Tablice.

Obiekt

Kolekcje klucz-wartość, w innych językach nazywane czasem hashami albo słownikami. Klucze są napisami; wartości mogą być dowolnego typu.

niech osoba = {
    "imie": "Anna",
    "wiek": 30,
    "miasto": "Warszawa"
}

pokazl osoba["imie"]    # Anna
osoba["wiek"] = 31
osoba["email"] = "[email protected]"

Sprawdzanie typów w czasie wykonania

Każda wartość odpowiada na metodę typ(), która zwraca jej nazwę typu jako napis:

pokazl (42).typ()       # calkowita
pokazl 3.14.typ()       # zmiennoprzecinkowa
pokazl "tekst".typ()    # napis
pokazl prawda.typ()     # logiczna
pokazl nic.typ()        # nic
pokazl [1, 2].typ()     # tablica
pokazl ({"a": 1}).typ() # obiekt

Dla literałów liczbowych czasem trzeba użyć nawiasów, żeby pomóc parserowi, jak w (42).typ().

Operatory

Arytmetyka

niech a = 7
niech b = 3

pokazl a + b      # 10   dodawanie
pokazl a - b      # 4    odejmowanie
pokazl a * b      # 21   mnożenie
pokazl a / b      # 2    dzielenie całkowite (oba operandy całkowite)
pokazl a % b      # 1    modulo
pokazl a ** b     # 343  potęgowanie

Gdy któryś z operandów jest zmiennoprzecinkowy, dzielenie zwraca wartość zmiennoprzecinkową:

pokazl 7 / 3        # 2
pokazl 7.0 / 3      # 2.3333333333333335
pokazl 7 / 3.0      # 2.3333333333333335

Jednoargumentowy minus neguje liczbę:

niech x = 5
pokazl -x           # -5

Operator ** działa też z liczbami zmiennoprzecinkowymi, co daje zwięzły sposób zapisu pierwiastków — x ** 0.5 to pierwiastek kwadratowy z x, x ** (1.0 / 3) to pierwiastek sześcienny:

pokazl 9 ** 0.5         # 3.0
pokazl 27 ** (1.0 / 3)  # 3.0000000000000004

Operator + służy też do konkatenacji napisów:

pokazl "Hello, " + "World"

Żeby przekonwertować liczbę na napis do konkatenacji, użyj .napis():

niech wiek = 30
pokazl "Mam " + wiek.napis() + " lat"

W praktyce interpolacja napisów jest zwykle przyjemniejsza niż konkatenacja przez + przy budowaniu komunikatów z wartości różnego typu.

Porównanie

pokazl 5 == 5     # prawda
pokazl 5 != 3     # prawda
pokazl 5 > 3      # prawda
pokazl 5 < 3      # falsz
pokazl 5 >= 5     # prawda
pokazl 5 <= 4     # falsz

Porównanie działa na liczbach i napisach (leksykograficznie dla napisów). Równość działa na każdym typie i jest strukturalna — dwie tablice o tych samych elementach są równe, podobnie jak dwa obiekty o tych samych kluczach i wartościach:

pokazl [1, 2, 3] == [1, 2, 3]    # prawda
pokazl {"a": 1} == {"a": 1}      # prawda

Logiczne

niech a = prawda
niech b = falsz

pokazl a i b          # falsz   logiczne AND
pokazl a lub b        # prawda  logiczne OR
pokazl !a             # falsz   logiczne NOT

Operatory i i lub są zwierające: jeśli lewy operand i jest falsz, prawy operand nie jest obliczany; podobnie dla lub, jeśli lewy jest prawda.

funkcja drukuj_i_zwroc(x) {
    pokazl "obliczam: #{x}"
    zwroc x
}

# Prawa strona nigdy się nie wykona:
pokazl falsz i drukuj_i_zwroc(prawda)
# Wyjście:
# falsz

jesli i dopoki przyjmują tylko warunki logiczne — nie ma niejawnej „prawdziwości” napisów albo liczb, jak w Pythonie czy JavaScripcie. jesli "tekst" { ... } rzuci błąd uruchomieniowy zamiast po cichu potraktować niepusty napis jako prawdę.

Bitowe

Dla liczb całkowitych AlexScript udostępnia pełne operacje bitowe. Działają jak w C, Pythonie czy Rubym — na binarnej reprezentacji liczby.

niech a = 12          # 1100
niech b = 10          # 1010

pokazl a & b          # 8     bitowe AND  (1000)
pokazl a | b          # 14    bitowe OR   (1110)
pokazl a ^ b          # 6     bitowe XOR  (0110)
pokazl ~a             # -13   bitowe NOT
pokazl a << 2         # 48    przesunięcie w lewo (110000)
pokazl a >> 1         # 6     przesunięcie w prawo (110)

Operatory przesunięcia wymagają, żeby prawy operand był nieujemną liczbą całkowitą; przesunięcie o ujemną wartość rzuca błąd uruchomieniowy.

Operator << ma drugie znaczenie: gdy lewy operand jest tablicą, dopisuje do tej tablicy. Interpreter wybiera właściwe znaczenie na podstawie typu lewego operandu — w czasie wykonania nie ma niejednoznaczności.

Są też metody bitowe na instancjach liczb całkowitych — bit(n), ustaw_bit(n), wyczysc_bit(n), przelacz_bit(n), policz_bity(), dlugosc_bitowa(), binarnie(), szesnastkowo(), osemkowo() — przydatne, gdy chcesz pracować na pojedynczych bitach bez wyrażeń manipulujących bitami:

niech n = 13          # 1101
pokazl n.binarnie()         # "1101"
pokazl n.bit(0)             # 1
pokazl n.bit(1)             # 0
pokazl n.policz_bity()      # 3
pokazl n.dlugosc_bitowa()   # 4

Przypisanie złożone

Znajome operatory skrótowe +=, -=, *=, /=, %= są wspierane:

niech x = 10
x += 5      # 15
x -= 3      # 12
x *= 2      # 24
x /= 4      # 6
x %= 4      # 2

Operator trójargumentowy

Wyrażenie trójargumentowe warunek ? wartosc_jesli_prawda : wartosc_jesli_falsz oblicza dokładnie jedną z dwóch gałęzi w zależności od warunku:

niech wiek = 18
niech status = wiek >= 18 ? "dorosly" : "nieletni"
pokazl status    # dorosly

Operator trójargumentowy jest prawostronnie łączny, więc łańcuchuje się naturalnie bez nawiasów:

niech pkt = 75
niech ocena = pkt >= 90 ? "A" :
              pkt >= 70 ? "B" :
              pkt >= 50 ? "C" : "F"
pokazl ocena     # B

Tylko wybrana gałąź jest obliczana — efekty uboczne w niewybranej gałęzi nigdy się nie wykonują. Sprawia to, że operator trójargumentowy jest bezpieczny dla wyrażeń ze strażą typu lista.dlg() > 0 ? lista[0] : nic.

Komentarze

# To jest komentarz jednoliniowy

/* To jest komentarz blokowy.
   Może rozciągać się na wiele linii. */

niech x = 5    # komentarze mogą też iść po kodzie

Komentarze są wycinane przez lekser przed parsowaniem — nie mają wpływu na zachowanie programu.

Sterowanie przepływem

if / else / else if

Konstrukcja warunkowa używa jesli, albojesli i albo:

niech x = 10

jesli x > 5 {
    pokazl "duze"
} albojesli x > 0 {
    pokazl "male"
} albo {
    pokazl "niedodatnie"
}

Możesz mieć dowolną liczbę gałęzi albojesli; albo jest opcjonalne. Pierwsza gałąź, której warunek jest prawda, się wykonuje; reszta jest pomijana.

Dla bardzo krótkich warunków forma wbudowana z to brzmi naturalnie:

jesli x > 100 to pokazl "duzo"

Forma wbudowana wykonuje pojedynczą instrukcję po warunku. Dla wielu instrukcji użyj formy blokowej.

Sam warunek musi być wartością logiczną. AlexScript nie konwertuje niejawnie napisów, liczb ani tablic na prawdę/fałsz — akceptowane są tylko prawda, falsz albo (specjalnie dopuszczone) nic. Przekazanie napisu rzuca BladWykonania: Warunek musi byc boolem lub "nic".

Wyjście z programu: wyjscie

Funkcja wyjscie() natychmiast kończy program. Bez argumentu proces wychodzi z kodem statusu 0 (sukces):

pokazl "przed"
wyjscie()
pokazl "to się nie wykona"

Przekaż literał całkowity, żeby ustawić kod wyjścia — konwencja jest taka, że 0 oznacza sukces, a niezerowa wartość oznacza błąd:

jesli !konfiguracja_ok() {
    pokazl "Krytyczny blad konfiguracji"
    wyjscie(1)
}

Argument wyjscie musi być literałem całkowitym — nie możesz przekazać dowolnego wyrażenia ani komunikatu napisowego. Jeśli musisz coś wypisać przed wyjściem, zrób to w osobnej linii. Trzymaj wyjscie na terminalne porażki: w normalnym przepływie zwracanie z funkcji albo doprowadzenie main do końca jest bardziej idiomatyczne.

pętla while

Słowo kluczowe dopoki uruchamia blok tak długo, jak warunek jest prawdziwy:

niech k = 0

dopoki k < 5 {
    pokazl k
    k = k + 1
}

Warunek jest obliczany przed każdą iteracją. Jeśli jest fałszywy na wejściu, ciało nigdy się nie wykona. Tak samo jak w jesli, warunek musi być wartością logiczną.

pętla for (numeryczna)

Numeryczna pętla for w stylu C ma postać dla niech zmienna = start; koniec; krok { ... }:

dla niech k = 0; 10; 1 {
    pokazl k        # wypisuje 0..9
}

Trzy części to: wartość początkowa, wartość końcowa (wyłączna) i krok. Zmienna liczy w górę, dopóki jest mniejsza od wartości końcowej. Żeby liczyć w dół, użyj kroku ujemnego:

dla niech k = 10; 0; -1 {
    pokazl k        # wypisuje 10..1
}

Część kroku jest opcjonalna i domyślnie wynosi 1:

dla niech k = 0; 5 {
    pokazl k        # wypisuje 0..4
}

Wszystkie trzy części nagłówka są pełnoprawnymi wyrażeniami, nie tylko literałami. Wartością końcową może być długość tablicy, wynik wywołania funkcji, cokolwiek, co zostanie obliczone do liczby całkowitej:

niech arr = [10, 20, 30, 40]

dla niech k = 0; arr.dlg(); 1 {
    pokazl arr[k]
}

Uwaga: Ponieważ i jest operatorem logicznego AND, nie może być nazwą licznika pętli. Konwencja w kodzie AlexScript to k, j, n albo idx.

pętla for (po kolekcji)

Żeby iterować po tablicy, użyj dla element w tablica { ... }:

niech owoce = ["jablko", "gruszka", "sliwka"]

dla owoc w owoce {
    pokazl owoc
}

Żeby iterować po obiekcie, użyj formy dwuzmiennej dla klucz, wartosc w obiekt { ... }:

niech osoba = { "imie": "Anna", "wiek": 30 }

dla klucz, wartosc w osoba {
    pokazl "#{klucz} = #{wartosc}"
}

Kilka istotnych ograniczeń, o których warto wiedzieć:

Iteracja jest wspierana tylko dla tablic i obiektów. Nie możesz iterować bezpośrednio po napisie przez dla znak w "tekst" — rzuca Moze iterowac tylko po tablicach. Żeby przetwarzać napis znak po znaku, albo indeksuj go przez s.indeks(k) w pętli numerycznej, albo najpierw rozdziel go przez s.rozdziel("").

Dla obiektów forma dwuzmienna jest tym, czego używasz — nie ma powszechnie używanej iteracji jednozmiennej po kluczach obiektu. Jeśli potrzebujesz tylko kluczy, najpierw wywołaj obj.klucze() i iteruj po wyniku.

Pętla nieskończona

Gdy chcesz zapętlić w nieskończoność i wyjść jawnie ze środka, użyj petla:

niech licznik = 0

petla {
    licznik = licznik + 1
    jesli licznik >= 10 to zakoncz
}

pokazl licznik    # 10

break i continue

Wewnątrz dowolnej pętli zakoncz natychmiast wychodzi z pętli, a nastepny przeskakuje do następnej iteracji:

dla niech k = 0; 20; 1 {
    jesli k % 2 == 0 to nastepny     # pomiń liczby parzyste
    jesli k > 10 to zakoncz          # zatrzymaj po 10
    pokazl k                         # wypisuje 1, 3, 5, 7, 9
}

Oba słowa kluczowe stosują się tylko do najbardziej wewnętrznej pętli. Działają w dla, dopoki i petla — w każdej konstrukcji pętli.

Wejście i wyjście

Widziałeś już pokaz i pokazl do wyjścia. Do wejścia AlexScript udostępnia wczytaj:

niech imie = wczytaj("Jak masz na imie? ")
pokazl "Cześć, #{imie}!"

wczytaj wyświetla swój argument-prompt (bez znaku nowej linii), czyta linię ze standardowego wejścia i zwraca ją jako napis. Jeśli potrzebujesz liczby, konwertuj ją jawnie przez .liczba():

niech wiek_tekst = wczytaj("Twoj wiek: ")
niech wiek = wiek_tekst.liczba()

jesli wiek == nic {
    pokazl "To nie jest poprawna liczba"
} albojesli wiek >= 18 {
    pokazl "Witamy w klubie"
}

Dwie rzeczy do zapamiętania o .liczba(). Po pierwsze, zawsze zwraca wartość zmiennoprzecinkową, nawet dla wejścia, które wygląda na całkowite — "42".liczba() to 42.0. Po drugie, zwraca nic (nie zero, nie wyjątek) dla wejścia, które nie parsuje się jako liczba, więc możesz zabezpieczyć się przed złym wejściem, sprawdzając nic.

Napisy

Napisy to sekwencje znaków w cudzysłowach. Napisy AlexScript są niemutowalne: metody takie jak duzymi() czy wyczysc() zwracają nowe napisy zamiast modyfikować oryginał.

Interpolacja napisów

Wewnątrz napisu składnia #{wyrazenie} oblicza wyrażenie i wstawia jego reprezentację napisową. To najergonomiczniejszy sposób budowania komunikatów ze zmiennych:

niech imie = "Anna"
niech wiek = 30

pokazl "Cześć, #{imie}!"
pokazl "Mam #{wiek} lat."
pokazl "Za rok będę miał #{wiek + 1} lat."

Wyrażenie wewnątrz #{ ... } może być czymkolwiek — arytmetyką, wywołaniami metod, wyrażeniami trójargumentowymi, nawet zagnieżdżonymi wywołaniami funkcji:

niech liczby = [1, 2, 3, 4, 5]

pokazl "Suma: #{liczby.suma()}"
pokazl "Średnia: #{liczby.srednia()}"
pokazl "Status: #{liczby.dlg() > 0 ? "ma elementy" : "puste"}"

Żeby umieścić w napisie literalne #{ bez wywoływania interpolacji, eskejpuj # ukośnikiem wstecznym:

pokazl "literal: \#{nie_interpolowane}"

Interpolacja działa w każdym napisie ze zwykłymi cudzysłowami — wszędzie tam, gdzie AlexScript przyjąłby literał napisowy. Komponuje się naturalnie ze wszystkimi innymi operacjami na napisach.

Inspekcja

niech s = "Cześć świat"

pokazl s.dlg()             # 11
pokazl s.pusta()           # falsz
pokazl s.zawiera("świat")  # prawda
pokazl s.indeks(0)         # "C"   znak na pozycji
pokazl s.indeks(-1)        # "t"   ujemny indeks liczy od końca

Transformacja

niech s = "  Hello World  "

pokazl s.duzymi()          # "  HELLO WORLD  "
pokazl s.malymi()          # "  hello world  "
pokazl s.wyczysc()         # "Hello World"     ucina białe znaki
pokazl s.zduzej()          # "  hello world  " wielka litera na początku
pokazl s.odwroc()          # "  dlroW olleH  "
pokazl s.usun(" ")         # "HelloWorld"      usuwa znaki

Wycinanie

niech s = "AlexScript"

pokazl s.wydziel(0, 4)     # "Alex"   (start, długość)
pokazl s.wycinek(4, 9)     # "Script" (start, koniec włącznie)

Rozdzielanie i parsowanie

niech csv = "anna,jan,ewa"
pokazl csv.rozdziel(",")   # ["anna", "jan", "ewa"]

niech liczba = "42".liczba()
pokazl liczba              # 42.0   (zawsze zmiennoprzecinkowa)

niech zla = "abc".liczba()
pokazl zla                 # nic    (nie udało się sparsować)

rozdziel przyjmuje też Wyrazenie (wyrażenie regularne) dla bardziej elastycznego rozdzielania — zobacz Wyrażenia regularne.

Konwersja

Żeby przekonwertować dowolną wartość na jej reprezentację napisową, wywołaj .napis():

pokazl 42.napis()           # "42"
pokazl 3.14.napis()         # "3.14"
pokazl prawda.napis()       # "prawda"
pokazl [1, 2].napis()       # "[1, 2]"

Przydatne, gdy konkretnie potrzebujesz napisu i chcesz być w tym jawny. Do budowania komunikatów z osadzonymi wartościami lepiej używaj interpolacji napisów.

Tablice

Tablice są indeksowane od zera, mogą trzymać typy mieszane i rosną dynamicznie.

Tworzenie i dostęp

niech liczby = [10, 20, 30, 40, 50]
niech pusta = []

pokazl liczby[0]    # 10
pokazl liczby[2]    # 30

# Indeksy ujemne liczą od końca:
pokazl liczby[-1]   # 50
pokazl liczby[-2]   # 40

# Modyfikacja przez indeks:
liczby[1] = 99
pokazl liczby       # [10, 99, 30, 40, 50]

Indeksowanie poza końcem rzuca wyjątek BladZakresu.

Dodawanie i usuwanie elementów

niech arr = [1, 2, 3]

arr.dodaj(4)              # dodaj na koniec
arr << 5                  # to samo, krócej
pokazl arr                # [1, 2, 3, 4, 5]

arr.wstaw(0, 0)           # wstaw na indeks 0
pokazl arr                # [0, 1, 2, 3, 4, 5]

arr.usun(2)               # usuń z indeksu 2
pokazl arr                # [0, 1, 3, 4, 5]

arr.wyczysc()             # usuń wszystko
pokazl arr                # []

<< na tablicy oznacza dopisanie — ten sam operator, który na liczbach całkowitych oznacza przesunięcie w lewo. Interpreter wybiera znaczenie na podstawie typu lewego operandu, więc nie ma niejednoznaczności.

Sprawdzanie

niech arr = [10, 20, 30, 20, 10]

pokazl arr.dlg()              # 5
pokazl arr.pusta()            # falsz
pokazl arr.zawiera(20)        # prawda
pokazl arr.indeks(20)         # 1   (pierwsze wystąpienie)
pokazl arr.pierwszy()         # 10
pokazl arr.ostatni()          # 10

Żeby policzyć wystąpienia wartości, połącz filtruj z dlg:

pokazl arr.filtruj(fn(x) { x == 20 }).dlg()    # 2

Wycinanie, łączenie, kopiowanie

niech arr = [10, 20, 30, 40, 50]

pokazl arr.wycinek(1, 3)      # [20, 30, 40]   włącznie z oboma końcami
pokazl arr.kopiuj()           # [10, 20, 30, 40, 50]   płytka kopia
pokazl arr.odwroc()           # [50, 40, 30, 20, 10]
pokazl arr.zlacz(", ")        # "10, 20, 30, 40, 50"
pokazl arr.polacz([60, 70])   # [10, 20, 30, 40, 50, 60, 70]

zlacz łączy wszystkie elementy w pojedynczy napis z podanym separatorem. polacz konkatenuje dwie tablice w nową tablicę.

Tablice numeryczne

Gdy wszystkie elementy są liczbami, dostępne są dodatkowe metody:

niech liczby = [3, 1, 4, 1, 5, 9, 2, 6]

pokazl liczby.suma()      # 31
pokazl liczby.srednia()   # 3.875
pokazl liczby.min()       # 1
pokazl liczby.max()       # 9

Wywołanie ich na tablicy z wartościami nieliczbowymi rzuca błąd uruchomieniowy.

Metody wyższego rzędu

Tablice wspierają kompletny zestaw metod wyższego rzędu, które przyjmują funkcję i stosują ją do każdego elementu. To miejsce, w którym AlexScript robi się naprawdę ekspresyjny.

mapuj

Stosuje funkcję do każdego elementu i zwraca nową tablicę wyników. Oryginalna tablica nie jest modyfikowana.

niech arr = [1, 2, 3, 4]
niech podwojone = arr.mapuj(fn(x) { x * 2 })
pokazl podwojone    # [2, 4, 6, 8]
pokazl arr          # [1, 2, 3, 4]   bez zmian

Jeśli callback przyjmuje dwa parametry, drugi dostaje indeks:

niech arr = [10, 20, 30]
niech wynik = arr.mapuj(fn(el, idx) { el + idx })
pokazl wynik        # [10, 21, 32]

To zachowanie z dwiema arnościami — przekaż callback jednoargumentowy albo dwuargumentowy — działa dla mapuj i kazdy. Interpreter sprawdza, ile parametrów deklaruje Twój callback, i wywołuje go odpowiednio.

filtruj

Zwraca nową tablicę zawierającą tylko te elementy, dla których callback zwrócił prawda:

niech liczby = [1, 2, 3, 4, 5, 6, 7, 8]
niech parzyste = liczby.filtruj(fn(x) { x % 2 == 0 })
pokazl parzyste     # [2, 4, 6, 8]

redukuj

Łączy wszystkie elementy w pojedynczą wartość, startując od jawnej wartości początkowej. Callback dostaje bieżący akumulator i bieżący element:

niech liczby = [1, 2, 3, 4, 5]
niech suma = liczby.redukuj(fn(acc, x) { acc + x }, 0)
pokazl suma         # 15

niech iloczyn = liczby.redukuj(fn(acc, x) { acc * x }, 1)
pokazl iloczyn      # 120

Wartość początkowa (drugi argument redukuj) jest wymagana — nie ma niejawnej domyślnej. Sprawia to, że redukuj jest bezpieczne na pustych tablicach:

pokazl [].redukuj(fn(acc, x) { acc + x }, 0)    # 0

kazdy

Uruchamia callback dla efektów ubocznych na każdym elemencie. Zwraca nic. To mapuj dla przypadków, gdy nie chcesz tablicy wynikowej:

[1, 2, 3].kazdy(fn(x) {
    pokazl "element: #{x}"
})

znajdz

Zwraca pierwszy element, dla którego callback zwrócił prawda, albo nic, jeśli żaden taki nie istnieje:

niech ludzie = [
    {"imie": "Anna", "wiek": 25},
    {"imie": "Jan", "wiek": 40},
    {"imie": "Ewa", "wiek": 35}
]

niech starszy = ludzie.znajdz(fn(os) { os["wiek"] > 30 })
pokazl starszy["imie"]    # Jan

dowolny / wszystkie

Sprawdzenia kwantyfikatorowe: dowolny zwraca prawda, jeśli callback dał prawda dla przynajmniej jednego elementu, wszystkie zwraca prawda tylko wtedy, gdy callback dał prawda dla każdego elementu.

niech liczby = [2, 4, 6, 8]

pokazl liczby.dowolny(fn(x) { x > 5 })       # prawda
pokazl liczby.wszystkie(fn(x) { x % 2 == 0 }) # prawda
pokazl liczby.wszystkie(fn(x) { x > 5 })     # falsz

Przypadki brzegowe idą za standardową konwencją matematyczną: dowolny na pustej tablicy zwraca falsz (żaden element niczego nie spełnia), wszystkie na pustej tablicy zwraca prawda (banalnie — każdy nieistniejący element spełnia cokolwiek).

sortuj

Bez argumentów sortuj zwraca nową tablicę posortowaną w naturalnej kolejności — rosnąco dla liczb, leksykograficznie dla napisów:

pokazl [3, 1, 4, 1, 5, 9, 2, 6].sortuj()
# [1, 1, 2, 3, 4, 5, 6, 9]

pokazl ["b", "a", "c"].sortuj()
# ["a", "b", "c"]

Z funkcją porównującą sortuje według niej. Komparator dostaje dwa elementy i musi zwrócić liczbę: ujemną, jeśli pierwszy ma być przed drugim, dodatnią, jeśli po, zero, jeśli równe.

niech liczby = [3, 1, 4, 1, 5, 9, 2, 6]

# malejąco
niech malejaco = liczby.sortuj(fn(a, b) { b - a })
pokazl malejaco    # [9, 6, 5, 4, 3, 2, 1, 1]

# po długości napisu
niech slowa = ["pies", "kot", "hipopotam", "as"]
niech wg_dlugosci = slowa.sortuj(fn(a, b) { a.dlg() - b.dlg() })
pokazl wg_dlugosci  # [as, kot, pies, hipopotam]

sortuj zwraca nową posortowaną tablicę — oryginał nie jest modyfikowany.

Łączenie metod

Metody wyższego rzędu dobrze się komponują — łańcuchuj je, żeby budować pipeline’y:

niech liczby = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

niech wynik = liczby
    .filtruj(fn(x) { x % 2 == 0 })
    .mapuj(fn(x) { x * x })
    .redukuj(fn(acc, x) { acc + x }, 0)

pokazl wynik    # 220   (4 + 16 + 36 + 64 + 100)

Ten styl — wyrażaj swoją intencję jako serię transformacji — jest często czytelniejszy niż równoważna pętla z pośrednimi zmiennymi.

Obiekty

Obiekty to kolekcje klucz-wartość z napisowymi kluczami.

Tworzenie i dostęp

niech osoba = {
    "imie": "Anna",
    "wiek": 30,
    "miasto": "Warszawa"
}

# Dostęp przez nawiasy:
pokazl osoba["imie"]      # Anna

# Modyfikacja albo dodanie kluczy:
osoba["wiek"] = 31
osoba["email"] = "[email protected]"

# Usuwanie kluczy:
osoba.usun("miasto")

Czytanie brakującego klucza zwraca nic, nie błąd:

pokazl osoba["nieistniejacy"]    # nic

Metody obiektów

niech obj = {"a": 1, "b": 2, "c": 3}

pokazl obj.dlg()             # 3
pokazl obj.pusty()           # falsz
pokazl obj.klucze()          # ["a", "b", "c"]
pokazl obj.wartosci()        # [1, 2, 3]
pokazl obj.ma_klucz("a")     # prawda
pokazl obj.ma_wartosc(2)     # prawda
pokazl obj.na_tablice()      # [["a", 1], ["b", 2], ["c", 3]]

Żeby iterować po obiekcie, użyj dwuzmiennej pętli dla:

dla klucz, wartosc w obj {
    pokazl "#{klucz} -> #{wartosc}"
}

Funkcje

Funkcje deklarowane są przez funkcja:

funkcja powitaj(imie) {
    pokazl "Cześć, #{imie}!"
}

powitaj("Anna")    # Cześć, Anna!

Ciało idzie w nawiasach klamrowych { }. Żeby zwrócić wartość, użyj zwroc:

funkcja kwadrat(x) {
    zwroc x * x
}

pokazl kwadrat(7)    # 49

Funkcja bez jawnego zwroc zwraca nic. Zauważ, że samo zwroc (bez wyrażenia) jest błędem składni — napisz zwroc nic, jeśli chcesz jawnie zwrócić wartość pustą.

Parametry domyślne

Parametry mogą mieć wartości domyślne, które są stosowane, gdy argument jest pominięty:

funkcja powitaj(imie, formalna = falsz) {
    jesli formalna {
        zwroc "Dzień dobry, #{imie}"
    }
    zwroc "Cześć, #{imie}"
}

pokazl powitaj("Anna")               # Cześć, Anna
pokazl powitaj("Anna", prawda)       # Dzień dobry, Anna

Wartości domyślne mogą być dowolnym wyrażeniem. Są obliczane przy każdym wywołaniu funkcji z brakującym argumentem.

Parametry rest

Poprzedź ostatni parametr *, żeby zebrać wszystkie pozostałe argumenty pozycyjne do tablicy:

funkcja suma(*liczby) {
    niech wynik = 0
    dla n w liczby {
        wynik = wynik + n
    }
    zwroc wynik
}

pokazl suma()              # 0
pokazl suma(1, 2, 3)       # 6
pokazl suma(1, 2, 3, 4, 5) # 15

Parametr rest może iść po zwykłych parametrach:

funkcja pierwszy_plus(pierwszy, *reszta) {
    niech wynik = pierwszy
    dla n w reszta {
        wynik = wynik + n
    }
    zwroc wynik
}

pokazl pierwszy_plus(10, 1, 2, 3)    # 16

Reguły deklarowania parametrów

Dwie reguły rządzą tym, jak można łączyć parametry:

Wartości domyślne muszą iść po parametrach wymaganych. funkcja test(a = 1, b, c) jest niepoprawne — gdy raz zadeklarujesz parametr z wartością domyślną, każdy parametr na prawo od niego też musi mieć domyślną. Błąd kompilacji brzmi Parametry bez wartosci domyslnych nie moga występowac po parametrach z wartosciami domyslnymi.

Parametr rest musi być ostatni. funkcja test(*a, b) jest niepoprawne. Parametr rest, jeśli jest obecny, musi iść na końcu. Błąd brzmi …musi być ostatnim parametrem….

Poprawna pełna deklaracja wygląda jak funkcja f(a, b, c = 10, d = 20, *reszta) — wymagane, potem z domyślnymi, potem opcjonalnie rest.

Walidacja argumentów

Jeśli wywołasz funkcję ze zbyt małą liczbą argumentów, AlexScript rzuca BladArgumentu:

funkcja suma(a, b) {
    zwroc a + b
}

# suma(5)
# BladArgumentu: Funkcja suma oczekiwala minimum 2 argumentów, otrzymała 1

Liczba „minimalna” to liczba parametrów bez wartości domyślnych. Z parametrem rest nie ma maksimum — dodatkowe argumenty po prostu lądują w tablicy rest. Bez parametru rest wywołanie ze zbyt dużą liczbą argumentów również jest błędem.

Funkcje zagnieżdżone

Funkcje mogą być definiowane wewnątrz innych funkcji. Funkcja wewnętrzna jest widoczna tylko w obrębie ciała funkcji otaczającej:

funkcja zewnetrzna() {
    niech x = 10

    funkcja wewnetrzna() {
        pokazl x        # przechwytuje x z otaczającego zasięgu
    }

    wewnetrzna()
}

zewnetrzna()
# wewnetrzna()       # BladNazwy — wewnetrzna nie jest tu widoczna

Rekurencja

Funkcje mogą wywoływać same siebie. AlexScript narzuca limit głębokości rekurencji, żeby chronić przed wywołaniami „uciekającymi” — po przekroczeniu dostajesz BladWykonania: zbyt glebokie zagniezdzenie stosu. Zawsze potrzebujesz przypadku bazowego, żeby się zatrzymać:

funkcja silnia(n) {
    jesli n <= 1 to zwroc 1
    zwroc n * silnia(n - 1)
}

pokazl silnia(5)    # 120
pokazl silnia(10)   # 3628800

Dla algorytmów, które inaczej wymagałyby bardzo głębokiej rekurencji, przepisz je z użyciem akumulatorów albo skonwertuj do jawnej pętli ze stosem.

Funkcje anonimowe i domknięcia

Użyj fn(parametry) { ciało }, żeby utworzyć wartość-funkcję bez nadawania jej nazwy. Te są powszechnie nazywane lambdami albo funkcjami anonimowymi:

niech kwadrat = fn(x) { x * x }
pokazl kwadrat(5)    # 25

Jeśli ciało jest pojedynczym wyrażeniem, wynik tego wyrażenia jest automatycznie zwracany — bez zwroc:

niech podwoj = fn(x) { x * 2 }
pokazl podwoj(7)    # 14

Dla wielolinijkowego ciała używaj jawnego zwroc:

niech znormalizuj = fn(s) {
    niech wyczyszczone = s.wyczysc()
    niech male = wyczyszczone.malymi()
    zwroc male
}

pokazl znormalizuj("  Hello  ")    # hello

Funkcje anonimowe są najczęściej przekazywane jako argumenty do funkcji wyższego rzędu takich jak mapuj i filtruj:

niech liczby = [1, 2, 3, 4, 5]
niech kwadraty = liczby.mapuj(fn(x) { x * x })
pokazl kwadraty    # [1, 4, 9, 16, 25]

Możesz też wywołać funkcję anonimową w miejscu (tzw. „immediately invoked function expression”):

pokazl (fn(x) { x + 1 })(10)    # 11

Rekurencyjne funkcje anonimowe

Funkcja anonimowa przypisana do zmiennej może wywołać samą siebie, odwołując się do nazwy zmiennej. Funkcja przechwytuje zmienną razem ze wszystkim innym w swoim domknięciu:

niech silnia = fn(n) {
    jesli n <= 1 to zwroc 1
    zwroc n * silnia(n - 1)
}

pokazl silnia(5)    # 120

To działa, ponieważ ciało fn jest obliczane przy każdym wywołaniu funkcji, a wtedy zmienna silnia jest już przypisana.

Funkcje jako wartości

Nazwane funkcje są też wartościami pierwszej klasy. Możesz przypisywać je do zmiennych, przekazywać jako argumenty, przechowywać w tablicach i obiektach oraz zwracać z innych funkcji.

funkcja podwoj(x) { zwroc x * 2 }

niech alias = podwoj
pokazl alias(5)    # 10

Mieszanie ich na przykład w tablice funkcji jest w porządku:

niech operacje = [
    fn(x) { x + 1 },
    fn(x) { x * 2 },
    fn(x) { x - 3 }
]

dla op w operacje {
    pokazl op(10)    # 11, 20, 7
}

Funkcje wyższego rzędu

Funkcja, która przyjmuje inną funkcję jako argument albo zwraca jedną, nazywana jest funkcją wyższego rzędu. To potężny sposób na wyciągnięcie powtarzających się wzorców.

funkcja zastosuj(f, wartosc) {
    zwroc f(wartosc)
}

pokazl zastosuj(fn(x) { x * 3 }, 7)              # 21
pokazl zastosuj(fn(s) { s.duzymi() }, "alex")    # ALEX

Zwracanie funkcji z funkcji — wzorzec fabryki — pozwala budować dostosowane funkcje na żądanie:

funkcja mnoznik(n) {
    zwroc fn(x) { x * n }
}

niech podwoj = mnoznik(2)
niech potroj = mnoznik(3)

pokazl podwoj(10)    # 20
pokazl potroj(10)    # 30

Zwrócona funkcja „pamięta” wartość n z wywołania, które ją utworzyło — to domknięcie, omawiane dalej.

Domknięcia

Domknięcie to funkcja, która przechwytuje zmienne z zasięgu, w którym została utworzona. Przechwycone zmienne żyją tak długo jak domknięcie, nawet po tym, jak otaczająca funkcja zwróciła wynik.

funkcja licznik() {
    niech n = 0

    zwroc fn() {
        n = n + 1
        zwroc n
    }
}

niech c = licznik()
pokazl c()    # 1
pokazl c()    # 2
pokazl c()    # 3

Każde wywołanie licznik() tworzy świeże n, więc dwa liczniki są niezależne:

niech c1 = licznik()
niech c2 = licznik()

pokazl c1()    # 1
pokazl c1()    # 2
pokazl c2()    # 1   niezależny stan

Dwa konkretne wzorce zbudowane na domknięciach pojawiają się wszędzie w prawdziwym kodzie AlexScript, więc warto je nazwać jawnie.

Funkcje fabrykujące — zwracają skonfigurowaną funkcję zamiast wartości, żeby wywołujący mógł ją wielokrotnie użyć:

funkcja walidator_dlugosci(min, max) {
    zwroc fn(s) {
        zwroc s.dlg() >= min i s.dlg() <= max
    }
}

niech haslo_ok = walidator_dlugosci(8, 64)
niech imie_ok = walidator_dlugosci(2, 50)

pokazl haslo_ok("krotkie")             # falsz
pokazl haslo_ok("bezpieczne_haslo")    # prawda
pokazl imie_ok("Anna")                 # prawda

Stanowe callbacki — przekazywanie domknięcia, które utrzymuje stan między wywołaniami, przydatne dla akumulatorów, generatorów sekwencji i śledzenia zdarzeń:

funkcja akumulator() {
    niech suma = 0
    zwroc fn(x) {
        suma = suma + x
        zwroc suma
    }
}

niech akum = akumulator()
[1, 2, 3, 4, 5].kazdy(fn(x) { pokazl akum(x) })
# 1, 3, 6, 10, 15

Częste wzorce funkcyjne

Kilka wzorców, które często pojawiają się przy pracy z funkcjami pierwszej klasy:

Komponowanie funkcji — łączy dwie funkcje w jedną, która uruchamia je sekwencyjnie:

funkcja komponuj(f, g) {
    zwroc fn(x) { f(g(x)) }
}

niech dodaj1 = fn(x) { x + 1 }
niech razy2 = fn(x) { x * 2 }

niech pierwsze_dodaj_potem_razy = komponuj(razy2, dodaj1)
pokazl pierwsze_dodaj_potem_razy(4)    # 10

Pipeline — stosuje tablicę funkcji od lewej do prawej:

funkcja pipe(fns, wartosc) {
    niech wynik = wartosc
    dla f w fns {
        wynik = f(wynik)
    }
    zwroc wynik
}

niech kroki = [fn(x) { x + 1 }, fn(x) { x * 3 }, fn(x) { x - 2 }]
pokazl pipe(kroki, 4)    # 13

Dekorator — opakowuje istniejącą funkcję dodatkowym zachowaniem:

funkcja z_logowaniem(f) {
    zwroc fn(x) {
        pokazl "Wejście: #{x}"
        niech wynik = f(x)
        pokazl "Wyjście: #{wynik}"
        zwroc wynik
    }
}

niech podwoj = z_logowaniem(fn(x) { x * 2 })
podwoj(5)
# Wejście: 5
# Wyjście: 10

To nie są cechy wbudowane — to wzorce, które budujesz z tych samych podstawowych składników (fn, zwracanie funkcji, przekazywanie ich jako argumentów). Gdy je sobie przyswoisz, dużo kodu staje się znacznie krótsze.

Sprawdzanie istnienia nazwy: istnieje

Czasami chcesz sprawdzić, czy nazwa jest zdefiniowana, zanim jej użyjesz — dla opcjonalnej konfiguracji, wtyczek albo opcjonalnych funkcji. Słowo kluczowe istnieje() zwraca prawda, jeśli nazwa odnosi się do czegokolwiek (zmiennej, funkcji, klasy albo modułu), i falsz w przeciwnym razie. Co kluczowe, robi to bez rzucania błędu dla nazw niezdefiniowanych:

niech x = 5
pokazl istnieje(x)            # prawda
pokazl istnieje(brak)         # falsz   (bez błędu)

niech y = nic
pokazl istnieje(y)            # prawda  (y istnieje, mimo że trzyma nic)

istnieje rozpoznaje każdy rodzaj nazwanej encji — nie tylko zmienne:

funkcja powitaj() {}
klasa Test {}
modul Pomocniki {}

pokazl istnieje(powitaj)      # prawda
pokazl istnieje(Test)         # prawda
pokazl istnieje(Pomocniki)    # prawda

Respektuje zasięg leksykalny — zmienna zadeklarowana w bloku wewnętrznym nie jest widoczna z zewnątrz:

jesli prawda {
    niech wewnetrzna = 1
}
pokazl istnieje(wewnetrzna)    # falsz

Argument musi być pojedynczym identyfikatorem — nie wyrażeniem, nie dostępem do pola obiektu, nie elementem tablicy. Żeby sprawdzić, czy obiekt ma klucz, porównaj wartość z nic albo użyj obj.ma_klucz(...).

Importy

Program można podzielić między wiele plików .as. Żeby użyć kodu z innego pliku, zaimportuj go przez import("ścieżka"):

# math_utils.as
funkcja kwadrat(x) { zwroc x * x }
funkcja szescian(x) { zwroc x * x * x }
# main.as
import("./math_utils.as")

pokazl kwadrat(5)     # 25
pokazl szescian(3)    # 27

Ścieżki zaczynające się od ./ albo ../ są rozwiązywane względem importującego pliku. Ścieżki bez wiodącej kropki rozwiązują się do biblioteki standardowej AlexScript — więc import("json") ładuje bibliotekę JSON, import("plik") ładuje bibliotekę I/O na plikach i tak dalej.

Rozszerzenie .as jest opcjonalne w ścieżkach importu. Zarówno import("./math_utils"), jak i import("./math_utils.as") działają tak samo.

Każdy plik jest ładowany co najwyżej raz na program. Importowanie tego samego pliku z wielu miejsc jest bezpieczne — drugi import jest no-opem.

Importy tranzytywne

Jeśli plik A importuje B, a B importuje C, to wszystko z C jest dostępne z A. Definicje propagują się w górę łańcucha. To właśnie pozwala organizować kod w pliki narzędziowe importowane przez pliki domenowe importowane przez Twój główny punkt wejścia — import robisz tylko na poziomie, którego potrzebujesz, a reszta jest wciągana automatycznie.

Importy równoległe i scalanie modułów

Jeśli dwa osobno importowane pliki otwierają ten sam moduł, AlexScript scala je w jeden. Załóżmy, że dzielisz moduł Zubr na dwa pliki:

# a.as
modul Zubr {
    klasa A { funkcja konstruktor() {} }
}
# b.as
modul Zubr {
    klasa B { funkcja konstruktor() {} }
}
# main.as
import("./a.as")
import("./b.as")

niech a = Zubr::A.nowy()
niech b = Zubr::B.nowy()

Po obu importach Zubr zawiera zarówno A, jak i B. Pełne reguły scalania są opisane w sekcji Ponowne otwieranie modułów.

Raportowanie błędów importu

Gdy w zaimportowanym pliku wystąpi błąd uruchomieniowy, AlexScript pokazuje łańcuch importów, które do niego doprowadziły, razem z numerami linii — żebyś nie musiał zgadywać, który import pośrednio uruchomił który plik. Typowy błąd wygląda tak:

BladNazwy: Niezadeklarowany identyfikator nieznana_zmienna
  import './c.as' (b.as:1)
  import './b.as' (a.as:1)

Oryginalna klasa błędu (tu BladNazwy) jest zachowana — nie dostajesz ogólnego opakowania „import nie powiódł się”, które ukrywałoby prawdziwy problem. Jeśli sam plik nie istnieje, dostajesz BladImportu z tym samym śladem łańcucha importów.

Programowanie obiektowe

AlexScript wspiera kompletny model programowania obiektowego: klasy ze stanem i zachowaniem, dziedziczenie pojedyncze, klasy abstrakcyjne, metody statyczne i prywatne oraz polimorfizm przez nadpisywanie metod. Słownictwo słów kluczowych jest niewielkie — klasa, nowy, sam, super, statyczna, prywatne, abstrakcyjna — i gdy je raz zobaczysz, rozpoznasz większość tego, co klasa może robić.

Definiowanie klasy

Klasa jest deklarowana przez klasa. Wewnątrz ciała klasy definiujesz metody przez funkcja:

klasa Osoba {
    funkcja konstruktor(imie, wiek) {
        niech @imie = imie
        niech @wiek = wiek
    }

    funkcja przedstaw_sie() {
        pokazl "Cześć, jestem #{@imie} i mam #{@wiek} lat."
    }
}

Dwie rzeczy do zauważenia:

konstruktor to specjalna nazwa metody dla inicjalizatora. Uruchamia się automatycznie, gdy tworzona jest nowa instancja.

Nazwy zaczynające się od @ to zmienne instancji. Należą do instancji, nie do metody, która je wprowadza, i trwają przez całe życie obiektu.

Tworzenie instancji

Użyj Klasa.nowy(...), żeby utworzyć instancję. Argumenty są przekazywane do konstruktora:

niech anna = Osoba.nowy("Anna", 30)
anna.przedstaw_sie()    # Cześć, jestem Anna i mam 30 lat.

Możesz przechować instancję w zmiennej, przekazywać ją dalej, włożyć do kolekcji — instancje są wartościami pierwszej klasy, jak wszystko inne.

Zmienne instancji

Zmienne instancji (@imie, @wiek itd.) są prywatne dla instancji. Dostęp do nich i ich modyfikacja odbywa się z wnętrza klasy z prefiksem @:

klasa Licznik {
    funkcja konstruktor() {
        niech @wartosc = 0
    }

    funkcja zwieksz() {
        @wartosc = @wartosc + 1
    }

    funkcja wartosc() {
        zwroc @wartosc
    }
}

niech c = Licznik.nowy()
c.zwieksz()
c.zwieksz()
c.zwieksz()
pokazl c.wartosc()    # 3

Wewnątrz metody możesz zarówno zadeklarować zmienną instancji przez niech @nazwa = ... (zazwyczaj w konstruktorze), jak i przypisać ponownie istniejącą przez @nazwa = .... Stosuje się ta sama reguła zasięgu, co dla zmiennych lokalnych: samo przypisanie modyfikuje istniejącą zmienną instancji, a niech @nazwa = ... przesłaniałoby ją wewnątrz tego bloku.

Zmienne instancji nie są bezpośrednio dostępne z zewnątrz klasy — c.@wartosc nie jest poprawną składnią. Żeby wystawić stan, definiuj metody-akcesory (często nazwane tak samo jak zmienna, jak wartosc() powyżej). Próba użycia @cos poza metodą rzuca BladWykonania: Nie można użyć zmiennej instancji poza kontekstem instancji.

Jeśli odczytasz zmienną instancji, której nigdy nie przypisano, dostajesz nic — nie ma błędów typu „niezadeklarowana zmienna instancji”.

Metody

Wewnątrz ciała metody możesz wywoływać inne metody na tej samej instancji po prostu po nazwie:

klasa Kalkulator {
    funkcja konstruktor() {
        niech @historia = []
    }

    funkcja dodaj(a, b) {
        niech wynik = a + b
        zapisz_wynik(wynik)     # wywołanie siostrzanej metody
        zwroc wynik
    }

    funkcja zapisz_wynik(w) {
        @historia << w
    }

    funkcja historia() {
        zwroc @historia
    }
}

niech k = Kalkulator.nowy()
k.dodaj(2, 3)
k.dodaj(10, 4)
pokazl k.historia()    # [5, 14]

sam — odniesienie do siebie

Słowo kluczowe sam odnosi się do bieżącej instancji wewnątrz dowolnej z jej metod. To AlexScriptowy odpowiednik self (Python, Ruby) albo this (Java, JavaScript). W najprostszym przypadku nie potrzebujesz go — wywołania innych metod na tym samym obiekcie działają bez żadnego prefiksu — ale staje się niezbędne w trzech sytuacjach: łańcuchowanie metod, przekazywanie instancji do innego kodu i sprawianie, by intencja była jawna.

Jawne wywoływanie metod siostrzanych:

klasa Kalkulator {
    funkcja dodaj(a, b) {
        niech wynik = a + b
        sam.zapisz_wynik(wynik)    # jawne self-wywołanie
        zwroc wynik
    }

    funkcja zapisz_wynik(w) { @historia << w }
}

sam.zapisz_wynik(wynik) i zapisz_wynik(wynik) są tu równoważne. Forma niejawna jest częstsza; forma jawna bywa czasem przydatna dla jasności.

Przekazywanie instancji do innego obiektu:

klasa Visitor {
    funkcja odwiedz(element) {
        zwroc "Odwiedzono: " + element.klasa()
    }
}

klasa Element {
    funkcja akceptuj(visitor) {
        zwroc visitor.odwiedz(sam)    # przekazuje bieżącą instancję
    }
}

niech e = Element.nowy()
niech v = Visitor.nowy()
pokazl e.akceptuj(v)    # Odwiedzono: Element

Polimorficzna introspekcja: wywołane z wnętrza klasy nadrzędnej, sam.klasa() zwraca rzeczywistą (podklasy) nazwę, nie nazwę rodzica:

klasa Zwierze {
    funkcja jaki_typ() {
        zwroc sam.klasa()
    }
}

klasa Pies < Zwierze {}

niech p = Pies.nowy()
pokazl p.jaki_typ()    # "Pies", nie "Zwierze"

Czym sam nie jest: nie możesz użyć go jako nazwy zmiennej (niech sam = 42 to błąd składni), nie możesz mu nic przypisać (sam = ... to błąd składni) i nie ma znaczenia poza ciałem metody (pokazl sam na poziomie najwyższym rzuca Nie można użyć 'sam' poza kontekstem instancji).

Łańcuchowanie metod i wzorzec Builder

Metoda kończąca się na zwroc sam zwraca bieżącą instancję, co pozwala doczepić następne wywołanie bezpośrednio do wyniku. Łańcuchy takich wywołań dają płynny interfejs — wzorzec Builder w klasycznej terminologii OOP:

klasa Builder {
    funkcja konstruktor() {
        niech @x = 0
        niech @y = 0
    }

    funkcja ustaw_x(v) {
        @x = v
        zwroc sam
    }

    funkcja ustaw_y(v) {
        @y = v
        zwroc sam
    }

    funkcja suma() {
        zwroc @x + @y
    }
}

niech wynik = Builder.nowy().ustaw_x(10).ustaw_y(20).suma()
pokazl wynik    # 30

Wzorzec działa dla każdego rodzaju etapowej konstrukcji — zamówień pizzy, zapytań do bazy danych, builderów zapytań HTTP, obiektów konfiguracyjnych. Konwencja, której warto się trzymać: metody mutujące stan i zwracające sam są zwykle używane w łańcuchach; metody obliczające ostateczną wartość (tu suma) kończą łańcuch, zwracając rzeczywisty wynik.

Dziedziczenie

Klasa może dziedziczyć po innej klasie używając operatora <:

klasa Zwierze {
    funkcja konstruktor(nazwa) {
        niech @nazwa = nazwa
    }

    funkcja odglos() {
        zwroc "..."
    }

    funkcja przedstaw() {
        zwroc "Jestem #{@nazwa} i robię #{sam.odglos()}"
    }
}

klasa Pies < Zwierze {
    funkcja odglos() {
        zwroc "Hau hau!"
    }
}

klasa Kot < Zwierze {
    funkcja odglos() {
        zwroc "Miau!"
    }
}

niech p = Pies.nowy("Burek")
niech k = Kot.nowy("Mruczek")

pokazl p.przedstaw()    # Jestem Burek i robię Hau hau!
pokazl k.przedstaw()    # Jestem Mruczek i robię Miau!

Pies dziedziczy konstruktor i metodę przedstaw po Zwierze, ale nadpisuje odglos. Gdy przedstaw wywołuje sam.odglos(), używa najbardziej specyficznej dostępnej wersji — to polimorfizm w akcji.

AlexScript wspiera tylko dziedziczenie pojedyncze — klasa może mieć co najwyżej jednego rodzica. Do dzielenia zachowania między wieloma klasami używaj modułów z dolacz (omówione w sekcji Moduły).

Łańcuch dziedziczenia może mieć dowolną głębokość: Pies < Zwierze, potem Owczarek < Pies i tak dalej.

super

Żeby wywołać metodę rodzica z wnętrza nadpisania, używaj super. Najczęstszy przypadek to konstruktor:

klasa Pojazd {
    funkcja konstruktor(marka, predkosc) {
        niech @marka = marka
        niech @predkosc = predkosc
    }
}

klasa Samochod < Pojazd {
    funkcja konstruktor(marka, predkosc, kolor) {
        super(marka, predkosc)      # wywołaj konstruktor Pojazd
        niech @kolor = kolor
    }
}

niech s = Samochod.nowy("Toyota", 200, "czerwony")

Bez argumentów super() wywołuje rodzicowską wersję bieżącej metody:

klasa Bazowa {
    funkcja opis() {
        zwroc "obiekt"
    }
}

klasa Pochodna < Bazowa {
    funkcja opis() {
        zwroc super() + " ze wstawka"
    }
}

pokazl Pochodna.nowy().opis()    # "obiekt ze wstawka"

Możesz też wywołać konkretną nazwaną metodę na rodzicu przez super.metoda(...):

klasa Pochodna < Bazowa {
    funkcja zlozona() {
        zwroc super.opis() + " (zmodyfikowany)"
    }
}

super działa poprawnie przez łańcuchy o dowolnej głębokości. W hierarchii A → B → C → D, wywołanie super(arg) z konstruktora D uruchamia konstruktor C, który z kolei może wywołać super(arg), żeby dotrzeć do B, aż do A. Każdy poziom może transformować argumenty przed przekazaniem ich w górę:

klasa A {
    funkcja konstruktor(s) { niech @s = s }
    funkcja s() { zwroc @s }
}
klasa B < A {
    funkcja konstruktor(s) { super(s + "-B") }
}
klasa C < B {
    funkcja konstruktor(s) { super(s + "-C") }
}

pokazl C.nowy("X").s()    # "X-C-B"

Wywołanie super poza kontekstem instancji albo super.metoda w klasie bez rodzica — oba rzucają jasne błędy uruchomieniowe.

Metody i zmienne statyczne

Czasem metoda albo wartość należy do samej klasy, nie do żadnej konkretnej instancji. Oznaczaj te przez statyczna:

klasa Matematyka {
    statyczna niech PI = 3.14159
    statyczna niech E = 2.71828

    statyczna funkcja kwadrat(x) {
        zwroc x * x
    }

    statyczna funkcja pierwiastek(x) {
        zwroc x ** 0.5
    }
}

pokazl Matematyka.PI                # 3.14159
pokazl Matematyka.kwadrat(7)        # 49
pokazl Matematyka.pierwiastek(81)   # 9.0

Statyczne metody i zmienne są dziedziczone przez podklasy tak samo jak metody instancji.

Słowo kluczowe statyczna jest poprawne tylko wewnątrz ciała klasy. Użycie go na poziomie najwyższym (statyczna niech X = 10) rzuca BladSkladni: Słowo kluczowe 'statyczna' może być używane tylko w ciele klasy.

Metody prywatne

Żeby ukryć metody-szczegóły implementacyjne, na których wywołujący nie powinni polegać, oznacz je jako prywatne. Umieść słowo kluczowe prywatne w osobnej linii; każda metoda zadeklarowana po nim jest prywatna:

klasa Kalkulator {
    funkcja konstruktor() {
        niech @wynik = 0
    }

    funkcja dodaj(a, b) {
        zwroc oblicz_sume(a, b)        # OK — wywołanie wewnętrzne
    }

    prywatne

    funkcja oblicz_sume(a, b) {
        zwroc a + b
    }
}

niech k = Kalkulator.nowy()
pokazl k.dodaj(3, 4)         # 7
# k.oblicz_sume(3, 4)        # BladMetody — Próba wywołania prywatnej metody oblicz_sume

Metody prywatne mogą być wywoływane z wnętrza klasy i jej podklas, ale nie z zewnątrz. Podklasa wywołująca odziedziczoną prywatną metodę działa bez problemu — prywatność dotyczy dostępu zewnętrznego, nie dostępu wewnątrz klasy.

Klasy abstrakcyjne

Klasa abstrakcyjna to taka, której nie można instancjonować — istnieje tylko po to, żeby z niej dziedziczyć. Używaj jej, gdy chcesz zdefiniować wspólny interfejs albo dzielić częściowe zachowanie bez zobowiązywania się do konkretnego obiektu. Oznacz klasę jako abstrakcyjną przez abstrakcyjna przed klasa:

abstrakcyjna klasa Figura {
    funkcja konstruktor() {}

    funkcja pole() {
        rzuc BladMetody.nowy("pole() musi byc zaimplementowane w klasie pochodnej")
    }

    funkcja opis() {
        zwroc "Figura o polu #{sam.pole()}"
    }
}

klasa Kwadrat < Figura {
    funkcja konstruktor(bok) {
        super()
        niech @bok = bok
    }

    funkcja pole() {
        zwroc @bok * @bok
    }
}

# Figura.nowy()        # Nie można utworzyć instancji klasy abstrakcyjnej Figura
niech k = Kwadrat.nowy(5)
pokazl k.opis()        # Figura o polu 25

Oznaczenie „abstrakcyjna” w AlexScript to sprawdzenie uruchomienioweabstrakcyjna blokuje Klasa.nowy(), ale nie wymusza, żeby podklasy implementowały konkretne metody. Konwencja modelowania metod „musisz nadpisać” to napisanie domyślnej wersji rzucającej błąd (jak pole() w przykładzie powyżej).

Refleksja

Każda klasa i każda instancja odpowiadają na zestaw metod introspekcyjnych, które pozwalają sprawdzić strukturę programu w czasie wykonania. Najczęściej używane:

klasa Pojazd {
    funkcja konstruktor(marka) {
        niech @marka = marka
    }

    funkcja jedz() {}
}

klasa Samochod < Pojazd {
    funkcja zatankuj() {}
}

# Na klasie:
pokazl Samochod.nazwa()         # "Samochod"
pokazl Samochod.typ()           # "klasa"
pokazl Samochod.rodzic()        # "Pojazd"
pokazl Samochod.metody()        # ["zatankuj", ...]   włącznie z odziedziczonymi
pokazl Samochod.przodkowie()    # ["Pojazd"]
pokazl Samochod.ma_metode("jedz")    # prawda

# Na instancji:
niech s = Samochod.nowy("Toyota")
pokazl s.typ()                  # "instancja"
pokazl s.klasa()                # "Samochod"
pokazl s.czy_instancja("Pojazd")     # prawda  (dziedziczenie się liczy)
pokazl s.zmienne_instancji()         # ["marka"]
pokazl s.czy_odpowiada("jedz")       # prawda  (odziedziczona metoda)
pokazl s.identyczny(s)               # prawda

Refleksja jest najbardziej przydatna w trzech praktycznych sytuacjach: przy pisaniu generycznego serializera albo formatera, który nie wie o konkretnych klasach, przy budowaniu narzędzi debugowania i przy pisaniu pomocników testowych, które sprawdzają, czy klasa implementuje kontrakt. Pełna lista metod refleksyjnych na klasach (metody_prywatne, metody_statyczne, info_metody, potomkowie, moduly itd.) i na instancjach (wartosc_zmiennej_instancji, kopia, napis, debug_info itd.) jest długa — zawołaj obj.metody() na wartości, żeby odkryć, co jest dostępne.

Warto wiedzieć: wbudowane metody introspekcyjne takie jak id, typ, klasa i napis mogą być nadpisywane Twoimi własnymi metodami o tych samych nazwach. Jeśli Twoja klasa Uzytkownik definiuje funkcja id() { zwroc @id }, to właśnie to wywoła u.id() — wbudowana wersja jest zastąpiona. To jest celowe: Twoja klasa decyduje, co znaczą jej metody.

Moduły

Moduł to nazwany kontener na powiązany kod. Moduły w AlexScript służą dwóm celom:

Przestrzenie nazw — grupują powiązane klasy, funkcje i stałe pod wspólną nazwą, żeby uniknąć kolizji.

Mixiny — zbierają metody, które można dodać do klasy bez dziedziczenia.

Moduł jest deklarowany przez modul:

modul Matematyka {
    niech PI = 3.14159

    funkcja kwadrat(x) { zwroc x * x }

    funkcja szescian(x) { zwroc x * x * x }
}

Dostęp do członków modułu

Użyj operatora ::, żeby dostać się do stałych, funkcji i klas wewnątrz modułu z zewnątrz:

pokazl Matematyka::PI              # 3.14159
pokazl Matematyka::kwadrat(5)      # 25
pokazl Matematyka::szescian(3)     # 27

Wewnątrz modułu członkowie mogą odnosić się do siebie nawzajem bezpośrednio, bez prefiksu :::

modul Matematyka {
    niech PI = 3.14159

    funkcja pole_kola(r) {
        zwroc PI * r * r        # PI jest w tym samym module
    }
}

pokazl Matematyka::pole_kola(5)    # 78.53975

Stałe modułu

Deklaracje niech na poziomie modułu muszą używać nazw wielką literą — moduły nie mogą trzymać zmiennych pisanych małymi literami (uzasadnienie: moduł to przestrzeń nazw, nie obiekt z mutowalnym stanem). Używaj funkcji, jeśli potrzebujesz wartości obliczanych.

modul Konfiguracja {
    niech MAX_LICZBA_POLACZEN = 100      # OK
    niech NAZWA_APLIKACJI = "MyApp"      # OK

    # niech licznik = 0                  # BladSkladni
}

Tak samo jak stałe z poziomu najwyższego, stałe modułu nie mogą być przypisane ponownie po deklaracji.

Klasy wewnątrz modułów

Klasy można umieszczać wewnątrz modułów — to standardowy sposób organizowania większego programu:

modul Geometria {
    klasa Punkt {
        funkcja konstruktor(x, y) {
            niech @x = x
            niech @y = y
        }

        funkcja x() { zwroc @x }
        funkcja y() { zwroc @y }
    }

    klasa Kolo {
        funkcja konstruktor(srodek, promien) {
            niech @srodek = srodek
            niech @promien = promien
        }
    }
}

niech p = Geometria::Punkt.nowy(3, 4)
niech k = Geometria::Kolo.nowy(p, 10)

Wewnątrz ciała modułu klasy mogą się odwoływać do siebie bezpośrednio: konstruktor Kolo nie musi pisać Geometria::Punkt, wystarczy Punkt.

Kropka kontra podwójny dwukropek dla członków klasy

Dla statycznych metod i statycznych zmiennych klasy wewnątrz modułu zarówno ., jak i :: działają — są równoważne:

modul M {
    klasa K {
        statyczna niech WERSJA = 7

        statyczna funkcja info() { zwroc "K v#{WERSJA}" }
    }
}

pokazl M::K.WERSJA           # 7
pokazl M::K::WERSJA          # 7  (to samo)
pokazl M::K.info()           # K v7
pokazl M::K::info()          # K v7  (to samo)

Dla instancji zawsze używasz .M::K.nowy().metoda() — ponieważ .nowy() już zwraca instancję, a dispatch metod instancji odbywa się tylko przez kropkę.

Zagnieżdżone moduły

Moduły mogą zawierać inne moduły, a zagnieżdżać można je tak głęboko, jak wymaga tego Twoja aplikacja:

modul MojaApp {
    modul Modele {
        klasa Uzytkownik {}
        klasa Post {}
    }

    modul Kontrolery {
        klasa UzytkownicyController {}
    }

    modul Pomocniki {
        funkcja sformatuj_date(d) { zwroc d.napis() }
    }
}

niech u = MojaApp::Modele::Uzytkownik.nowy()

Używaj długiej ścieżki typu A::B::C tylko wtedy, gdy struktura naprawdę tego wymaga — większości programów wystarczy jeden albo dwa poziomy zagnieżdżenia.

Mixiny przez dolacz

Drugie zastosowanie modułów to dzielenie metod między niespokrewnionymi klasami. Klasa może wywołać dolacz (włącz) na module, co kopiuje metody modułu do klasy:

modul Identyfikator {
    funkcja id() {
        zwroc @id
    }

    funkcja id_string() {
        zwroc "ID:#{@id}"
    }
}

klasa Uzytkownik {
    dolacz Identyfikator

    funkcja konstruktor(id, imie) {
        niech @id = id
        niech @imie = imie
    }
}

klasa Post {
    dolacz Identyfikator

    funkcja konstruktor(id, tresc) {
        niech @id = id
        niech @tresc = tresc
    }
}

niech u = Uzytkownik.nowy(1, "Anna")
pokazl u.id()              # 1
pokazl u.id_string()       # ID:1

niech p = Post.nowy(42, "Treść")
pokazl p.id()              # 42

Wmieszane metody działają z pełnym dostępem do instancji: mogą czytać i modyfikować @zmienne_instancji, a sam wewnątrz metody mixinu odnosi się do instancji włączającej klasy, nie do modułu. Więc sam.klasa() z mixinu zwraca klasę, która włączyła moduł:

modul Identyfikowalny {
    funkcja kim_jestem() {
        zwroc "Jestem instancją: #{sam.klasa()}"
    }
}

klasa Osoba {
    dolacz Identyfikowalny
    funkcja konstruktor() {}
}

pokazl Osoba.nowy().kim_jestem()    # Jestem instancją: Osoba

Klasa może dolacz wiele modułów. Gdy dwa włączone moduły definiują tę samą nazwę metody, ostatni włączony wygrywa. Metody zdefiniowane bezpośrednio na klasie zawsze mają pierwszeństwo nad metodami z mixinu:

modul A {
    funkcja hello() { zwroc "from A" }
}

modul B {
    funkcja hello() { zwroc "from B" }
}

klasa K {
    dolacz A
    dolacz B    # B drugie — jego hello wygrywa
}

pokazl K.nowy().hello()    # "from B"

Kiedy dziedziczyć, kiedy mieszać

Przydatna reguła kciuka: dziedzicz, gdy nowa klasa jest rodzajem klasy nadrzędnej (Samochod jest Pojazdem). Wmieszaj moduł, gdy klasa zyskuje zdolność niezależną od jej tożsamości (Uzytkownik może być identyfikowany — ale Post też, a nie są w tej samej hierarchii).

Ponowne otwieranie modułów

Moduły w AlexScript są otwarte: możesz zadeklarować ten sam moduł w wielu miejscach, a każda deklaracja dodaje do tego samego współdzielonego modułu. To standardowy sposób dzielenia dużego modułu między wiele plików:

# w geom_core.as
modul Geometria {
    niech PI = 3.14159
}

# w geom_shapes.as
import("./geom_core.as")

modul Geometria {
    klasa Kolo {
        funkcja konstruktor(r) { niech @r = r }
        funkcja pole() { zwroc PI * @r * @r }
    }
}

Obie deklaracje współtworzą jeden moduł Geometria. Te same reguły scalania stosują się w obrębie pojedynczego pliku.

Polityka scalania zależy od rodzaju członka:

modul Operacje {
    funkcja dodaj(a, b) { zwroc a + b }
}

modul Operacje {
    funkcja odejmij(a, b) { zwroc a - b }
}

pokazl Operacje::dodaj(5, 3)     # 8
pokazl Operacje::odejmij(5, 3)   # 2

Ponowne otwieranie klas jest szczególnie potężne — możesz dodać metody do istniejącej klasy z innego pliku, a nawet istniejące instancje zobaczą nowe metody (dispatch metod jest dynamiczny):

modul M {
    klasa C {
        funkcja konstruktor() {}
        funkcja stara() { zwroc "stara" }
    }
}

niech c = M::C.nowy()    # instancja utworzona PRZED ponownym otwarciem

modul M {
    klasa C {
        funkcja nowa() { zwroc "nowa" }
    }
}

pokazl c.stara()    # "stara"
pokazl c.nowa()     # "nowa"  — widoczna na istniejącej instancji

Możesz też robić odwołania krzyżowe między blokami ponownego otwierania. Kod w drugim bloku modułu może użyć stałych, funkcji, a nawet modułów-mixinów zdefiniowanych w pierwszym bloku:

modul App {
    modul Helpers {
        funkcja powitanie() { zwroc "witaj" }
    }
}

modul App {
    klasa Controller {
        dolacz Helpers          # ← używa Helpers z poprzedniego bloku

        funkcja konstruktor() {}
        funkcja akcja() { zwroc powitanie() }
    }
}

pokazl App::Controller.nowy().akcja()    # "witaj"

Moduły jako wartości

Moduły są wartościami pierwszej klasy — możesz przypisywać je do zmiennych i sprawdzać metodami refleksyjnymi:

niech m = Matematyka

pokazl m.typ()          # "modul"
pokazl m.nazwa()        # "Matematyka"
pokazl m.stale()        # ["PI"]
pokazl m.funkcje()      # ["kwadrat", "szescian", "pole_kola"]
pokazl m.klasy()        # []
pokazl m.podmoduly()    # []

Przydatne głównie do narzędzi i metaprogramowania. Codzienny kod rzadko tego potrzebuje.

Wyjątki

Gdy coś idzie nie tak — brakujący klucz, awaria sieci, niepoprawny argument — AlexScript rzuca wyjątek. Możesz wyjątki przechwytywać i podejmować działania ratunkowe, propagować je w górę stosu wywołań albo definiować własne, żeby modelować błędy specyficzne dla domeny.

Hierarchia wyjątków

Wszystkie wyjątki dziedziczą po WyjatekPodstawowy. Klasy wbudowane to:

WyjatekPodstawowy
├── BladWykonania              # ogólny błąd uruchomieniowy
│   ├── BladTypu               # błąd związany z typem
│   ├── BladZakresu            # poza zakresem / błąd indeksu
│   ├── BladMetody             # brakująca albo niepoprawna metoda
│   ├── BladNazwy              # niezadeklarowany identyfikator
│   ├── BladArgumentu          # zła liczba albo rodzaj argumentów
│   └── BladDzieleniaPrzezZero
├── BladSkladni                # błąd składni
├── BladImportu                # błąd importu / ładowania modułu
└── BladLimituCzasu            # błąd przekroczenia limitu czasu

Te nazwy są dostępne bez żadnego importu — żyją w globalnej przestrzeni nazw od startu programu.

Łapanie błędów interpretera

Błędy pochodzące z wnętrza interpretera — dzielenie przez zero, dostęp do tablicy poza zakresem, nazwy niezdefiniowane, wywołanie nieistniejącej metody — są tłumaczone na odpowiednią klasę wyjątku AlexScript, zanim trafią do Twojego programu. Łapiesz je po nazwie AlexScript, dokładnie tak jak wyjątki, które rzucasz sam:

proba {
    niech arr = [1, 2, 3]
    pokazl arr[10]
} zlap (e : BladZakresu) {
    pokazl "Indeks poza zakresem: #{e["wiadomosc"]}"
}

Krótka referencja częstych błędów generowanych przez interpreter i tego, co je wywołuje:

Klasa Typowa przyczyna
BladDzieleniaPrzezZero 10 / 0
BladZakresu arr[100], gdy arr.dlg() < 100
BladNazwy odczyt zmiennej, która nie była zadeklarowana
BladMetody wywołanie metody, której nie ma na wartości, albo wywołanie prywatnej z zewnątrz
BladTypu zastosowanie operatora do niekompatybilnych typów, np. "abc" - 5
BladArgumentu wywołanie funkcji ze złą liczbą argumentów
BladWykonania zbiór ogólny, w tym błędy typu warunku i przepełnienie stosu

Nie musisz tego pamiętać — w razie wątpliwości łap szerszy BladWykonania (albo nawet WyjatekPodstawowy) i przeczytaj e["wiadomosc"], żeby zobaczyć dokładnie, co się stało.

Rzucanie wyjątków: rzuc

Użyj rzuc, żeby rzucić wyjątek. Akceptowane są dwie formy:

# Skrót napisowy — automatycznie opakowywany w BladWykonania:
rzuc "Coś poszło nie tak"

# Jawna instancjacja — gdy chcesz konkretnego typu:
rzuc BladTypu.nowy("Oczekiwano liczby")
rzuc BladDzieleniaPrzezZero.nowy("Mianownik nie moze byc zerem")

Komunikat konstruktora domyślnie to "Błąd", jeśli go pominiesz.

Łapanie: proba / zlap / wkoncu

Konstrukcja łapania ma trzy słowa kluczowe:

proba {
    niech wynik = ryzykowna_operacja()
    pokazl wynik
} zlap (e) {
    pokazl "Złapano: #{e["wiadomosc"]}"
}

Wewnątrz bloku łapiącego zmienna łapiąca (tutaj e) jest przypisana do obiektu wyjątku — opisanego niżej.

Łapanie typowane

Możesz zawęzić klauzulę łapiącą do konkretnej klasy wyjątku (i jej podklas), dając po zmiennej : i nazwę typu:

proba {
    rzuc BladTypu.nowy("zly typ")
} zlap (e : BladTypu) {
    pokazl "Błąd typu: #{e["wiadomosc"]}"
}

Wiele klauzul zlap jest dopasowywanych od góry do dołu — pierwsza pasująca wygrywa. Stawiaj bardziej specyficzne typy najpierw, bardziej ogólne na końcu:

proba {
    operacja()
} zlap (e : BladDzieleniaPrzezZero) {
    pokazl "Dzielenie przez zero!"
} zlap (e : BladTypu) {
    pokazl "Błąd typu: #{e["wiadomosc"]}"
} zlap (e) {
    pokazl "Inny błąd: #{e["wiadomosc"]}"
}

zlap (e) bez typu pasuje do wszystkiego — działa jak łapacz wszystkiego i powinien być umieszczony na końcu.

Jeśli żadna klauzula nie pasuje, wyjątek dalej propaguje się w górę stosu wywołań — dokładnie tak, jakby proba w ogóle nie było.

Typy wyjątków kwalifikowane modułem

Gdy definiujesz własne klasy wyjątków wewnątrz modułu, klauzula łapiąca może odnosić się do nich pełną kwalifikowaną ścieżką:

modul MojaApp {
    klasa BladDanych < WyjatekPodstawowy {
        funkcja konstruktor(k) { super(k) }
    }
}

proba {
    rzuc MojaApp::BladDanych.nowy("zła wartość")
} zlap (e : MojaApp::BladDanych) {
    pokazl "Błąd danych: #{e["wiadomosc"]}"
}

Stosuje się ta sama reguła dopasowania podklas — zlap (e : MojaApp::BladDanych) pasuje do samego typu i każdej z jego podklas.

Sprzątanie przez wkoncu

Blok wkoncu uruchamia się po chronionym bloku, niezależnie od tego, czy chroniony blok ukończył się normalnie, został złapany przez zlap, czy rzucił coś, do czego żadna klauzula zlap nie pasowała. To właściwe miejsce na sprzątanie, które musi się zawsze odbyć:

proba {
    otworz_plik()
    przetworz()
} zlap (e) {
    pokazl "Błąd: #{e["wiadomosc"]}"
} wkoncu {
    zamknij_plik()
}

wkoncu jest też poprawne bez żadnych klauzul zlap — gdy chcesz gwarantowanego sprzątania, ale bez specjalnej obsługi błędów na tym poziomie:

proba {
    pokazl "robie cos"
} wkoncu {
    pokazl "sprzatam"
}

Obiekt wyjątku

Zmienna łapiąca to hash z następującymi kluczami:

proba {
    rzuc BladTypu.nowy("zly typ")
} zlap (e) {
    pokazl e["wiadomosc"]    # "zly typ"
    pokazl e["typ"]          # nazwa typu jako napis
    pokazl e["klasa"]        # "BladTypu"
    pokazl e["linia"]        # numer linii, gdzie został rzucony
    pokazl e["instancja"]    # oryginalna instancja wyjątku
    pokazl e["stos"]         # stos wywołań w momencie rzutu
}

e["wiadomosc"] to to, czego będziesz używać najczęściej. e["stos"] jest przydatny do diagnostyki — to tablica napisów, jeden na ramkę stosu, najnowsza ramka pierwsza. e["instancja"] daje Ci oryginalną instancję wyjątku, więc dowolne metody, które zdefiniowałeś na swojej własnej klasie wyjątku, są przez nią wywoływalne.

Definiowanie własnych wyjątków

Własne wyjątki to zwykłe klasy — jedyny wymóg jest taki, żeby dziedziczyły (bezpośrednio albo tranzytywnie) po WyjatekPodstawowy albo jednej z jej podklas. Najprostszy przypadek: puste ciało dziedziczące domyślny konstruktor:

klasa MojWyjatek < WyjatekPodstawowy {}

rzuc MojWyjatek.nowy("Coś się zepsuło")

Budowanie hierarchii działa dokładnie jak zwykłe dziedziczenie, a zlap (e : ...) pasuje do dowolnej podklasy wymienionego typu:

klasa BladAplikacji < WyjatekPodstawowy {}
klasa BladDanych < BladAplikacji {}
klasa BladBazyDanych < BladDanych {}
klasa BladSieci < BladAplikacji {}

proba {
    operacja()
} zlap (e : BladBazyDanych) {
    # bardzo specyficzne
} zlap (e : BladDanych) {
    # dowolny błąd danych poza DB
} zlap (e : BladAplikacji) {
    # dowolny błąd aplikacji
}

Własne wyjątki mogą mieć swój stan i metody. Nadpisz konstruktor, żeby przyjmował dodatkowe informacje:

klasa BladHTTP < WyjatekPodstawowy {
    funkcja konstruktor(wiadomosc, kod) {
        super(wiadomosc)
        niech @kod = kod
    }

    funkcja kod() {
        zwroc @kod
    }
}

proba {
    rzuc BladHTTP.nowy("Not Found", 404)
} zlap (e : BladHTTP) {
    pokazl e["wiadomosc"]              # "Not Found"
    pokazl e["instancja"].kod()        # 404
}

Częsty wzorzec to definiowanie hierarchii wyjątków swojej aplikacji wewnątrz modułu — grupuje powiązane błędy i daje Ci pełne kwalifikowane nazwy, które nie zderzą się z czyimiś innymi:

modul Posel {
    klasa BladPosla < WyjatekPodstawowy {
        funkcja konstruktor(k) { super(k) }
    }
    klasa BladHttp < BladPosla {
        funkcja konstruktor(k) { super(k) }
    }
    klasa BladNieZnaleziono < BladHttp {
        funkcja konstruktor(k) { super(k) }
    }
}

proba {
    rzuc Posel::BladNieZnaleziono.nowy("404!")
} zlap (e : Posel::BladHttp) {
    pokazl "Błąd HTTP: #{e["wiadomosc"]}"
}

Ponowne rzucanie i opakowywanie

Blok zlap może sam zawierać rzuc. To sposób na ponowne rzucenie (być może po opakowaniu) albo na zamianę niskopoziomowej awarii w domenowy błąd:

proba {
    polacz_z_baza()
} zlap (e) {
    rzuc BladBazyDanych.nowy("Nie udalo sie polaczyc: #{e["wiadomosc"]}")
}

Nowy wyjątek leci w górę do następnego otaczającego proba (albo z programu, jeśli takiego nie ma).

Zagnieżdżone bloki proba komponują się naturalnie — wewnętrzny łapacz może albo obsłużyć, albo przekształcić-i-rzucić ponownie, albo pozwolić wyjątkowi dotrzeć do zewnętrznego łapacza:

proba {
    pokazl "Zewnętrzny try"
    proba {
        rzuc "Wewnętrzny wyjątek"
    } zlap (e) {
        pokazl "Wewnętrzny catch: #{e["wiadomosc"]}"
        rzuc "Zewnętrzny wyjątek"      # rzuć ponownie
    }
} zlap (e) {
    pokazl "Zewnętrzny catch: #{e["wiadomosc"]}"
}

Programowanie asynchroniczne

AlexScript wspiera kooperatywne programowanie asynchroniczne przez polską odmianę modelu async/await znanego z JavaScriptu i Pythona. Asynchroniczność pozwala wyrazić współbieżne operacje — wiele rzeczy dziejących się jednocześnie — bez złożoności wątków i współdzielonej pamięci.

Kluczowa idea: funkcja asynchroniczna może zawiesić swoje wykonanie w dobrze zdefiniowanych punktach (czekając na zegar, odpowiedź HTTP, zapytanie do bazy danych) i pozwolić, żeby inna praca posuwała się w tym samym wątku. Gdy oczekiwana rzecz jest gotowa, funkcja wznawia się w miejscu, w którym przerwała.

Pod spodem AlexScript używa fiberów Ruby’ego i kooperatywnego reaktora z Fiber Schedulerem. Cała praca asynchroniczna biegnie na pojedynczym wątku; współbieżność bierze się z oddawania sterowania w punktach zawieszenia, nie z paralelizmu. Co istotne, scheduler obejmuje też blokujące natywne I/O — sleep, odczyty z socketów i podobne operacje — te kooperatywnie oddają sterowanie zamiast blokować cały reaktor.

asynchroniczna i czekaj

Funkcję asynchroniczną deklaruje się przez asynchroniczna przed funkcja (albo fn dla asynchronicznej lambdy):

asynchroniczna funkcja pobierz_dane() {
    czekaj uspij(100)
    zwroc "gotowe"
}

Wywołanie funkcji asynchronicznej nie wykonuje ciała do końca. Zamiast tego zwraca Obietnica (promise) — obiekt reprezentujący „wartość, która będzie dostępna później” — i planuje uruchomienie ciała kooperatywnie.

Słowo kluczowe czekaj zawiesza bieżącą funkcję asynchroniczną do momentu, aż obietnica się rozstrzygnie, a potem zwraca jej wartość:

asynchroniczna funkcja main() {
    niech wynik = czekaj pobierz_dane()
    pokazl wynik    # gotowe
}

czekaj jest poprawne tylko wewnątrz funkcji asynchronicznej. Parser wymusza to statycznie: użycie czekaj na poziomie najwyższym, w funkcji synchronicznej albo w synchronicznej lambdzie fn to BladSkladni łapany w czasie parsowania, jeszcze zanim Twój program się uruchomi. Błąd mówi Ci dokładnie, co zrobić — opakuj wywołanie w funkcję asynchroniczną i użyj uruchom, żeby wejść w świat asynchroniczny z kodu synchronicznego.

Samo asynchroniczna musi być po nim funkcja albo fn. Pisanie asynchroniczna niech x = 1 to błąd składni.

uruchom — punkt wejścia

Kod na poziomie najwyższym (synchroniczny) nie może używać czekaj bezpośrednio. Żeby uruchomić pracę asynchroniczną z poziomu najwyższego, użyj uruchom:

asynchroniczna funkcja main() {
    czekaj uspij(500)
    zwroc "po pol sekundzie"
}

pokazl uruchom(main)    # po pol sekundzie

uruchom blokuje wątek wywołujący do momentu, aż dana funkcja asynchroniczna (albo obietnica) się rozstrzygnie, a potem zwraca jej wartość. Akceptuje trzy formy:

Przekazanie czegokolwiek innego — liczby, napisu, funkcji synchronicznej — rzuca błąd w stylu uruchom oczekuje obietnicy lub funkcji asynchronicznej.

Program zwykle ma dokładnie jedno wywołanie uruchom na poziomie najwyższym — Twoją asynchroniczną funkcję main — a wszystko inne płynie z tego.

uspij — kooperatywne opóźnienie

uspij(ms) zwraca obietnicę, która się spełnia po ms milisekundach. Połącz z czekaj, żeby zatrzymać funkcję asynchroniczną bez blokowania całego programu:

asynchroniczna funkcja powolne_powitanie() {
    pokazl "raz"
    czekaj uspij(1000)
    pokazl "dwa"
    czekaj uspij(1000)
    pokazl "trzy"
}

uruchom(powolne_powitanie)

W przeciwieństwie do blokującego sleepa, uspij pozwala innym zadaniom asynchronicznym działać w trakcie opóźnienia. Wiele funkcji asynchronicznych może uspij jednocześnie, a każda obudzi się niezależnie. uspij(0) jest też poprawne — daje kooperatywne ustąpienie bez rzeczywistego opóźnienia, przydatne, gdy chcesz dać innym zadaniom szansę na ruch.

Wywołanie uspij z argumentem nieliczbowym rzuca uspij oczekuje liczby.

Uruchamianie zadań równolegle: uruchom_rownolegle

Żeby uruchomić wiele zadań asynchronicznych współbieżnie, użyj uruchom_rownolegle. Przyjmuje funkcję bez argumentów i zwraca obietnicę dla jej wyniku:

asynchroniczna funkcja pobierz(id) {
    czekaj uspij(100)        # symulacja zapytania sieciowego
    zwroc id * 2
}

asynchroniczna funkcja main() {
    niech a = uruchom_rownolegle(fn() { czekaj pobierz(1) })
    niech b = uruchom_rownolegle(fn() { czekaj pobierz(2) })
    niech c = uruchom_rownolegle(fn() { czekaj pobierz(3) })

    # Wszystkie trzy wywołania pobierz biegną współbieżnie.
    pokazl czekaj a    # 2
    pokazl czekaj b    # 4
    pokazl czekaj c    # 6
}

uruchom(main)

Bez uruchom_rownolegle trzy sekwencyjne wywołania czekaj pobierz(...) zajęłyby łącznie ~300 ms. Z nim się nakładają — całkowity czas zegarowy to ~100 ms.

Kolejność, w której równoległe fibery się kończą, zależy od ich pracy — fibery przeplatają się w punktach czekaj, więc szybszy może skończyć przed wolniejszym, mimo że oba wystartowały razem:

asynchroniczna funkcja wolny() {
    czekaj uspij(60)
    pokazl "wolny"
}
asynchroniczna funkcja szybki() {
    czekaj uspij(20)
    pokazl "szybki"
}

asynchroniczna funkcja main() {
    niech k = uruchom_rownolegle(fn() { czekaj wolny() })
    niech s = uruchom_rownolegle(fn() { czekaj szybki() })
    czekaj k
    czekaj s
}

uruchom(main)
# Wyjście:
# szybki
# wolny

Subtelny punkt warty zapamiętania: sekcja kodu synchronicznego wewnątrz funkcji asynchronicznej biegnie bez oddawania sterowania — oddanie zachodzi tylko na czekaj. Więc trzy pokazl pod rząd wypiszą się w kolejności; dopiero czekaj daje innym fiberom szansę na przeplot.

Klasa Obietnica

Obietnica to wartość pierwszej klasy reprezentująca ostateczny wynik operacji asynchronicznej. Ma trzy możliwe stany:

Obietnica rozstrzyga się co najwyżej raz. Po spełnieniu albo odrzuceniu jej stan i wartość się nie zmieniają.

Możesz odpytywać stan obietnicy bezpośrednio bez czekania na nią:

asynchroniczna funkcja main() {
    niech p = pobierz_dane()       # obietnica, jeszcze oczekująca
    pokazl p.stan()                 # "oczekuje"
    czekaj p
    pokazl p.stan()                 # "spelniona"
    pokazl p.wartosc()              # spełniona wartość
}

Metody instancji na Obietnica:

Tworzenie obietnic bezpośrednio

Do testów jednostkowych, prostych pomocników asynchronicznych albo do mostkowania API nie-promise’owych, możesz tworzyć obietnice w już rozstrzygniętym stanie:

niech ok = Obietnica.spelniona(42)
niech zle = Obietnica.odrzucona("blad")

pokazl ok.stan()      # "spelniona"
pokazl ok.wartosc()   # 42
pokazl zle.stan()     # "odrzucona"
pokazl zle.powod()    # "blad"

Czekanie na spełnioną obietnicę zwraca jej wartość natychmiast, bez oddawania sterowania. Czekanie na odrzuconą obietnicę rzuca powód odrzucenia jako wyjątek, który możesz złapać przez proba/zlap jak każdy inny błąd:

asynchroniczna funkcja main() {
    proba {
        czekaj Obietnica.odrzucona("problem")
    } zlap (e) {
        pokazl "zlapane: #{e["wiadomosc"]}"
    }
}

uruchom(main)

Przyjemny lukier: czekaj na wartości, która nie jest obietnicą, zwraca tę wartość bez zmian. czekaj 42 to po prostu 42. Oznacza to, że nie musisz w swoich pomocnikach traktować specjalnie sytuacji „czy to obietnica, czy zwykła wartość” — czekaj zrobi właściwą rzecz w obu przypadkach.

Wzorzec executora: Obietnica.nowy

Do bardziej zaawansowanych przypadków — opakowywania API opartego na callbackach, odraczania rozstrzygnięcia do innego fibera — używaj formy executora. Obietnica.nowy przyjmuje funkcję, która dostaje dwa callbacki: spelnij i odrzuc. Executor biegnie synchronicznie; wywołanie któregoś z callbacków rozstrzyga obietnicę:

asynchroniczna funkcja main() {
    niech p = Obietnica.nowy(fn(spelnij, odrzuc) {
        spelnij(42)
    })
    zwroc czekaj p
}

pokazl uruchom(main)    # 42

Executor może odraczać rozstrzygnięcie, odpalając równoległy fiber:

asynchroniczna funkcja main() {
    niech p = Obietnica.nowy(fn(spelnij, odrzuc) {
        uruchom_rownolegle(fn() {
            czekaj uspij(20)
            spelnij("po opóźnieniu")
        })
    })
    zwroc czekaj p
}

pokazl uruchom(main)    # po opóźnieniu

Dwa istotne zachowania wzorca executora:

Obietnica rozstrzyga się co najwyżej raz. Po pierwszym wywołaniu spelnij albo odrzuc kolejne wywołania są po cichu ignorowane:

niech p = Obietnica.nowy(fn(spelnij, odrzuc) {
    spelnij("pierwszy")
    spelnij("drugi")        # no-op
    odrzuc("ignored")        # no-op
})
pokazl czekaj p    # "pierwszy"

Wyjątki rzucone z ciała executora automatycznie stają się odrzuceniami — nie musisz łapać i wywoływać odrzuc samodzielnie:

niech p = Obietnica.nowy(fn(spelnij, odrzuc) {
    rzuc BladWykonania.nowy("wybuch w executorze")
})

proba {
    czekaj p
} zlap (e) {
    pokazl "zlapane: #{e["wiadomosc"]}"
}

Kombinatory obietnic

Do koordynowania wielu obietnic klasa Obietnica udostępnia trzy statyczne kombinatory znane z API Promise w JavaScripcie.

Obietnica.wszystkie

Czekaj, aż wszystkie obietnice zostaną spełnione, zwróć ich wartości w pierwotnej kolejności. Odrzuca szybko, jeśli któreś wejście odrzuci.

asynchroniczna funkcja a() { czekaj uspij(30); zwroc "a" }
asynchroniczna funkcja b() { czekaj uspij(10); zwroc "b" }
asynchroniczna funkcja c() { czekaj uspij(20); zwroc "c" }

asynchroniczna funkcja main() {
    niech wyniki = czekaj Obietnica.wszystkie([a(), b(), c()])
    zwroc wyniki    # ["a", "b", "c"] — kolejność zachowana mimo różnego czasu
}

pokazl uruchom(main)

Pusta tablica spełnia się natychmiast z []. Wartości nie-promise’owe w tablicy są traktowane jako już spełnione, więc możesz mieszać obietnice z literałami:

niech wyniki = czekaj Obietnica.wszystkie([a(), "surowy", 42])
# ["a", "surowy", 42]

Obietnica.dowolna

Czekaj, aż pierwsza obietnica zostanie spełniona, zwróć jej wartość. Wolniejsze biegną dalej, ale ich wyniki są odrzucane.

asynchroniczna funkcja wolny() {
    czekaj uspij(100)
    zwroc "wolny"
}
asynchroniczna funkcja szybki() {
    czekaj uspij(20)
    zwroc "szybki"
}

asynchroniczna funkcja main() {
    zwroc czekaj Obietnica.dowolna([wolny(), szybki()])
}

pokazl uruchom(main)    # "szybki"

Przydatne dla wyścigów typu „najszybsze z wielu źródeł danych” albo „najszybciej-odpowiadający fallback”. Jeśli każda obietnica w tablicy odrzuci, dowolna też odrzuci.

Obietnica.limit_czasu

Opakuj obietnicę limitem czasu. Zwraca obietnicę, która spełnia się oryginalną wartością, jeśli rozstrzygnie się na czas, albo odrzuca błędem przekroczenia limitu w przeciwnym razie.

asynchroniczna funkcja powolne_zapytanie() {
    czekaj uspij(500)
    zwroc "wynik"
}

asynchroniczna funkcja main() {
    proba {
        niech wynik = czekaj Obietnica.limit_czasu(powolne_zapytanie(), 100)
        pokazl wynik
    } zlap (e) {
        pokazl "Przekroczony limit czasu"
    }
}

uruchom(main)

Jeśli opakowywana obietnica sama odrzuci (szybciej niż limit), oryginalne odrzucenie propaguje — nie tracisz błędu.

Obsługa błędów w kodzie asynchronicznym

Obietnica kończąca się wyjątkiem jest odrzucona. Gdy czekaj na odrzuconej obietnicy, odrzucenie jest ponownie rzucane jako zwykły wyjątek — łapane tym samym proba/zlap, którego użyłbyś dla kodu synchronicznego:

asynchroniczna funkcja moze_sie_zepsuc() {
    czekaj uspij(50)
    rzuc BladWykonania.nowy("cos poszlo nie tak")
}

asynchroniczna funkcja main() {
    proba {
        czekaj moze_sie_zepsuc()
    } zlap (e) {
        pokazl "zlapane: #{e["wiadomosc"]}"
    }
}

uruchom(main)

To dlatego kod asynchroniczny czyta się prawie identycznie jak synchroniczny — jedyna różnica to słowa kluczowe asynchroniczna/czekaj. Sterowanie przepływem, obsługa błędów i manipulacja danymi działają tak samo.

Jeśli funkcja asynchroniczna rzuci, a nic w jej środku nie złapie wyjątku, obietnica, którą zwróciła, kończy w stanie odrzuconym. Czekanie na nią później rzuca wyjątek ponownie w punkcie czekaj.

Nieobsłużone odrzucenia

Jeśli obietnica zostanie odrzucona i nikt nigdy na nią nie poczeka, AlexScript wypisze ostrzeżenie na stderr na końcu programu. Łapie to częsty błąd — zapomnienie o czekaj na zadaniu równoległym, którego błąd chciałbyś znać:

asynchroniczna funkcja main() {
    uruchom_rownolegle(fn() {
        rzuc BladWykonania.nowy("ignored error")
    })
    czekaj uspij(200)
    zwroc "done"
}

uruchom(main)
# stdout:  done
# stderr:  nieobsluzone odrzucenie: ignored error

Gdy raz czekaj na obietnicy (nawet po to, żeby tylko ją zaobserwować), odrzucenie jest uznawane za obsłużone — nawet jeśli natychmiast łapiesz i wyrzucasz. Sprawdzenie .stan() nie liczy się jako obsługa; tylko czekaj.

Metody asynchroniczne na klasach

Możesz deklarować metody asynchroniczne wewnątrz klasy:

klasa Klient {
    funkcja konstruktor(adres) {
        niech @adres = adres
    }

    asynchroniczna funkcja pobierz() {
        czekaj uspij(100)
        zwroc "dane z #{@adres}"
    }
}

asynchroniczna funkcja main() {
    niech k = Klient.nowy("api.example.com")
    pokazl czekaj k.pobierz()
}

uruchom(main)

sam działa wewnątrz metod asynchronicznych tak jak w zwykłych metodach. Wiele wywołań asynchronicznych na tej samej instancji dzieli stan instancji — zmienne instancji utrzymują się przez granice czekaj w obrębie tej samej instancji:

klasa Akumulator {
    funkcja konstruktor() { niech @suma = 0 }

    asynchroniczna funkcja dodaj(x) {
        czekaj uspij(5)
        @suma = @suma + x
        zwroc @suma
    }
}

asynchroniczna funkcja main() {
    niech a = Akumulator.nowy()
    czekaj a.dodaj(10)
    czekaj a.dodaj(20)
    zwroc czekaj a.dodaj(30)
}

pokazl uruchom(main)    # 60

Asynchroniczność z natywnym I/O

Kooperatywny scheduler obejmuje blokujące operacje natywne — sleep, odczyty z socketów, I/O na plikach. Oznacza to, że możesz pisać coś, co wygląda na zwykły blokujący kod, uruchamiać wiele jego instancji w równoległych fiberach, a one będą się kooperatywnie przeplatać zamiast serializować:

import("socket")

asynchroniczna funkcja pobierz(port) {
    niech s = SocketTcp.nowy("127.0.0.1", port)
    niech dane = s.czytaj_linie()
    s.zamknij()
    zwroc dane
}

asynchroniczna funkcja main() {
    niech a = uruchom_rownolegle(fn() { czekaj pobierz(8080) })
    niech b = uruchom_rownolegle(fn() { czekaj pobierz(8080) })
    pokazl czekaj a
    pokazl czekaj b
}

uruchom(main)

Oba wywołania czytaj_linie() blokują fiber, który je wystawił, ale scheduler trzyma reaktor w ruchu — więc drugi fiber może posuwać się do przodu, gdy pierwszy czeka. To właśnie sprawia, że AlexScript nadaje się do serwerów i klientów sieciowych bez wątków i callbacków.

Kiedy używać asynchroniczności

Asynchroniczność jest właściwym narzędziem, gdy masz pracę ograniczoną przez I/O, która inaczej by blokowała — zapytania sieciowe, operacje na plikach, zegary, czekanie na wejście. Wiele niezależnych operacji I/O może biec współbieżnie w czasie, w którym jedna z nich biegłaby w kodzie synchronicznym.

Asynchroniczność nie jest właściwym narzędziem dla pracy ograniczonej przez CPU. Ponieważ wszystkie zadania asynchroniczne dzielą jeden wątek, ciasna pętla numeryczna zablokuje każdą inną operację asynchroniczną, dopóki się nie skończy. Dla paralelizmu opartego na CPU sięgasz po prawdziwe wątki — których AlexScript obecnie nie wystawia kodowi użytkownika.

Mieszaj asynchroniczność oszczędnie z kodem synchronicznym w tym samym programie. Najklarowniejszy wzorzec: trzymaj większość kodu synchronicznym, a funkcje asynchroniczne umieszczaj na granicy, gdzie odbywa się I/O. Miej jedno wywołanie uruchom(main) na poziomie najwyższym, które zarządza całym cyklem życia asynchronicznym.

Wyrażenia regularne

Do dopasowywania wzorców na napisach AlexScript udostępnia klasę Wyrazenie — obiekty wyrażeń regularnych jako wartości pierwszej klasy. Skonstruuj wzorzec raz przez Wyrazenie.nowy(wzor), a potem wykorzystuj go wielokrotnie do dopasowywania, zamiany i rozdzielania.

niech wz = Wyrazenie.nowy("[0-9]+")

pokazl wz.pasuje("rok 2026")              # prawda  (dopasowanie podnapisu)
pokazl wz.dopasuj("rok 2026").tekst()     # "2026"
pokazl wz.skanuj("ma 3 koty i 2 psy")     # ["3", "2"]
pokazl wz.zamien_wszystkie("a1 b2 c3", "#")    # "a# b# c#"

Flagi możesz przekazać jako opcjonalny drugi argument — "i" dla nieczułości na wielkość liter, "m" dla trybu wieloliniowego, "x" dla trybu rozszerzonego (ignorującego białe znaki):

niech wz = Wyrazenie.nowy("hello", "i")
pokazl wz.pasuje("HELLO")    # prawda

Metoda dopasuj zwraca obiekt Dopasowanie (albo nic, jeśli nie ma dopasowania) z informacjami o dopasowaniu:

niech wz = Wyrazenie.nowy("(\\w+)=(\\w+)")
niech d = wz.dopasuj("name=alex")

pokazl d.tekst()      # "name=alex"
pokazl d.grupa(1)     # "name"
pokazl d.grupa(2)     # "alex"
pokazl d.indeks()     # 0

Nazwane grupy przechwytujące są wspierane przez (?<nazwa>...):

niech wz = Wyrazenie.nowy("(?<klucz>\\w+)=(?<wartosc>\\w+)")
niech d = wz.dopasuj("name=alex")
pokazl d.nazwana("klucz")     # "name"
pokazl d.nazwana("wartosc")   # "alex"

Dopasowanie udostępnia też przed() i po() dla tekstu przed i po dopasowaniu, grupy() dla wszystkich grup przechwytujących jako tablicy, nazwane() dla wszystkich nazwanych przechwyceń jako obiektu i indeks_konca() dla pozycji końcowej.

Gdy budujesz wzorzec z wejścia użytkownika albo dowolnego niezaufanego źródła, najpierw eskejpuj go, żeby zneutralizować metaznaki regexa:

niech termin = wczytaj("Wyszukaj: ")
niech wz = Wyrazenie.nowy(Wyrazenie.escapuj(termin), "i")

Dla wygody trzy najczęstsze operacje są też dostępne bezpośrednio na napisach:

"123".pasuje(Wyrazenie.nowy("^[0-9]+$"))                     # prawda
"the 2026 year".dopasuj(Wyrazenie.nowy("[0-9]+")).tekst()    # "2026"
"a, b, c".rozdziel(Wyrazenie.nowy(",\\s*"))                  # ["a", "b", "c"]

Do powtarzanego dopasowywania zawsze konstruuj wzorzec raz, poza pętlą. Wzorce są kompilowane w czasie konstrukcji, a ponowne kompilowanie w każdej iteracji to cicha pułapka wydajnościowa.

Debugger

AlexScript jest wyposażony we wbudowany interaktywny debugger inspirowany ruby’owym byebug. Pozwala wstrzymać wykonanie w dowolnym punkcie, sprawdzać i modyfikować zmienne, kroczyć przez kod linia po linii, ustawiać breakpointy, obserwować wartości i obliczać wyrażenia w czasie rzeczywistym.

Aktywowanie debuggera

Umieść wywołanie debug() gdziekolwiek w swoim kodzie. Gdy wykonanie do niego dotrze, program się wstrzymuje i wpada w interaktywny REPL debuggera:

niech x = 10
niech y = 20
debug()
niech z = x + y
pokazl z

Gdy program się uruchomi, zobaczysz coś w stylu:

⏺  debug() w test.as:3
     1 | niech x = 10
     2 | niech y = 20
  => 3 | debug()
     4 | niech z = x + y
     5 | pokazl z
debug>

Znacznik => wskazuje bieżącą linię. Jesteś teraz w debuggerze i możesz wydawać komendy.

Możesz też wywołać debug() warunkowo — jesli problem to debug() to przydatny wzorzec na zatrzymywanie się tylko wtedy, gdy dzieje się coś interesującego.

Gdy nie napotkano żadnego debug(), debugger ma zerowy wpływ na wydajność Twojego programu.

Sterowanie wykonaniem

Po wstrzymaniu kontrolujesz, jak wznawia się wykonanie:

Sprawdzanie stanu

Wpisz dowolne wyrażenie AlexScript w prompcie, a zostanie obliczone w bieżącym zasięgu:

debug> x
  => 10
debug> x + y
  => 30
debug> arr.dlg()
  => 5

Żeby wylistować wszystkie zmienne w bieżącym zasięgu pogrupowane według kategorii, użyj zmienne (albo z):

debug> zmienne
  [Zmienne lokalne]
    x = 10
    y = 20
  [Zmienne instancji]  (Kalkulator)
    @wynik = 30

zmienne wszystkie przechodzi przez cały łańcuch zasięgów (bieżący → rodzic → globalny) i pokazuje zmienne na każdym poziomie.

Żeby zobaczyć stos wywołań, użyj stos (albo s):

debug> stos
  [Stos wywołań]
    0: Kalkulator#dodaj (kalkulator.as:15)
    1: funkcja oblicz (main.as:8)
    2: funkcja start (main.as:3)

Żeby zobaczyć kod źródłowy w okolicy bieżącej linii, użyj kod.

Modyfikowanie zmiennych

Możesz modyfikować istniejące zmienne prosto z debuggera:

debug> x
  => 10
debug> x = 42
debug> x
  => 42

Program kontynuuje ze zmodyfikowaną wartością. (Z debuggera nie możesz deklarować nowych zmiennych przez niech — tylko modyfikować istniejące.)

Breakpointy, watchpointy, logpointy

Debugger wspiera kilka rodzajów breakpointów:

Listuj aktywne breakpointy przez punkty. Usuwaj je przez usun N, usun_metode N, usun_sledz N albo usun_loguj N.

Te funkcje czynią debugger czymś więcej niż narzędziem do krokowania — to faktycznie środowisko eksploracji uruchomieniowej, w którym dowiadujesz się, co Twój program rzeczywiście robi.

Przegląd biblioteki standardowej

Biblioteka standardowa AlexScript jest celowo niewielka, ale praktyczna. Każdą bibliotekę wczytuje się przez import("nazwa") (bez prefiksu ścieżki). Biblioteki to:

Mały przykład łączący kilka z nich — wczytaj plik JSON, przekształć dane, zapisz z powrotem:

import("json")
import("plik")

niech dane = Json.parsuj_plik("./osoby.json")
niech dorosli = dane.filtruj(fn(o) { o["wiek"] >= 18 })
Json.generuj_plik("./dorosli.json", dorosli, prawda)

pokazl "Zapisano #{dorosli.dlg()} rekordow."

Każda biblioteka ma swoją własną dedykowaną dokumentację — ten przegląd to tylko mapa tego, co jest dostępne.

Co dalej

Masz teraz pełen obraz: zmienne i typy, sterowanie przepływem, funkcje i domknięcia, klasy i moduły, wyjątki, asynchroniczność, wyrażenia regularne i debugger. Najlepszy sposób, żeby to wszystko sobie przyswoić, to coś zbudować — małe narzędzie konsolowe, skrypt napędzany JSON-em, malutki serwer HTTP, kalkulator, parser. Biblioteka standardowa AlexScript ma wystarczająco do każdego z nich.

Kilka sugestii, jak iść dalej:

Używaj REPL-a agresywnie. Próbuj języka interaktywnie, zanim zatwierdzisz rzeczy do pliku — większość cech języka da się zbadać w pojedynczej sesji, a zmienna _ na ostatni wynik czyni eksperymentowanie szybkim.

Czytaj kod .as innych ludzi. Wzorce takie jak serwer HTTP Zubr (router, parser, stos middleware i handler połączeń, wszystko napisane w AlexScript) pokazują, jak język skaluje się do nietrywialnych systemów.

Używaj debuggera, gdy utkniesz. Krokowanie przez kod linia po linii często jest najszybszym sposobem na zrozumienie, co rzeczywiście się dzieje, a integracja debug() w AlexScript jest o jedną linijkę.

Gdy zachowanie Cię zaskoczy, napisz dla niego mały test — jednoplikowy program .as demonstrujący przypadek. Z czasem stanie się to Twoją osobistą referencją „rzeczy, których nauczyłem się na własnych błędach”.

Witaj w AlexScript. Baw się dobrze.