Advent of Code 2022: Lua


Advent of Code to wyzwanie, które podejmuję już od kilku lat. W tym roku udało mi się wytrwać 18 dni (z czego 15 dni do złotych gwiazdek), ale nie o tym. Jak co roku poza oczywistym wyzwaniem pod postacią wyborną fabułę zawierających zadań dodałem kilka własnych.

Moja codzienna rutyna przez początkowe dni to pobudka o 5:00-ish, kawa, rozpoczęcie robienia zadania ok. 6:00, submit, śniadanie i daily. Czy zdrowo? Raczej nie. Czy sportowo? Też raczej nie. Ale czy było efektywnie? Też nie. Ale o iluzji produktywności napiszę innym razem.

Side-questy

  1. Każdego roku wybierać nowy (względem wyzwania) język programowania. W 2021 roku padło na C++. Rok 2022 jest rokiem języka Lua.
  2. W tym roku dodatkowym wyzwaniem było użycie wersji Preview nowego edytora kodu od JetBrains – Fleet, o którym bardziej szczegółowo tak, ale nie teraz.
  3. Starać się nie używać ułatwiających zadanie bibliotek,
    • co oznacza:
      • żadnych bibliotek implementujących algorytmy lub
      • kodu znalezionego na StackOverflow i innych miejscach z gotowymi rozwiązaniami
    • za wyjątkiem:
      • bibliotek wchodzących w skład definicji języka (np. stdlib c++) lub
      • zawierających poważną matematykę (np. BigNum, sin/cos, you name it) lub
      • przez tradycję niemalże wcielonych w język (dla Javy byłby to Lombok, Apache Commons itp.).
  4. Nie wyciągać z danych informacji o danych, zwłaszcza metodą organoleptyczną.
  5. Nie wczytywać wszystkich punktów z map (działać na macierzach rzadkich).
  6. Poznać jak najwięcej tego, co daje wybrany język programowania.
  7. Do końca wyzwania commitować kod w stanie brudnym (łącznie z bezczelnymi printami).
  8. Po wyzwaniu przejrzeć kod, wyciągnąć wnioski, ewentualnie poprawić.

Odkrycia

Dzień 1: Indeksowanie od 1

Indeksowanie tablic w Lua wygląda tak:

-- Indeksowanie od 1
tablica = {}
tablica[0] = 'Błąd'
tablica[1] = 'To zadziała'
tablica[#tablica] = 'To nadpisuje ostatni element'
tablica[#tablica + 1] = 'A tutaj dodajemy kolejny element'

Wprawne oko zauważy kontrowersyjną rzecz, a mianowicie indeksowanie od 1. Ale czy rzeczywiście jest to takie kontrowersyjne i nie na miejscu?

Lua powstało jako język rozszerzeń dla większych aplikacji napisanych w C/C++ do opisu rzeczy biznesowych – oznacza to, że podobnie jak w prawdziwym świecie tabele nie zaczynają się od pozycji zerowej, ale od jedynki. Obecnie ten język jest najszerzej wykorzystywany przez amarotów-skrypterów i twórców gier lub modyfikacji do nich.

Dla programisty-profesjonalisty indeksowanie od 0 jest naturalne. Dlaczego? Dawno, dawno temu w języku C zaprojektowano arytmetykę wskaźników – i miało to sens, ponieważ C jest bardzo blisko związane ze sprzętem, który przecież operuje na miejscach w pamięci. Dlatego w zamierzchłych czasach poniższe miało sens:

int arr[] = {0, 1, 2};
int pos = 0;
if (arr[pos] == pos[arr]) {
    printf("Indeksowanie jest przemienne!");
}

Programując w Lua nie mamy możliwości odwoływania do konkretnych miejsc w pamięci, a Java już nawet nie wie co to jest pamięć, więc wybór wydaje się zasadny.

Jednakże indeksowanie od 1 w Lua jest dobrym pomysłem, ale złym byłoby zaimplementowanie tego w Javie. Do Javy siadają osoby, które mają już jakąś wiedzę i przyzwyczajenia z innych języków programowania, które pochodzą z tradycji przekazywanej od Denisa Ritchiego aż do dzisiaj. Jakby nagle tych ludzi posadzić przed nienaturalnym dla nich językiem to lepiej, żeby nie pisali systemów bankowych.

Dzień 2: Tablice

Na początku była tablica… Typ table różnicuje się na tablicę i mapę dopiero w momencie, kiedy zapisywany jest do niej pierwszy element. Indeksujemy typ table przy pomocy liczby? Otrzymujemy tablicę. W przypadku czegokolwiek innego mamy do czynienia z mapą. Korzyści? Nie. Problemy? Tak:

-- Tablica czy mapa?
t = {}
t[1] = 'a' -- OK
t['b'] = 'b' -- błąd

t = {}
t['a'] = 'a' -- OK
t[#t + 1] = 'b' -- błąd

W obu przypadkach mamy do czynienia z tym samym typem, ale jego zachowanie ulega zmianie. Nawet bym nie miał nic przeciwko i nazwał to 'Duck Typingiem’, ale Lua obsługuje jednak sprawdzanie typów. W tym przypadku musimy nie tylko mieć na uwadze typ danych, ale także typ typu danych…

Dzień 3: goto zamiast continue

Programowanie w QBASICu to w 90% GOTO i w 10% logika aplikacji. I mogłoby się wydawać, że używanie goto to relikt, a w Javie zarezerwowane słowo kluczowe wywołujące błąd kompilacji, ale mamy Lua.

-- goto
for i=1,#t do
    if i % 2 == 0 then
        goto continue
    end
    print(t[i])
    ::continue::
end

Do wyboru mamy praktycznie dwie opcje – albo dzika indentacja i wrzucenie wszystkiego w ifa, albo goto.

Albo grzecznie poprosić twórców o dodanie continue. (proszę…)

Dzień 5: Nauczyłem się wywoływać metody

Czasami potrzebuję trochę więcej czasu, żeby coś ogarnąć i się czegoś nauczyć. Tak było i w przypadku korzystania z obiektów (i przyznaję, że jest to dosyć Pythonowe rozwiązanie).

-- Wywołanie metody
s = '21:37'
g, h = string.match(s, '(%d+):(%d+)')
g, h = s:match('(%d+):(%d+)')

Dzień 7: Zaczyna mi się to nawet podobać

Po tygodniu walki z nieznajomością Lua pisanie w tym języku zaczęło mi się nawet podobać. I trzeba przyznać, że Lua posiada kilka cech, które ułatwiają życie:

  • for i = begin, end, step do ... – iterowanie w przedziale z zadanym krokiem jako element składni języka
  • #tablica – bardzo szybkie odwołanie do ilości elementów tablicy
  • for k, v in pairs(tablica) do ... – wsparcie dla for-each

Standardowo jednak istnieją przeszkadzajki:

  • for _, v in pairs(tablica) do ... – jako jedyna opcja przy iterowaniu tablicy bez kluczy
  • result = result + 1 – brak operatorów do zwiększania i zmniejszania zmiennej
  • nieżyciowe tworzenie klas i obiektów

Dzień 9: table to mapa, set i tablica

Java, TypeScript i C++ przyzwyczaiło mnie do posiadanie wyspecjalizowanych typów danych typu List, Set, Map… W Lua wszystko można „rozpykać” typem table:

-- Tablica
tablica = {1, 2, 3}
tablica[1] = 4
print(tablica[2])

-- Mapa
mapa = {['klucz1'] = 1, ['klucz2'] = 2}
mapa['klucz3'] = 3
print(mapa['klucz1'])

-- Set
mapa = {['A'] = 1, ['B'] = 1}
mapa['C'] = 1
print(mapa['D'] == nil)

Genialne w swojej prostocie.

Dzień 11: Używanie modułów

Tego dnia w drugiej części zadania poczułem potrzebę skorzystania z modułu BigInt. Zadanie wymagało jednak optymalizacji i z niego nie skorzystałem. System modułów w Lua jest dość prosty i polega na wczytaniu pliku źródłowego funkcją require('module.lua'), w ramach bonusu możemy zapisać sobie taki kontekst do zmiennej.

Dzień 13: Dodawanie stringów

JavaScript posiada mechanizm, który przy operacjach arytmetycznych próbuje sobie zamienić argumenty na liczby i spróbować je przetworzyć. Zazwyczaj korzyści takie rozwiązanie nie ma, a powoduje tylko głupie błędy (które zabierają dużą ilość życia przy debugowaniu – więcej o tym na koniec).

-- Dodawanie stringów?
b = '1' + 2
print(b) -- 3

Dzień 17: Spróbujmy tego modnego OOP

Lua wspiera programowanie obiektowe, z którego trochę skorzystałem. Niestety, przejrzystość definiowania klas w Lua nie powala i bardziej przeszkadza niż pomaga:

Klasa = {}

function Klasa:new(o)
    o = o or {}
    setmetatable(o, self)
    self.__index = self
    return o
end

obiekt = Klasa:new{param = 1}

Widzę w tym potencjał podczas definiowania każdej klasy w oddzielnym pliku, ale bez oddzielania definicji poszczególnych klas jak na przykład w C++/Javie oddzielnymi blokami jest to bardzo mało czytelne.

Wnioski końcowe

  1. Lua dla swoich zastosowań jest spoko, ale to nie będzie mój ulubiony język.
  2. Pomimo, że jest to język skryptowy, Lua działa bardzo szybko i sprawnie.
  3. Brakuje możliwości debugowania kodu jak na przykład w Javie i Pythonie. Jedyne, co pomaga to printowanie wszystkiego na potęgę.
  4. Ze względu na problemy z typowaniem i średnio czytelną składnię, tego języka bym nie zastosował do krytycznych aplikacji.
  5. Trudno będąc przyzwyczajonym do języków obiektowych nagle przestawić się na programowanie proceduralne.

Dlaczego zrezygnowałem tak szybko?

Po pierwsze – nie szybko. Pobiłem swój rekord wytrwałości o całe 5 dni. Niestety z każdym kolejnym dniem walka z zadaniem, językiem i średnim (nieistniejącym) debuggerem przepala motywację. Brak woli walki ze strony współpracowników, z którymi też się ścigałem, nie uzupełniał niestety tego niedoboru motywacji.

To nie jest porażka, ale roztropne zostawienie sobie miejsca na rozwój.


Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *