Multihooki w git

W poprzednim wpisie poruszyłem problem konfigurowania hooków w gitcie. Dzisiaj przyjrzymy się jak zapanować nad hookami, w których chcemy wykonać wiele zadań. Będzie trochę basha, będzie trochę „magii” i w końcu będzie trochę porad jak sobie ułatwić życie.

Hooki – podstawy

Git, podobnie jak wiele innych narzędzi, pozwala na uruchomienie dodatkowych skryptów lub programów w wyniku zaistnienia pewnych zdarzeń. Jest to model reaktywny, to znaczy, że w systemie są zdefiniowane „punkty obserwacyjne”. Po osiągnięciu takiego punktu uruchamiane są wszystkie skrypty „obserwujące” dany punkt.

Jakie to są punkty?

W gitcie jest wiele punktów zaczepienia dla hooków, ale można je podzielić na te po stronie klienta i te po stronie serwera (repozytorium). Te po stronie serwera służą przede wszystkim do weryfikacji poprawności samych commitów. Czy nie ma gdzieś nadużywanego fast-forward, czy nie commitujemy do „chronionej” gałęzi (czyli takiej, do które powinny być dopisywane tylko MR/PR), czy w końcu do rozgłaszania zmian w repozytorium. Ten ostatni hook jest często wykorzystywany, do powiadamiania systemów CI o zmianach. Podsumowując po stronie serwera mamy:

  • pre-recive – odpalany raz gdy przychodzi kod (robisz git push).
  • update – odpalany raz dla każdej gałęzi zmienianej w przychodzącym kodzie.
  • post-recive – odpalany raz po zakończeniu procesowania zmian.

Co i jak można tutaj wykorzystać, to temat na osobny artykuł.

Po stronie klienta jest jednak znacznie więcej opcji. Przede wszystkim dlatego, że po stronie klienta dzieje się znacznie więcej rzeczy. Commitujemy, łączymy gałęzie, rebaseujemy, wypychamy, ciągniemy… a do tego niektóre z tych operacji są wieloetapowe. Dlatego też mamy tutaj większe pole do popisu. Oznacza to też, że wraz z rozwojem naszej bazy hooków zaczną pojawiać się pewne problemy. Zanim jednak o nich opowiem i pokażę, jak je rozwiązać zobaczmy, w jakiej kolejności wykonywane są skrypty. Po prostu w katalogu .git/hooks (lub tam, gdzie wskazuje core.hooksPath) umieśćmy pliki takie jak:

  • applypatch-msg
  • commit-msg
  • fsmonitor-watchman
  • post-checkout
  • post-commit
  • post-merge
  • post-update
  • post-receive
  • pre-applypatch
  • pre-commit
  • prepare-commit-msg
  • pre-push
  • pre-rebase
  • pre-receive
  • update

A w każdym z nich umieśćmy poniższy kod:

Listing 1. gdzie ja jestem

#!/usr/bin/env bash

hook_type=${BASH_SOURCE##*/}

echo "Hello from $hook_type"
echo Params
echo $@
echo -------------

Następnie wystarczy coś zmienić w repozytorium i scommitować zmiany. Naszym oczom ukaże się coś w stylu:

Listing 2. Kolejność hooków przy commicie

$ git commit -am "chore: Some example changes"
pre-commit
Params

-------------
prepare-commit-msg
Params
.git/COMMIT_EDITMSG message
-------------
commit-msg
Params
.git/COMMIT_EDITMSG
-------------
Commit message meets Conventional Commit standards...
post-commit
Params

-------------
[master a1cc644] chore: Some example changes
 1 file changed, 1 insertion(+), 1 deletion(-)

Następnie wypchnijmy te zmiany do repozytorium:

Listing 3. Kolejność hooków przy push

$ git push
pre-push
Params
origin git@github.com:Koziolek/multihooks.git
-------------
Enumerating objects: 23, done.
Counting objects: 100% (23/23), done.
Delta compression using up to 8 threads
Compressing objects: 100% (15/15), done.
Writing objects: 100% (20/20), 2.24 KiB | 327.00 KiB/s, done.
Total 20 (delta 7), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (7/7), completed with 2 local objects.
To github.com:Koziolek/multihooks.git
   1452343..4bbecdc  master -> master

Jak widać w normalnej codziennej pracy tylko niektóre z hooków będą nam potrzebne. Oczywiście nie oznacza to, że inne nie będą nam potrzebne. W pewnych sytuacjach na pewno. Jednak nie o tym chciałem dzisiaj mówić. Skupmy się na najpopularniejszym, jak sądzę, hooku, czyli commit-msg.

Multihooki

Jak wspomniałem na początku hooki działają w modelu reaktywnym. Przy czym jest on ograniczony. Dla jednego rodzaju hooków możemy mieć tylko jeden skrypt. Co to oznacza w praktyce? Chcą wykonać wiele operacji w reakcji na jedno zdarzenie, musimy umieścić wszystko w jednym pliku. Oczywiście istnieją funkcje, można użyć polecenia source, by zmodularyzować nasz skrypt, ale chyba nie o to chodzi.

Typową i powszechnie stosowaną techniką w systemach *-xiowych jest umieszczenie wszystkich skryptów, które chcemy wykonać, w katalogu. Nazwa katalogu odpowiada „głównej” nazwie skryptu zakończonej na .d. Tak samo też możemy podejść do naszego problemu. Poniższy skrypt jest rozwiązaniem problemu:

Listing 4. Multihook – uruchamia wszystkie pliki w katalogu NAZWA_HOOKA.d

#!/usr/bin/env bash

# Allow multiple hooks.
#
# To use it copy file to ./git/hooks directory or to directory pointed in git config core.hooksPath.
# Next create symbolic links from this file to hook files. This script should have execution rights.
# Put your scripts in <>.d folders in your hook directory. Scripts will be executed in
# alphabetical order.
#
# Original code https://gist.github.com/damienrg/411f63a5120206bb887929f4830ad0d0
#

hook_type=${BASH_SOURCE##*/}
hook_dir="${BASH_SOURCE[0]}.d"

case "$hook_type" in
  applypatch-msg | \
  commit-msg | \
  fsmonitor-watchman | \
  post-checkout | \
  post-commit | \
  post-merge | \
  post-update | \
  post-receive | \
  pre-applypatch | \
  pre-commit | \
  prepare-commit-msg | \
  pre-push | \
  pre-rebase | \
  pre-receive | \
  update)
  IFS= read -rd '' stdin
  if [[ -d $hook_dir && "$(ls -I .gitkeep -A ${hook_dir})" ]]; then
      for file in "${hook_dir}"/*; do
        "./$file" "$@" <<<"$stdin" || exit 2
      done
  fi
  exit 0
  ;;
*)
  echo "unknown hook type: $hook_type"
  exit 2
  ;;
esac

Jest tu trochę magii. Niedużo 🙂 Najpierw w zmiennej hook_type zapisujemy nazwę hooka, a tak naprawdę jest to nazwa pliku lub linku, który został uruchomiony. Szczegóły notacji i co oznaczają magiczne znaczki na końcu znajdziecie w manualu lub tutaj. Następnie tworzymy sobie nazwę katalogu ze skryptami dla danego hooka.
Później jest switch, który technicznie rzecz biorąc, jest średnio potrzebny. Jednak ma sens w przypadku, gdyby pojawiły się nowe hooki, a my byśmy na pałę podpięli ten skrypt. Zabezpiecza on przed „przykrymi” niespodziankami w przyszłości. Teraz czas na mięsko. Przedefiniujemy sobie IFS na pusty ciąg znaków, by sensowniej obsługiwać wywołanie naszego skryptu. If sprawdza, czy mamy co wywołać. Przy czym jeżeli commitujemy puste katalogi (zawierające jedynie .gitkeep), to ten .gitkeep jest ignorowany. W pętli wywołujemy wszystkie skrypty (w kolejności alfabetycznej), przekazując im wejście, jakie dostał nasz skrypt. To wejście to parametry, z jakimi klient gita wywołał nasz hook. Dodatkowo przekazujemy im całe wejście, jakie dostaliśmy. Jak coś się nie powiedzie, to całość się wywali.

Naszym zadaniem teraz jest utworzenie odpowiednich katalogów i wrzucenie do nich skryptów. Jeden skrypt, to jedna niezależna funkcjonalność.

Multihooki - kolejność wywołania

Na koniec jeszcze jedna uwaga. Jeżeli chcemy zapewnić kolejność wywołania skryptów, to warto użyć konwencji nazewniczej dla skryptów na zasadzie 000-nazwa_skryptu.sh. Bash będzie wywoływać skrypty w kolejności alfabetycznej, a dzięki nazwom zaczynającym się od cyfr, mamy możliwość sterowania kolejnością. Nie warto jednak numerować skryptów po kolei. Co dziesięć jest ok, ponieważ mamy dzięki temu miejsce na przyszłe „wtrącenia”.

Podsumowanie

Skryptologia jest bardzo przydatna przy pracy z gitem. Sam git potrafi dość dużo, ale ma też swoje ograniczenia. Wynikają one z jego natury. Skrypty pozwalają na eleganckie obudowanie skomplikowanych operacji. Hooki stanowią znowuż pierwszą linię obrony w przypadku prostych błędów, które mogą pojawić się w codziennej pracy.

Prawdziwy greenfield – Conventional Commits i jak zmusić do używania standardów

Mam masę rozgrzebanych tematów. Ego się pisze, ale brakuje mi trochę czasu i trochę więcej motywacji. Gdzieś tam jest jeszcze temat drzewek Merkle. Jeszcze książka się pisze…

Po prostu czasami nie da się wszystkiego naraz. Dlatego dzisiaj temat trochę z pogranicza programowania i pożycia projektowego.

Czym jest greenfield?

Jeżeli ktoś w naszej branżuni mówi, że „dostał mu się greenfield”, to oznacza jedno. Ma to szczęście, że zaczyna z pustym repozytorium. Zespół zaczyna od zera. Może swobodnie kształtować architekturę, dobierać narzędzia i tworzyć kod według Najlepszych Praktyk Rynkowych™. Niby idealnie, ale nie do końca. Jeżeli projekt jest tworzony na potrzeby klienta, który nie ma doświadczenia ani zaplecza, to rzeczywiście mamy dużą swobodę. Jedynym ogranicznikiem jest tak naprawdę budżet klienta i nasz profesjonalizm. Profesjonalizm rozumiem tutaj jako pewną etykę pracy, czyli niewciskanie klientowi rozwiązań, których nie potrzebuje.

A co w przypadku gdy greenfield jest realizowany u dojrzałego klienta? Wtedy pojawia się więcej ograniczeń. Ma on wypracowane m.in. standardy kodu, architekturę i procedury. Do tego dodać należy całą masę drobnostek jak np. nazewnictwo gałęzi w repozytorium albo szablon wiadomości wypychanego kodu. W tym przypadku nie mamy do czynienia z greenfieldem. Raczej jest to taki brownfield. Niby robimy coś nowego, ale nie do końca. Przed nami nie ma łąki, a ugór.

// offtopic

Tomek Nurkiewicz ma swoje repozytorium Polski w IT, w którym zbiera odpowiedniki angielskojęzycznych nazw dla różnych bytów. Brakuje tam słówka commit. Rzecz w tym, że w języku angielskim commit jest czasownikiem, a w polskojęzycznym IT mówiąc commit, myślimy o rzeczowniku. Dlatego jest to nie do przetłumaczenia.

// offtopic

Jest jednak jeszcze trzecia możliwość. Klient może chcieć zdefiniować od nowa swoje zasady. Ma przestarzałą infrastrukturę, istniejące rozwiązania osiągnęły kres możliwości technologicznych, a organizacja rozrosła się i musi szybko znaleźć nową drogę. W tym przypadku możemy mówić o „prawdziwym” greenfieldzie. Organizacja jest na tyle duża i zmotywowana (lub zdesperowana), że praktycznie znika ograniczenie budżetu. Oczywiście nie oznacza to, że możemy robić wszystko, ale mamy naprawdę duże pole do popisu.

Po raz drugi robię tego typu projekt. I dzisiaj będzie o „otoczce”.

Co każdy projekt mieć powinien, a niewiele ma?

No właśnie… Projekt jest co do zasady zadaniem zespołowym. Zespół tworzą nie tylko osoby techniczne, ale też tzw. „biznesowi”, którzy poza głównym celem, jakim jest realizacja projektu, muszą też „zaliczać” kolejne cele cząstkowe. Te cząstkowe cele, to oddawanie kolejnych elementów aplikacji lub systemu. Sama praca programistyczna w tym miejscu jest drugoplanowa, ponieważ liczą się artefakty. Te znowuż najłatwiej jest opisać metrykami. Ergo, projekt powinien mieć pewne metryki.

Metryki mogą być różne, począwszy od prymitywnego pokrycia kodu testami, poprzez złożoność cyklomatyczną, a np. na regresji kończąc. Trochę tego jest. Jednak wszystkie te metryki można stosunkowo łatwo zebrać i na upartego łatwo też je spełnić. Nawet na chama.

Poza metrykami istnieje też pewna klasa wymagań, które są trudniejsze w opisaniu. Istnieją pewne formalizmy, których zadaniem jest zapewnienie, że projekt będzie w prawidłowy sposób funkcjonował w ramach organizacji. Bełkot? A co powiesz np. na schemat nazywania commitów, by można było z nich generować listę zmian? Albo sławetny „JĘZYK WSZECHOBECNY”, którego zadaniem jest zapewnienie, że wszyscy zaangażowani w projekt mają taką samą definicję różnych bytów?

Conventional Commits

Zajmę się jednym, dla mnie obecnie najciekawszym w tym momencie zagadnieniem. Jako osoba odpowiedzialna za zespół (do czego to doszło) chcę mieć sformalizowany dziennik zmian. Po każdym sprincie chcę mieć możliwość wygenerowania dokumentu, który będzie można we w miarę tani sposób porównać z wymaganiami, specyfikacją, backlogiem, czy zamówieniem klienta i odhaczyć zakończone zadania. Najłatwiej jest wziąć historię zmian w kodzie i na tej podstawie wygenerować odpowiedni dokument. Jednak taki generator musiałby być albo bardzo cwany, by zrozumieć wiadomości i jakoś je obrobić, albo należy wypracować jakiś standard wiadomości. Co do zasady reguły pracy powinien uzgadniać zespół, ale znacznie łatwiej jest posłużyć się pewną sztuczką.

Gdy dochodzi do dyskusji na temat jakiegoś standardu pracy, to chcemy szybko ją zakończyć (bo zazwyczaj są to dyskusje jałowe jak sędzina Pawłowicz). Jak to zrobić? Dać zespołowi wybór mamy standard A lub B, lub siedzimy i opracowujemy własny. Ostatnia opcja choć kusząca, bo pozwala na prokrastynacjęW, to wymaga przygotowania narzędzi. Jakieś integracje, jakieś klikanie w CI, GitLabie czy innej JIRZe. Gotowe rozwiązania zazwyczaj mają gotowe narzędzia i integracje. Jednym takich gotowców jest Conventional Commits. Obok wersjonowania semantycznego jest to rozwiązanie dające najwięcej „standardowego kopa” w projekcie.

CC – kilka słów o regułach

Ogólna zasada jest taka, że wiadomość przy wypychaniu zmian wpisuje się w szablon:

Listing 1. Szablon wiadomości

(Typ)[zakres][!]: (Tytuł)

[Długi opis]

[Stopka]

Elementy w [] są opcjonalne, w () obowiązkowe. Ponadto Typ jest zestandaryzowany i jest zawężony do kilkunastu wartości takich jak feat, fix, chore, test, itp. Zakres odpowiada za uszczegółowienie obszaru, którego dotyczy zmiana. Może to być na przykład security, api, front, konfiguracja, itp. Przydatne jeżeli projekt ma naturę monolityczną lub jeden zespół utrzymuje wiele modułów w ramach jednego repozytorium. Następnie mamy Tytuł, czyli krótki opis zmian. Po nim następuje opcjonalny dłuższy opis, który może być wymagany w pewnych sytuacjach. Przykładowo możemy tam zamieścić linki do dokumentacji, czy też niektóre organizacje wymagają jakiegoś dłuższego opisu w konkretnym formacie. Na koniec mamy stopkę, która spełnia dwa zadania. Jeżeli zaczyna się od frazy BREAKING CHANGE:, to mamy do czynienia z czymś co łamie kontrakt. Identyczną rolę pełni wykrzyknik, który można umieścić po Typie i Zakresie. Stopka jest też miejscem, gdzie można umieścić gitowe trailery. Ciągi te będą interpretowane w specyficzny sposób. Próbkę tego typu zachowania mamy na githubie, gdzie można zamykać zadnia umieszczając ich numer poprzedzony słówkiem close.

Jak widać, możemy wykorzystać istniejący standard, by znormalizować wiadomości. Ale jak wymusić jego użycie?

Kontrola podstawą zaufania

Powyższe słowa są przypisywane Leninowi, choć historycy nie są co do tego zgodni. Podobne słowa przypisuje się też Stalinowi i stąd wiele problemów, ze źródłem. Maksyma to choć wydaje się sprzeczna, to dość dobrze oddaje ludzką naturę. Ufamy ludziom, że będą zachowywać się odpowiedzialnie i nie chcą sobie ani bliźnim zrobić krzywdy, a jednocześnie staramy się projektować rzeczy w bezpieczny sposób. Ludzie mogą popełniać błędy, a naszym zadaniem jest tak projektować procesy, urządzenia czy interfejsy, by minimalizowały szansę popełnienia błędu, a w przypadku jego wystąpienia ograniczały negatywne skutki.
Pracując z gitem podstawowym narzędziem, które może posłużyć do zapewnienia nam bezpieczeństwa, są wyzwalacze – hooks.

CC – walidacja wiadomości

Poniższy kod pozwalana sprawdzenie wiadomości i w razie czego wyświetla odpowiednią informację:

Listing 2. hook sprawdzający wiadomość

#!/usr/bin/env bash

set -eou pipefail

# Create a regex for a conventional commit.
convetional_commit_regex="^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z \-]+\))?!?: .+$"

# Get the commit message (the parameter we're given is just the path to the
# temporary file which holds the message).
commit_message=$(cat "$1")

# Check the message, if we match, all good baby.
if [[ "$commit_message" =~ $convetional_commit_regex ]]; then
   echo -e "\e[32mCommit message meets Conventional Commit standards...\e[0m"
   exit 0
fi

# Uh-oh, this is not a conventional commit, show an example and link to the spec.
echo -e "\e[31mThe commit message does not meet the Conventional Commit standard\e[0m"
echo "An example of a valid message is: "
echo "  feat(login): add the 'remember me' button"
echo "More details at: https://www.conventionalcommits.org/en/v1.0.0/#summary"

exit 1

źródło

Jeżeli wiadomość nie pasuje do formatu, to całość zwróci błąd, a proces commitowania zostanie przerwany. No dobra, ale jak wymusić użycie tego rozwiązania?

Współdzielone hooki

Git przechowuje hooki w katalogu .git/hooks. Zatem nie są one współdzielone w zespole. Wystarczy jednak utworzyć katalog w projekcie i tam umieścić skrypty. Następnie katalog ten będziemy współdzielić jak inne pliki w projekcie. Ale to oczywiście nie wszystko. Kolejnym krokiem jest wskazanie gitowi nowego katalogu. W tym celu należ zmienić konfigurację w następujący sposób:

Listing 3. Nowa konfiguracja gita

git config core.hooksPath .hooks

W tym przypadku nasze skrypty znajdują się w katalogu .hooks. Ale czy to wszystko? Nie do końca. Jeżeli ktoś nie zmieni konfiguracji gita, to oczywiście nie będzie podlegać weryfikacji (pomijam włączenie weryfikacji na serwerze) i będzie nam bruździł w repo. Pozostaje wymusić taką konfigurację w inny sposób.

Wymuszenie konfiguracji

Opisana tutaj metoda jest „krzywa” tzn. opiera się o zewnętrzne narzędzia, w tym przypadku służące do budowania projektów. Na czym polega? Skonfigurujmy nasz projekt tak by każde uruchomienie mavena/mixa/npma/sbt czy czego tam używasz spowodowało wykonanie powyższego polecenia konfiguracyjnego gita. Przykładowo w mavenie będzie to wyglądać tak:

Listing 4. Dodatkowa konfiguracja mavena


  exec-maven-plugin
  org.codehaus.mojo
  
    
      Git setup
      generate-sources
      
        exec
      
      
        ${basedir}/.hooks/setup.sh
      
    
  

Przy czym skrypt setup.sh może ustawiać też inne elementy w projekcie. O tym kiedy indziej (na św. Nigdy pewno). Dodatkowo należy tę konfigurację zamknąć w dodatkowym profilu, który nie będzie uruchamiany np. przy tworzeniu kontenerów dockera.

Podsumowanie

Tworząc projekty greenfield zazwyczaj wpadamy w środowisko, w którym są już ustalone pewne zasady. Mamy narzucony stos technologiczny, jakiś szablon architektury i procedury. Niezykle rzadko możemy jednak trafić na projekt, który poza budową rozwiązania od zera, ma też za zadanie ustanowić (nowe) reguły dla całej organizacji. W tym momencie musimy podejmować decyzje o tzw. duperelach. Przy czym, jak zwykle, trudniejszym elementem w tej zabawie jest wymuszenie przestrzegania reguł. Można, a wręcz należy, zaufać ludziom. Jednocześnie warto wprowadzić elementy (automatycznej) kontroli. Ich zadaniem jednak nie jest jebanie po ludziach (dlatego mechanizmy te nie mogą zbierać metryk), ale ograniczenie potencjalnych strat.

Na dłuższą metę będziemy w stanie znacznie sprawniej zarządzać techniczna stroną projektu. Skupić się na ciekawszych aspektach niż klepanie raportów dla kierownictwa i klientów.

Testuj na produkcji, serio

Zeszły tydzień zakończyliśmy eleganckim fakapem ekipy mBanku, która to ekipa „testowała na produkcji” powiadomienia push aplikacji mobilnej. W dużym skrócie, dla tych, co czytają ten tekst jakiś czas później, o poranku klienci mBanku otrzymali trzy powiadomienia push, w tym takie o treści „ęśąćż”. Ogólnie mało ciekawe zdarzenie i ktoś zapewne oberwał po uszach. O możliwych przyczynach na koniec. Teraz o testowaniu na produkcji.

Zanim zaczniemy

Załóżmy na chwilę, że jesteś architektem. Jednak nie takim od stawiania kloców w UML-u, ale takim prawdziwym, który projektuje budynki. Nie jesteś jednak zwykłym architektem, od projektowania kostki mazowieckiej. Jesteś architektem-urbanistą, czyli poza budynkami projektujesz też ich grupy (systemy). Dostajesz zadanie zaprojektowania budynków, które będą spełniać pewne określone warunki. Ich dokładna treść nie jest istotna. Istotne jest jednak, że już na samym początku masz kilka różnych pomysłów. Każdy z nich będzie spełniał część warunków. W dodatku zbiór warunków, które spełniają wymyślone przez ciebie projekty, jest, mniej więcej, wspólny dla wszystkich pomysłów. Istnieje też pewna grupa warunków, których nie możesz przetestować na etapie projektowania. Co robisz?

Sytuacja wydaje się beznadziejna. Co prawda możesz odwołać się do swojego doświadczenia i spróbować intuicyjnie wybrać najlepsze rozwiązania. W przypadku małych elementów w projekcie, takich jak elementy mieszkania, możesz spróbować użyć wzorców projektowych AlexandraW. Czy jednak samo mieszkanie będzie dobrze zaprojektowane?

Na to pytanie próbowali odpowiedzieć Szwedzi z Instytut Badań DomowychW. Przeprowadzili oni badania nad zachowaniem gospodyń domowych w obrębie kuchni. Celem było zaprojektowanie kuchni funkcjonalnej. Do dziś wyniki tych badań są widoczne w projektach kuchni IKEA. Swoją drogą, badania te stały się też przedmiotem żartu w postaci filmu Historie kuchenneW, który to film gorąco polecam.

Można jednak podejść do problemu trochę inaczej. Pamiętaj, że mamy do opracowania NOWY projekt, a nie bazujemy na starych. Zrealizujmy więc większość naszych pomysłów na niewielką skalę. Zbadajmy, który spełnia wszystkie założenia, a następnie zrealizujmy go na większą skalę. Taki sposób myślenia przyświecał twórcom Osiedla Prototypów, na warszawskim Mokotowie. Więcej na temat samego osiedla tutaj. W dużym skrócie. W latach 60-tych wybudowano osiedle składające się kilkunastu budynków, które były budowane zgodnie z różnymi projektami. Były to zarówno pojedyncze budynki, jak i niewielkie zespoły urbanistyczne. Niektóre projekty różniły się szczegółami w rodzaju lustrzanego odbicia mieszkań albo innego ustawienia budynku w stosunku do stron świata.

Dzisiaj osiedle ma się chyba nieźle. Co prawda w latach 90-tych bardzo mocno dotknął je kryzys związany z transformacją, ale obecnie następuje już zmiana pokoleniowa i idzie ku lepszemu.

Testy na produkcji – oczywistości

Po przydługim wstępnie czas na mięsko. Po pierwsze testy na produkcji nie są czymś niezwykłym. Najpopularniejszym przykładem tego rodzaju działań są test A/B. Pozwalają one na porównanie różnych wersji elementów UI. Są stosunkowo tanie w porównaniu z testami laboratoryjnymi. Można też prowadzić je we w miarę bezpieczny sposób z punktu widzenia produktu. Nic więc dziwnego, że tego typu testy są często stosowane przy ocenie np. reklam. Przygotowujemy kilka wersji reklamy i patrzymy, która jest efektywna. Ba! Testy te, dla reklam, prowadzą nie tylko reklamodawcy, ale też (tak naprawdę, przede wszystkim) autorzy stron, gdzie reklamy są umieszczane. W końcu celem jest maksymalizacja kliknięć w reklamę. Jednak testy te nie są do końca tym, czym chcemy się tutaj zająć.

Drugim obszarem „na produkcji” są testy bliskie krzemu. Jak przetestować bootloader? Na emulatorze. Jednak koniec końców i tak trzeba przeprowadzić testy na fizycznym sprzęcie. Tak też dzieje się w przypadku ww. bootloadera. Ktoś odpala go na konkretnym sprzęcie i patrzy czy działa. Jest to czasochłonne i drogie, ale innej drogi nie ma. Zresztą dlatego tak ważna jest współpraca pomiędzy programistami kernela, a producentami sprzętu. Niektóre rzeczy można testować wcześniej. Oczywiście te testy są ograniczone. Tak naprawdę nie odbywają się „na produkcji”, a jedynie na środowisku takim jak produkcyjne. Czym to się różni? Fajny przykład podała Alicja Kubera na SegFaulcie:

Jak widzicie testy na kopii produkcji to jedno, a produkcja to drugie.

Część wspólna

Co łączy powyższe przykłady i przykład ze wstępu?

Łączy je trudność w wyborze rozwiązania, przy uwzględnieniu pewnych wymagań, których spełnienie nie może zostać zweryfikowane za pomocą testów w warunkach laboratoryjnych.

Czym są warunki laboratoryjne?

W przypadku testów jednostkowych mówimy, o ich powtarzalności, wzajemnej niezależności, niezależności wobec otoczenia, szybkości i możliwości automatyzacji. Dobre testy powinny ponadto być łatwe w implementacji, możliwe do uruchomienia przez każdego i niezmienne w stosunku do API (tylko zmiana API zmienia test). W przypadku testów wyższego poziomu, integracyjnych, e2e, rozluźniamy niektóre z powyższych warunków, ponieważ charakter testów tego rodzaju tego wymaga. Przykładowo trudno mówić o szybkich (trwających milisekundy) testach integracyjnych dla dużej liczby modułów.
Jednak wspomniane warunki laboratoryjne oznaczają, że testy można prowadzić w kontrolowanym i (w miarę) stabilnym środowisku. Pozwalają one na uzyskanie powtarzalnych wyników.

Mocki i inne zaślepki

Przy okazji warto wspomnieć o mockach i różnego rodzaju zaślepkach. Nie służą one do testowania naszego kodu. Ich zadaniem jest zapewnienie właśnie takiego stabilnego i w pewnym sensie sterylnego środowiska. Co prawda istnieje pewna niewielka nisza, gdzie weryfikacja stanu naszej zaślepki jest clou testu. Jednak w 99% przypadków „z życia” tak naprawdę testujemy bibliotekę, a nie nasz kod. Bardzo fajnie mówił kiedyś o tym Jose Valim, że funkcja powinna dostać wszystko, czego potrzebuje. Dzięki temu nie ma potrzeby korzystania z mocków.

Testy na produkcji – kiedy

Testy na produkcji prędzej czy później okazują się koniecznością. Co ciekawe mam pewną hipotezę na temat tego, kto wykonuje tego typu testy. Otóż są dwie grupy organizacji, które robią tego typu testy. Pierwsza grupa, to bardzo małe organizacje, które dostarczają produkty o niewielkim znaczeniu dla użytkownika końcowego. W takim przypadku taniej jest dostarczyć coś, co zostało przetestowane jednostkowo, a użytkownik końcowy przeklika sobie produkt i w razie czego zgłosi błąd. Szczególnie że błędy w takich produktach wynikają w dużej mierze z niedostatków w procesie projektowania. Dotyczą też rzeczy bliskich zagadnieniom, które obejmują testy A/B. Klasyką gatunku jest tu hasło „większe logo”. Czasami chodzi o inne umieszczenie przycisków, albo lekkie zmiany w procesie. Proces projektowania w takich produktach jest potraktowany po macoszemu nie ze względu na lenistwo czy niechęć do robienia porządnie, ale ze względu na oszczędność. Szkoda czasu na projektowanie hello worlda, choć i są fani tego typu zadań
Druga grupa to organizacje, które są po prostu gigantyczne. W ich przypadku nie opłaca się testować niektórych rzeczy, ponieważ ze względu na specyfikę klientów i tak nie pokryjemy wszystkich przypadków. W dodatku taniej wychodzi zrobienie dobrze niezadowolonemu klientowi na poziomie supportu niż przepalanie czasu testerów. Co ciekawe wielu błędów i tak nikt nigdy nie wykryje. Przy czym organizacje te mają dobrze zorganizowany proces wdrożenia i łatwo mogą wycofać wadliwy kod oraz mają sprawnie działający dział wsparcia, który potrafi (i może) identyfikować problemy z funkcjonalnościami.
Średniej wielkości organizacje zazwyczaj nie testują na produkcji. Z jednej strony mają już wystarczająco dużo środków, by testować laboratoryjnie. Z drugiej strony ich produkty nie są na tyle duże, by testerzy nie byli w stanie ogarnąć wszystkich zagadnień.

Osobną grupę stanowią dostawcy różnych krytycznych rozwiązań, którzy mają zupełnie inne podejście do problemu testowania, ale to temat na osobny wpis.

Podsumowanie

Trochę się rozwlekłem, więc szybkie podsumowanie. Testy na produkcji nie są czymś złym. Jeżeli potrafimy je wykonać, czyli mówiąc prościej, umiemy odkręcić ich rezultaty, to mogą okazać się niezłym narzędziem. Ważne jest też wypracowanie i przestrzeganie zasad tego typu testów. Inaczej może okazać się, że mamy problem jak mBank. A co tam padło? Najprawdopodobniej w narzędziu do wysyłania powiadomień tester nie zaznaczył grupy docelowej i poszło do wszystkich. Zapewne narzędzie było niedostatecznie przetestowane.

Cyfrodziewczyny kontra Brotopia

Udało mi się w końcu usiąść do bloga. Kolejna część pisania Ego w Elixirze poczeka, a teraz obiecana i wyczekiwana, przez co poniektórych recenzja podwójna. Zanim jednak przejdziemy do mięska, muszę was ostrzec. Poniższe książki traktują o podobnym temacie, ale nie można ich bezpośrednio porównywać. Opisują dwie różne epoki w dwóch różnych systemach politycznych. Nie mam też tutaj zamiaru rozstrzygać czy PRLowski komunizm był lepszy dla kobiet w kontekście rynku pracy, czy też współczesny neoliberalizm na rynku IT w amerykańskim wykonaniu jest zły. Zostawię ten temat wykopowym ekspertom.

Okładka Brotopia
Tytuł: Brotopia
Autor: Emily Chang
Rok: 2019 (2018 EN)
ISBN: 978-83-954-1060-4

O czym jest ta książka? Jeżeli przyjmiemy prostą interpretację, to będzie ona o silnie zmaskulinizowanym zawodzie, w którym kobiety są niemile widziane. Będzie to też historia kilkunastu kobiet, które spotkały się z różnego rodzaju zachowaniami, które w cywilizacji zachodniej kwalifikuje się jako molestowanie seksualne. Mówię tu o cywilizacji zachodniej, ponieważ istnieją różnice w postrzeganiu tego, co jest molestowaniem lub niestosownym zachowaniem u nas, w USA, w krajach UE, czy w Japonii. I raczej mam tu na myśli drobnostki niż przestępstwa.

Emily Chang jest znana ze swojego feminizmu i wspierania mniejszości. Czy to rzutuje na książkę? Do pewnego stopnia, ponieważ wybrała ona taki, a nie inny temat. Z drugiej strony w spójny i sensowny sposób przedstawia fakty. Jej bohaterkami są kobiety, które są w jakiś sposób znane w Dolinie Krzemowej. Czy to jako kobiety sukcesu jak Susan WojcickiW, czy też jako sygnalistka, które zaczęły „kręcić Małysza” w temacie molestowania jak Susan FowlerW. Pod tym względem autorka gra fair i pokazuje, że nie wszystko jest OK niezależnie od poziomu korpo hierarchii, na którym jesteśmy.

Sama książka zaczyna się od historii tesów predyspozycji William Cannona i Dallis Perriego i tego jak wpłynęły one na kształt rynku pracy w IT. Niestety jest tu bardzo dużo uproszczeń, które mogą prowadzić do błędnych wniosków, że przez źle skonstruowane testy, branża wyparła kobiety. Co prawda są też wspomniane „komputery jako zabawki dla chłopców”, ale nadal brakuje tu kompletnej analizy zjawiska. Co ciekawe w Cyfrodziewczynach dużo lepiej widać przyczyny, które w USA „zamyka się” w hasłach o niezbyt sensownym teście kompetencji i samo nakręcającej się nerdoizacji branży. O tym jednak za chwilę. Znajdziemy tutaj też opisaną późniejszą historię przekształcania się IT w „męską” dziedzinę. Pierwsza część to też opis historii powstania tzw. Mafii Paypala, debunk mitu merytokracji czy też opis znanego z innych branży mechanizmu zatrudniania przez polecenia. Całość ładnie zamknięta historią Google, które miało dobre chęci, sensowne procesy, a i tak się było wyjebało na rowerku „diversity”.

Kolejna część książki (część nie jest tożsama z rozdziałem), to zbiór historii związanych z molestowaniem, dyskryminacją i poniżaniem kobiet. Będzie tutaj o niestosownych zachowaniach na imprezach integracyjnych, ale też o tym jak płeć wpływa na ocenę przez VC, czy o tym, w jaki sposób załatwia się interesy. Mamy więc historię Ubera, gdzie szef był społecznym prymitywem, ale miał niezłe kontakty. Mamy kluby ze stritizem, gdzie załatwia się biznesy. Mamy w końcu opis różnych pseudodziałań ze strony korporacji, które udają, że chcą rozwiązać problem. Co ciekawe mamy tutaj też widoczny podział na różne podbranże naszej Kochanej Branżuni. Inaczej sprawy załatwia się w startupach, inaczej w korpo, a jeszcze inaczej wśród twórców gier.

Na koniec autorka funduje nam krótki rozdział, gdzie opisuje, co branżunia mogłaby zrobić, by było lepiej. Jest tu też poruszony „problem” mniejszości zarówno etnicznych jak i seksualnych. Rzecz w tym, że cały pomysł na naprawę sytuacji opiera się o błędne założenia, że możemy poświęcić jakość kadr oraz jakość produktu w imię budowania różnorodności. Niestety taki mechanizm nie będzie działał, bo jest to strategia przegrywająca na rynku. Cóż, amerykanie są dziwni, ale ta dziwność powoduje, że ich biznesy dobrze działają.

Czy warto?

Moim zdaniem warto. Książka jest dobra, choć jeżeli ktoś chciałby ją przełożyć jeden do jednego na nasze Polskie warunki, to będzie to bezsensowne. Daje jednak wgląd w pewne mechanizmy, które rządzą rynkiem w USA. W naszym zhomogenizowanym społeczeństwie, w którym za „inność” etniczną uważa się mówienie jo zamiast tak, wiele problemów po prostu nie istnieje. Ewentualnie ich siła rażenia jest stosunkowo niewielka. Książka jest też ciekawym studium samego biznesu. Różnic, które występują pomiędzy USA i Europą, a które mogą powodować, że nasze pomysły będą działać inaczej tu i tam.

Okładka Cyfrodziewczyny
Tytuł: Cyfrodziewczyny
Autor: Karolina Wasilewska
Rok: 2020
ISBN: 978-83-662-3287-7

Reportaż, zbiór wywiadów, czy powieść historyczna? Trudno powiedzieć. Cyfrodziewczyny, to kilkanaście zredagowanych rozmów, które ułożone według chronologii budują nam obraz polskiego IT od wczesnych lat 50-tych, aż do upadku PRLu. Pośrednio też możemy poznać współczesne losy bohaterek. Całość zaczyna się od skrótowej historii komputerów na zachodzie i kończy się mniej więcej na wspomnianym tu wcześniej teście kompetencji zawodowych. Mając taką „bazę”, autorka przechodzi do pierwszych polskich maszyn, czyli ARAL i ARR. Koniec końców wstęp do naszej historii kończy się powstaniem EMAL-a i pojawieniem się ogłoszeń o pracy na UW i PW…

Później jesteśmy prowadzeni przez historię pierwszych polskich komputerów jak XYZ, serii ZAM i UMC. Każda z tych historii to też historia jakiejś grupy kobiet, które trafiły do zakładów badawczo-rozwojowych prosto po studiach. Mamy też ELWRO i historię rywalizacji pomiędzy Warszawą i Wrocławiem. Jednak wraz z upływem czasu widać, że polityka coraz bardziej wchodzi w IT. Mamy tu też mały debunk sławnego K202. W dużym skrócie – jak zawsze polityka dała o sobie znać. I to nawet nie na zasadzie, że ZSRR coś tam chciało, bo przy okazji i tak był problem z projektem RIAD, czyli takim pan-demoludowym komputerem. Tutaj wyszła zwykła przepychanka między ludźmi i ich animozje.

I w sumie to jest tyle… Serio. Można by się rozpisywać, o poszczególnych etapach, ale ta książka, to dobre opracowanie historii. Zakończenie, omawia obecny udział kobiet w IT. Jest trochę marudzenia o różnicach w wychowaniu czy promocji różnych projektów, ale to takie wypełniacze.

Czy warto?

Tak. Szczególnie w pakiecie z Brotopią. Inaczej nie będzie można zrozumieć tej pierwszej. Jest to też ciekawa lekcja historii informatyki oraz źródło dobrych historyjek, które tak naprawdę nie różnią się wiele od współczesnych problemów z którymi musimy się mierzyć 😀

Lekser Ego w Elixirze część III – komentarze i liczby – wideo

Zgodnie z zapowiedzią dziś jest nagranie. Nie jest pro, nie jest nawet amateur, ale jest 😀

ps. blog nie wyświetla się na jvm-bloggers 🙁 Będzę walczyć o powrót

Lekser Ego w Elixirze część II – preoptymalizacje

Jarek i Wiktor udostępnili swój kod na githubie. Pierwsza rzecz, która rzuciła mi się w oczy, to ilość pracy, jaką wykonali poza kamerą. Ich rozwiązanie wspiera już kod w wielu linijkach oraz liczby. To nadal są proste zagadnienia, ale nie są trywialne. Dlatego też, w tym wpisie zajmiemy się pewnymi preoptymalizacjami, które ułatwią nam pracę w przyszłości.

Motywacja

Oryginalne założenie było takie, by implementacja w Elixirze była jak najbliższa tej w Javie. Dzięki temu osoby, które czytają mój kod, a nie znają Elixira, będą w stanie zrozumieć konstrukcję programu, patrząc i porównując go z programem napisanym w Javie. Jednak nie jest to dobra strategia. Lekser zaczyna się komplikować, a przenoszenie kodu strukturalno-obiektowego do świata funkcyjnego nie jest dobrym pomysłem. Z wielu powodów, ale najważniejszy to, że w efekcie otrzymujemy kod, który jest słaby. Dlatego ugryzę ten problem trochę inaczej. Kod będę tworzył w Elixirze. Zachowam spójność nazewniczą, o ile będzie to możliwe. Jednak przede wszystkim postaram się, by kolejne kroki były małe. Pozwoli to na lepsze wytłumaczenie różnych zagadnień i łatwiejszą analizę kodu przez czytelników.

Zacznijmy więc zabawę, ale najpierw małe przypomnienie. Kod wyjściowy wygląda w następujący sposób:

Listing 1. Kod wyjściowy

defmodule Ego.Lexer do
  
  def tokenize(program) when is_binary(program) do
    program
    |> String.split("", trim: true)
    |> token
  end

  defp token(charlist, accumulator \\ [], buffer \\ [], mode \\ :common)
  defp token([], accumulator, buffer, _), do: accumulator ++ [:eof]

  defp token(charlist, accumulator, buffer, :common) do
    [h | t] = charlist

    case h do
      "(" -> token(t, accumulator ++ read_buffer(buffer) ++ [:open_bracket], [])
      ")" -> token(t, accumulator ++ read_buffer(buffer) ++ [:close_bracket], [])
      " " -> token(t, accumulator ++ read_buffer(buffer), [])
      "\"" -> token(t, accumulator ++ read_buffer(buffer), [], :text)
      _ -> token(t, accumulator, buffer ++ [h])
    end
  end

  defp token(charlist, accumulator, buffer, :text) do
    [h | t] = charlist

    case h do
      "\"" -> token(t, accumulator ++ read_buffer(buffer), [], :common)
      _ -> token(t, accumulator, buffer ++ [h], :text)
    end
  end

  defp read_buffer([]), do: []

  defp read_buffer(buffer) do
    [buffer |> Enum.join("") |> String.to_atom()]
  end
end

Odwrotna kolejność

Pierwszą drobną optymalizacją będzie sposób, w jaki tworzę listę tokenów. Dotychczasowy kod (Listing 1.) działał w taki sposób, że gdy mamy już gotowy token, to zostaje on dołączony na końcu akumulatora. Jest to łatwa do zrozumienia konstrukcja, która jednak nie jest optymalna. W Erlangu operator ++ działa w ten sposób, że kopiuje argument po lewej stronie, by wykorzystać go w nowej liście. Lepszym rozwiązaniem jest dodawanie nowych elementów na początku listy, a następnie wywołanie Enum.reverse, by uzyskać końcowy rezultat. Opisane jest to w artykule The Seven Myths of Erlang Performance. W efekcie otrzymamy:

Listing 2. Zmiana sposobu tworzenia akumulatora

defmodule Ego.Lexer do

  def tokenize(program) when is_binary(program) do
    program
    |> String.split("", trim: true)
    |> token
    |> Enum.reverse
  end

  defp token(charlist, accumulator \\ [], buffer \\ [], mode \\ :common)
  defp token([], accumulator, buffer, _), do: [:eof] ++ accumulator

  defp token(charlist, accumulator, buffer, :common) do
    [h | t] = charlist

    case h do
      "(" -> token(t, [:open_bracket] ++ read_buffer(buffer) ++ accumulator, [])
      ")" -> token(t, [:close_bracket] ++ read_buffer(buffer) ++ accumulator   , [])
      " " -> token(t, read_buffer(buffer) ++ accumulator, [])
      "\"" -> token(t, read_buffer(buffer) ++ accumulator, [], :text)
      _ -> token(t, accumulator, [h] ++ buffer)
    end
  end

  defp token(charlist, accumulator, buffer, :text) do
    [h | t] = charlist

    case h do
      "\"" -> token(t, read_buffer(buffer) ++ accumulator, [], :common)
      _ -> token(t, accumulator, [h] ++ buffer, :text)
    end
  end

  defp read_buffer([]), do: []

  defp read_buffer(buffer) do
    [buffer |> Enum.reverse |> Enum.join("") |> String.to_atom()]
  end
end

Jak widać, zmiana dotknęła wiele miejsc w kodzie. Jednak dzięki temu, że Elixir opiera się o dopasowanie wzorców, nie musieliśmy zmieniać API. Operację odwrócenia realizujemy w dwóch miejscach. W funkcji tokenize odwracamy listę tokenów, by otrzymać wynik. W funkcji read_buffer odwracamy bufor przed jego zrzuceniem, bo bufor też jest akumulatorem, tyle tylko, że lokalnym.

No właśnie…

Nazewnictwo

Kolejny etap to doprowadzenie do porządku nazewnictwa. Funkcja read_buffer wydaje się idealnym przykładem. Zadanie tej funkcji to odczytanie zawartości bufora, bo chcemy go opróżnić, ale… opróżniamy bufor w innym miejscu. Taka mała podpucha, dla kogoś, kto lubi refaktoryzaować nazwy dla samej refaktoryzacji nazw. Prawdziwym problemem jest funkcja token, która produkuje listę tokenów. Odwróconą. Jak ją nazwać? Na obecnym etapie tokens będzie ok. Kolejnym drobnym problemem jest zmienna buffer, w metodzie token(s) dopasowanej do pustego ciągu znaków. Jest nieużywana, więc można ją zastąpić znakiem _. Ostatnia rzecz związana z nazewnictwem, będzie polegać na pozbyciu się niepotrzebnego przypisania [h | t] = charlist. To są w sumie drobne poprawki, które nie wpływają na kod.

Listing 3. Zmiana nazw

defmodule Ego.Lexer do

  def tokenize(program) when is_binary(program) do
    program
    |> String.split("", trim: true)
    |> tokens
    |> Enum.reverse
  end

  defp tokens(charlist, accumulator \\ [], buffer \\ [], mode \\ :common)
  defp tokens([], accumulator, _, _), do: [:eof] ++ accumulator

  defp tokens([h | t], accumulator, buffer, :common) do
    case h do
      "(" -> tokens(t, [:open_bracket] ++ read_buffer(buffer) ++ accumulator, [])
      ")" -> tokens(t, [:close_bracket] ++ read_buffer(buffer) ++ accumulator   , [])
      " " -> tokens(t, read_buffer(buffer) ++ accumulator, [])
      "\"" -> tokens(t, read_buffer(buffer) ++ accumulator, [], :text)
      _ -> tokens(t, accumulator, [h] ++ buffer)
    end
  end

  defp tokens([h | t], accumulator, buffer, :text) do
    case h do
      "\"" -> tokens(t, read_buffer(buffer) ++ accumulator, [], :common)
      _ -> tokens(t, accumulator, [h] ++ buffer, :text)
    end
  end

  defp read_buffer([]), do: []

  defp read_buffer(buffer) do
    [buffer |> Enum.reverse |> Enum.join("") |> String.to_atom()]
  end
end

Nie wygląda to źle. Jednak jak zawsze w przypadku nazw można tu coś usprawnić. Kolejna zmiana jest znacznie ciekawsza.

Reprezentacja znaków – Charlisty

W Javie jest tak, że String i Character rozróżniamy na podstawie rodzaju cudzysłowu użytego do stworzenia obiektu. String to cudzysłów podwójny, zwany polskim cudzysłowem apostrofowym. Character to cudzysłów pojedynczy, zwany brytyjskim. Istotne jest tutaj to, że String może zawierać wiele znaków, a Charakter to pojedynczy znak. W Elixirze jest trochę inaczej.

Co to jest Charlist?

Podwójny cudzysłów definiuje nam String. Pojedynczy definiuje tzw. Charlist, czyli listę znaków. Jest to lista, która zawiera liczby całkowite odpowiadające kodom poszczególnych znaków. I tak znaki ( i ) mają kody odpowiednio 40 i 41, więc zapis `()` będzie odpowiadać [40, 41]. Pojedynczy znak może być reprezentowany jako liczba lub też jako lista. Jest to trochę zagmatwane i jak zaraz zobaczycie, może prowadzić do „dziwnych” konstrukcji. Najłatwiej jednak przyjąć, że w Javie będzie to po prostu List<Character>.
Ale przecież String, który podzielimy na znaki, też jest listą. Tak, jest listą. Jednak w tym przypadku nie mamy rozróżnienia na znaki wielobajtowe i jednobajtowe. W efekcie nie możemy łatwo odsiać pewnych znaków. Z drugiej strony nie musimy tak przejmować się znakami wymagającymi znaku ucieczki jak np. znak końca linii.

Refaktoryzacja do Charlist

Zmiana jest dość prosta, ale znowuż, dotykamy praktycznie całego kodu.

Listing 4. Wprowadzamy charlist

defmodule Ego.Lexer do
  def tokenize(program) when is_binary(program) do
    program
    |> String.to_charlist()
    |> tokens
    |> Enum.reverse()
  end

  defp tokens(charlist, accumulator \\ [], buffer \\ [], mode \\ :common)
  defp tokens([], accumulator, _, _), do: [:eof] ++ accumulator

  defp tokens([h | t], accumulator, buffer, :common) do
    case [h] do
      '(' -> tokens(t, [:open_bracket] ++ read_buffer(buffer) ++ accumulator, [])
      ')' -> tokens(t, [:close_bracket] ++ read_buffer(buffer) ++ accumulator, [])
      ' ' -> tokens(t, read_buffer(buffer) ++ accumulator, [])
      '"' -> tokens(t, read_buffer(buffer) ++ accumulator, [], :text)
      _ -> tokens(t, accumulator, [h] ++ buffer)
    end
  end

  defp tokens([h | t], accumulator, buffer, :text) do
    case [h] do
      '"' -> tokens(t, read_buffer(buffer) ++ accumulator, [], :common)
      _ -> tokens(t, accumulator, [h] ++ buffer, :text)
    end
  end

  defp read_buffer([]), do: []

  defp read_buffer(buffer) do
    [buffer |> Enum.reverse() |> List.to_string() |> String.to_atom()]
  end
end

Clue zmiany leży w warunku case. Można ją przeprowadzić na dwa sposoby. W powyższym zapisałem case [h], ponieważ chcę dopasować h do kolejnych list znaków stworzonych za pomocą pojedynczego cudzysłowa. Zmiast tego mogę użyć dopasowania do wartości liczbowych, ale takie rozwiązanie oznacza, że będę mósiał znać kody znaków. Ta zmiana prowadzi nas do kolejnej, która jest pierwą z dwóch zmian „na przyszłość”.

Case na cond i co to jest if

W Elixirze istnieje kilka różnych struktur, które można zbiorczo nazwać instrukcjami warunkowymi. Różnią się one składnią i możliwościami, ale docelowo są to różne odmiany dobrze znanego if. Sam if istrnije w Elixirze i służy do zapisu pojedynczego warunku. Jego negacją jest unless. Nie interesują nas one, bo nie mają tu zastosowania. Kolejną instrukcją jest case. Dopasowuje on argument, do kolejnych wzorców. Możemy wykorzystywać strażników do opisania wzorca. Jest to chyba najpowszechniejsza instrukcja warunkowa. Rzecz w tym, że pracuje ona na dopasowaniu, a nie na warunku więc nie zawsze możemy ją wykorzystać. Mówiąc prościej jest ona odpowiednikiem javowego switch (upraszczająć), a my chcemy mieć coś w rodzaju if else if i pracować na funkcjach zwracających wartśc logiczną. Do tego właśnie służy cond. Wykona on kolejne funkcje do momentu aż nie otrzyma wartości true. Czasami może oznaczać to spadek wydajności, ale w naszym przypadku nie będzie to aż tak istotne.

Kod po zmianach będzie więc wyglądać następujaco:

Listing 5. Zmiana case na cond

defp tokens([h | t], accumulator, buffer, :common) do
  cond do
    '(' === [h]-> tokens(t, [:open_bracket] ++ read_buffer(buffer) ++ accumulator, [])
    ')' === [h]-> tokens(t, [:close_bracket] ++ read_buffer(buffer) ++ accumulator, [])
    ' ' === [h]-> tokens(t, read_buffer(buffer) ++ accumulator, [])
    '"' === [h]-> tokens(t, read_buffer(buffer) ++ accumulator, [], :text)
    true -> tokens(t, accumulator, [h] ++ buffer)
  end
end

defp tokens([h | t], accumulator, buffer, :text) do
  cond do
    '"' === [h] -> tokens(t, read_buffer(buffer) ++ accumulator, [], :common)
    true -> tokens(t, accumulator, [h] ++ buffer, :text)
  end
end

Na tym kończą sie proste refaktoryzacje, które nie dodawały nowych elementów do kodu. Jeden z użytkowników 4programmers, WeiXiao, stwierdził w komentarzu na mikro, że kod jest zwięzły. Czas go trochę rozsmarować i dodać struktury, które będą przechowywać nam więcej informacji.

Token jako struktura

Do tej pory w implementacji Jarka i Wiktora poszczególne tokeny były reprezentowane przez konkretne obiekty. Moja implementacja była pozbawiona tego elementu, ponieważ w znaczny sposób upraszczało to pisanie posta. Ot lenistwo… Teraz jednak wprowadzę odpowiednią strukturę, która będzie przechowywać dane.

Struktura Token

Elixir nie ma klas. Jest funkcyjny więc ich nie potrzebuje. W zamian ma jednak struktury, które pozwalają na operowanie nazwanymi składowymi mapy. Efektywnie jest to mapa doposażona w kilka udogodnień jak np. ograniczenie nazw kluczy do pewnego zbioru. Struktury definiuje się w modułach dzięki czemu nadal możemy w ramach tej samej przestrzeni nazw tworzyć funkcje. Zanim jednak utworzymy jakieś funkcje stwórzmy strukturę.

Listing 6. Moduł Ego.Token

defmodule Ego.Token do
  @enforce_keys  [:kind, :value]
  defstruct [:kind, :value]
end

Na razie nie potrzeba nam nic więcej. Javowy enum Kind zastąpimy atomami i być może walidacją. Jedna uwaga. Atrybut modułu @enforce_keys pozwala na określenie obowiązkowych pól w strukturze. Bez niego jeżeli stworzymy strukturę, to nieprzypisane pola będą nil.

Refaktoryzacja

Czas na wprowadzenie nowego formatu tokenów do kodu. Zaczniemy od przepisania testów, tak by obsługiwały nowy format.

Listing 7. Poprawione testy

defmodule Ego.LexerTest do
  use ExUnit.Case
  import Assertions
  alias Ego.Lexer
  alias Ego.Token

  @moduletag :capture_log

  doctest Lexer

  test "()" do
    result = Lexer.tokenize("()")

    assert_lists_equal(result, [
      %Token{kind: :open_bracket, value: '('},
      %Token{kind: :close_bracket, value: ')'},
      %Token{kind: :eof, value: ''}
    ])
  end

  test "( )" do
    result = Lexer.tokenize("( )")

    assert_lists_equal(result, [
      %Token{kind: :open_bracket, value: '('},
      %Token{kind: :close_bracket, value: ')'},
      %Token{kind: :eof, value: ''}
    ])
  end

  test "(Print Hello)" do
    result = Lexer.tokenize("(Print Hello)")

    assert_lists_equal(result, [
      %Token{kind: :open_bracket, value: '('},
      %Token{kind: :atom, value: 'Print'},
      %Token{kind: :atom, value: 'Hello'},
      %Token{kind: :close_bracket, value: ')'},
      %Token{kind: :eof, value: ''}
    ])
  end

  test "(Print \"Hello World\")" do
    result = Lexer.tokenize("(Print \"Hello World\")")

    assert_lists_equal(result, [
      %Token{kind: :open_bracket, value: '('},
      %Token{kind: :atom, value: 'Print'},
      %Token{kind: :string, value: 'Hello World'},
      %Token{kind: :close_bracket, value: ')'},
      %Token{kind: :eof, value: ''}
    ])
  end

  test "(Print \"Hello ( ) World\")" do
    result = Lexer.tokenize("(Print \"Hello ( ) World\")")

    assert_lists_equal(result, [
      %Token{kind: :open_bracket, value: '('},
      %Token{kind: :atom, value: 'Print'},
      %Token{kind: :string, value: 'Hello ( ) World'},
      %Token{kind: :close_bracket, value: ')'},
      %Token{kind: :eof, value: ''}
    ])
  end

  test "(Print \"Hello (\n) World\")" do
    result =
      Lexer.tokenize("""
      (Print \"Hello (
      ) World\")
      """)

    assert_lists_equal(result, [
      %Token{kind: :open_bracket, value: '('},
      %Token{kind: :atom, value: 'Print'},
      %Token{kind: :string, value: 'Hello (\n) World'},
      %Token{kind: :close_bracket, value: ')'},
      %Token{kind: :eof, value: ''}
    ])
  end

  test "(\u0061\u0301)" do
    result = Lexer.tokenize("(\u0061\u0301)")

    assert_lists_equal(result, [
      %Token{kind: :open_bracket, value: '('},
      %Token{kind: :atom, value: '\u0061\u0301'},
      %Token{kind: :close_bracket, value: ')'},
      %Token{kind: :eof, value: ''}
    ])
  end
end

Oczywiście wszystko będzie od razu czerwone, bo zmiana jest bardzo głęboka. Poprawny więc kod. Będziemy to robić po kawałku, ponieważ zmian jest dużo i warto je omówić.

Na pierwszy ogień idzie przywrócenie do życia testów związanych z obsługą nawiasów oraz znaku końca pliku. To są bardzo proste zmiany, które szybko rozwiązują nam problem.

Listing 8. Podstawowe elmenty

defmodule Ego.Lexer do

  alias Ego.Token

  def tokenize(program) when is_binary(program) do
    program
    |> String.to_charlist()
    |> tokens
    |> Enum.reverse()
  end

  defp tokens(charlist, accumulator \\ [], buffer \\ [], mode \\ :common)
  defp tokens([], accumulator, _, _), do: [%Token{kind: :eof, value: ''}] ++ accumulator

  defp tokens([h | t], accumulator, buffer, :common) do
    cond do
      '(' === [h] -> tokens(t, [%Token{kind: :open_bracket, value: '('}] ++ read_buffer(buffer) ++ accumulator, [])
      ')' === [h] -> tokens(t, [%Token{kind: :close_bracket, value: ')'}] ++ read_buffer(buffer) ++ accumulator, [])
      ' ' === [h] -> tokens(t, read_buffer(buffer) ++ accumulator, [])
      '"' === [h] -> tokens(t, read_buffer(buffer) ++ accumulator, [], :text)
      true -> tokens(t, accumulator, [h] ++ buffer)
    end
  end

  defp tokens([h | t], accumulator, buffer, :text) do
    cond do
      '"' === [h] -> tokens(t, read_buffer(buffer) ++ accumulator, [], :common)
      true -> tokens(t, accumulator, [h] ++ buffer, :text)
    end
  end

  defp read_buffer([]), do: []

  defp read_buffer(buffer) do
    [buffer |> Enum.reverse() |> List.to_string() |> String.to_atom()]
  end
end

Kolejny krok to poprawki w funkcji read_buffer, tak by zamiast ciągu znaków emitowała odpowiedni token.

Listing 9. Obsługa atomów i zrzut bufora

defp read_buffer([]), do: []

defp read_buffer(buffer) do
  value = buffer |> Enum.reverse()
  [%Token{kind: :atom, value: value}]
end

No i nie działa… Dotychczas wykorzystywaliśmy tę funkcję do produkcji elixirowych atomów. To było OK, bo implementacja była prosta, ale z drugiej strony ukrywaliśmy w ten sposób klasę atomów, które reprezentują ciąg znaków. Wszystko było atomem! Teraz mamy kilka różnych klas do których przyporządkowujemy byty, więc nasza funkcja pełniąca rolę emitera musi to jakoś obsługiwać. Na całe szczęście jest to bardzo prosta zmiana, ponieważ potrafimy opisać z jaką klasą mamy do czynienia. Gdzie? Mówi nam o tym parametr mode funkcji tokens. W efekcie kod będzie wyglądał w następujący sposób:

Listing 10. Działający kod

defmodule Ego.Lexer do

  alias Ego.Token

  def tokenize(program) when is_binary(program) do
    program
    |> String.to_charlist()
    |> tokens
    |> Enum.reverse()
  end

  defp tokens(charlist, accumulator \\ [], buffer \\ [], mode \\ :common)
  defp tokens([], accumulator, _, _), do: [%Token{kind: :eof, value: ''}] ++ accumulator

  defp tokens([h | t], accumulator, buffer, :common) do
    cond do
      '(' === [h] -> tokens(t, [%Token{kind: :open_bracket, value: '('}] ++ read_buffer(buffer, :atom) ++ accumulator, [])
      ')' === [h] -> tokens(t, [%Token{kind: :close_bracket, value: ')'}] ++ read_buffer(buffer, :atom) ++ accumulator, [])
      ' ' === [h] -> tokens(t, read_buffer(buffer, :atom) ++ accumulator, [])
      '"' === [h] -> tokens(t, read_buffer(buffer, :atom) ++ accumulator, [], :text)
      true -> tokens(t, accumulator, [h] ++ buffer)
    end
  end

  defp tokens([h | t], accumulator, buffer, :text) do
    cond do
      '"' === [h] -> tokens(t, read_buffer(buffer, :string) ++ accumulator, [], :common)
      true -> tokens(t, accumulator, [h] ++ buffer, :text)
    end
  end

  defp read_buffer([], _), do: []

  defp read_buffer(buffer, kind) do
    value = buffer |> Enum.reverse()
    [%Token{kind: kind, value: value}]
  end
end

Jest prawie dobrze. Ostatnią refaktoryzacją, którą można zrobić, to wyciągnięcie funkcji fabrykujących do modułu Ego.Token. Tym samym zmieni się też funkcja read_buffer, która będzie tak jak dotychczas zwracać jedynie wartość bufora. Niestety od razu widać pewien problem. Pierwotnie read_buffer zwracało wartość opakowaną w listę albo pustą listę. Dzięki temu łatwo było nam dodawać listy. Teraz zwracamy wartość lub pustą listę (sic!). Zatem możemy doprowadzić do stuacji, gdzie zaczniemy emitować tokeny, które będą reprezentować puste listy (bufor był pusty) i nie będą EOF. Dotyczy to tokenów reprezentujących atom i ciąg znaków. Ale i na to będzie prosta rada. Widać ją w funkcji tokenize. Wypłaszczamy listę tokenów. W efekcie nasz końcowy kod wygląda w następujący sposób:

Listing 11. Ego.Token po zmianach

defmodule Ego.Token do
  @enforce_keys [:kind, :value]
  defstruct [:kind, :value]

  def open_bracket(), do: %Ego.Token{kind: :open_bracket, value: '('}
  def close_bracket(), do: %Ego.Token{kind: :close_bracket, value: ')'}
  def eof(), do: %Ego.Token{kind: :eof, value: ''}
  def atom([]), do: []
  def atom(value), do: %Ego.Token{kind: :atom, value: value}
  def string([]), do: []
  def string(value), do: %Ego.Token{kind: :string, value: value}

end

Listing 11. Ego.Lexer po zmianach

defmodule Ego.Lexer do
  import Ego.Token

  def tokenize(program) when is_binary(program) do
    program
    |> String.to_charlist()
    |> tokens
    |> Enum.reverse()
    |> List.flatten()
  end

  defp tokens(charlist, accumulator \\ [], buffer \\ [], mode \\ :common)
  defp tokens([], accumulator, _, _), do: [eof()] ++ accumulator

  defp tokens([h | t], accumulator, buffer, :common) do
    cond do
      '(' === [h] -> tokens(t, [open_bracket()] ++ [atom(read_buffer(buffer))] ++ accumulator, [])
      ')' === [h] -> tokens(t, [close_bracket()] ++ [atom(read_buffer(buffer))] ++ accumulator, [])
      ' ' === [h] -> tokens(t, [atom(read_buffer(buffer))] ++ accumulator, [])
      '"' === [h] -> tokens(t, [atom(read_buffer(buffer))] ++ accumulator, [], :text)
      true -> tokens(t, accumulator, [h] ++ buffer)
    end
  end

  defp tokens([h | t], accumulator, buffer, :text) do
    cond do
      '"' === [h] -> tokens(t, [string(read_buffer(buffer))] ++ accumulator, [], :common)
      true -> tokens(t, accumulator, [h] ++ buffer, :text)
    end
  end

  defp read_buffer([]), do: []

  defp read_buffer(buffer) do
    buffer |> Enum.reverse()
  end
end

I gotowe.

Podsumowanie

Refaktoryzacje były proste i skomplikowane. Dały one elastyczniejszy kod, który będzie można znacznie łatwiej rozszerzać. Taka mała ciekawostka. Wczorajsza wersja miała 37 linii kodu. Dzisiejsza ma tyle samo. Przy czym nie pociągnąłem jej elixirowrym formaterem w domyślnych ustawieniach. Zmieniłem długość linii na 120 znaków.

Kolejne kroki to uzupełnienie mojego kodu o to, co Jarek i Wiktor dodali poza kamerą. Będzie wsparcie dla liczb i komentarzy. Kod jak zwykle na githubie.

Lexer Ego w Elixirze

Jarek Pałka i Wiktor Sztajerowski zaczęli z nudów cykl wykładów o tym jak stworzyć język programowania. Na tapetę trafiło jarkowe Ego, czyli język programowania przeznaczony do ćwiczenia programowania w dziwnych paradygmatach. Pierwszy wykład poświęcili na napisanie prostego lekseraW języka. Całość do obejrzenia poniżej:

A ja postanowiłem, że pobawię się ich zabawką w trochę inny sposób. Napiszę w elixirze lekser, który będzie umiał dokonać analizy leksykalnej Ego z takim samym wynikiem, co oryginalne rozwiązanie. Zaczynamy!

Konfiguracja

By rozpocząć naszą zabawę, należy na początek utworzyć projekt za pomocą mixa. Następnie dla własnej wygody dodałem jedną zależność – bibliotekę assertions, która pozwoli w prosty sposób porównywać listy w testach. W efekcie mój mix.exs wygląda w następujący sposób:

Listing 1. Konfiguracja projektu

defmodule Ego.MixProject do
  use Mix.Project

  def project do
    [
      app: :ego,
      version: "0.1.0",
      elixir: "~> 1.10",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  def application do
    [extra_applications: [:logger]]
  end

  defp deps do
    [{:assertions, "~> 0.10", only: :test}]
  end
end

Mając taką konfigurację, możemy przystąpić do pisania!

Prosty program – ()

Podobnie jak Jarek i Wiktor rozpocznę od najprostszego programu. W Ego będzie to program złożony z nawiasu otwierającego i zamykającego:

Listing 2. Prosty program w Ego

()

Program ten po przetworzeniu przez lekser powinien wygenerować listę, na której znajdują się trzy tokeny. Pierwszy reprezentuje nawias otwierający, drugi nawias zamykający, a trzeci jest znacznikiem końca pliku. Test, który opisuje to zachowanie, wygląda w następujacy sposób:

Listing 3. Test programu ()

test "()" do
  result = Lexer.tokenize("()")
  assert_lists_equal(result, [:open_bracket, :close_bracket, :eof])
end

Uwaga! Tutaj troszeczkę oszukuję, bo używam elixirowych atomów wprost. Wynika to z chęci utrzymania kodu we w miarę zwięzłej postaci. Oczywiście można ten kod uszczegółowić w podobny sposób jak na filmie, ale ciężko by się go czytało pod postacią blogonotki.

By wypełnić ten test, musimy oczywiście napisać kod. Na początek będzie on bardzo prosty:

Listing 4. Pierwsze elementy leksera

defmodule Ego.Lexer do
  def tokenize(program) when is_binary(program) do
    program
    |> String.split("", trim: true)
    |> token
  end

  defp token(charlist, accumulator \\ [])
  defp token([], accumulator), do: accumulator ++ [:eof]

  defp token(charlist, accumulator) do
    [h | t] = charlist

    case h do
      "(" -> token(t, accumulator ++ [:open_bracket])
      ")" -> token(t, accumulator ++ [:close_bracket])
      _ -> token(t, accumulator)
    end
  end
end

Co my tu mamy? W linii 2 strażnik sprawdza, czy mamy do czynienia z ciągiem znaków. Uroki słabego typowania są urocze… Następnie w liniach 15 i 16 emitujemy odpowiednie tokeny, a w linii 17 ignorujemy inne znaki. Linia 8 jest oczywista, bo jeżeli lista znaków do przetworzenia jest pusta, to znaczy, że osiągnęliśmy koniec pliku i trzeba wyemitować EOF.

Wariacja i kolejny test – ( )

W tym miejscu pójdę trochę inną drogą nich chłopaki, bo dodam obsługę białych znaków.

Listing 5. Test programu ( )

test "( )" do
  result = Lexer.tokenize("( )")
  assert_lists_equal(result, [:open_bracket, :close_bracket, :eof])
end

Inaczej mówiąc, jeżeli trafię na spację, to mogę ją olać. Ten test już przejdzie, bo warunek z 17 linijki nie jest po nic. Jednak delikatnie zmienię kod:

Listing 6. Małe zmiany – uwzględniamy spację

defmodule Ego.Lexer do
  def tokenize(program) when is_binary(program) do
    program
    |> String.split("", trim: true)
    |> token
  end

  defp token(charlist, accumulator \\ [])
  defp token([], accumulator), do: accumulator ++ [:eof]

  defp token(charlist, accumulator) do
    [h | t] = charlist

    case h do
      "(" -> token(t, accumulator ++ [:open_bracket])
      ")" -> token(t, accumulator ++ [:close_bracket])
      " " -> token(t, accumulator)
      _ -> token(t, accumulator)
    end
  end
end

Nic się praktycznie nie zmieniło, ale to tylko pozory. W ten sposób mamy już obsługiwane (prawie) wszystkie symbole jednoznakowe.

Symbole wieloznakowe

Kolejnym krokiem jest obsługa symboli wieloznakowych. Zaczniemy tradycyjnie od napisania testu:

Listing 7. Test programu (Print Hello)

test "(Print Hello)" do
  result = Lexer.tokenize("(Print Hello)")
  assert_lists_equal(result, [:open_bracket, :Print, :Hello, :close_bracket, :eof])
end

Sprawa wydaje się być bardzo prosta. Musimy dodać do naszego leksera bufor, w którym będziemy zbierać kolejne znaki i w momencie gdy trafimy na znak z pierwszej grupy, to wyemitujemy odpowiedni token.

Listing 8. Obsługa symboli wieloznakowych

defmodule Ego.Lexer do
  def tokenize(program) when is_binary(program) do
    program
    |> String.split("", trim: true)
    |> token
  end

  defp token(charlist, accumulator \\ [], buffer \\ [])
  defp token([], accumulator, buffer), do: accumulator ++ [:eof]

  defp token(charlist, accumulator, buffer) do
    [h | t] = charlist

    case h do
      "(" -> token(t, accumulator ++ read_buffer(buffer) ++ [:open_bracket], [])
      ")" -> token(t, accumulator ++ read_buffer(buffer) ++ [:close_bracket], [])
      " " -> token(t, accumulator ++ read_buffer(buffer), [])
      _ -> token(t, accumulator, buffer ++ [h])
    end
  end

  defp read_buffer([]), do: []

  defp read_buffer(buffer) do
    [buffer |> Enum.join("") |> String.to_atom()]
  end
end

Tu się robi ciekawie. Po pierwsze przyjmujemy, że trafiając na któryś ze znaków „specjalnych”, opróżniamy bufor i emitujemy token. Po drugie bufor może być pusty, a zatem nie możemy emitować pustego tokenu. W Elixirze :”” jest poprawnym atomem, więc trzeba tu trochę pohakować. W ten sposób mamy już prosty lekser, który możemy zacząć komplikować. Wzorem Jarka i Wiktora dodajmy obsługę ciągów znaków.

Ciągi znaków

Wspomniałem, że mamy już obsługę prawie wszystkich symboli jednoznakowych. Pozostał nam jeszcze jeden symbol – cudzysłów. Jego obsługa skomplikuje nam lekser, ale też pozwoli na opisanie całej klasy problemów. Zacznijmy jednak od napisania testów:

Listing 9. Test programu (Print Hello)

test "(Print \"Hello World\")" do
  result = Lexer.tokenize("(Print \"Hello World\")")
  assert_lists_equal(result, [:open_bracket, :Print, :"Hello World", :close_bracket, :eof])
end

test "(Print \"Hello ( ) World\")" do
  result = Lexer.tokenize("(Print \"Hello ( ) World\")")
  assert_lists_equal(result, [:open_bracket, :Print, :"Hello ( ) World", :close_bracket, :eof])
end

test "(Print \"Hello (\n) World\")" do
  result = Lexer.tokenize("""
rint \"Hello (
  ) World\")
  """)
  assert_lists_equal(result, [:open_bracket, :Print, :"Hello (\n) World", :close_bracket, :eof])
end

Te trzy małe testy pokrywają użycie znaków (,), spacji i znaku nowej linii w ciągu znaków. Jednocześnie wymuszają na nas duże zmiany w implementacji leksera. Od tego momentu lekser będzie musiał działać w dwóch trybach. Pierwszy zwykły tryb, to wszystko to co dotychczas napisaliśmy. W momencie gdy trafi na znak , lekser przejdzie w tryb procesowania tekstu, którego nie opuści do momentu, gdy po raz kolejny trafi na znak . Proste, prawda?

Listing 10. Obsługa ciągów znaków

defmodule Ego.Lexer do
  def tokenize(program) when is_binary(program) do
    program
    |> String.split("", trim: true)
    |> token
  end

  defp token(charlist, accumulator \\ [], buffer \\ [], mode \\ :common)
  defp token([], accumulator, buffer, _), do: accumulator ++ [:eof]

  defp token(charlist, accumulator, buffer, :common) do
    [h | t] = charlist

    case h do
      "(" -> token(t, accumulator ++ read_buffer(buffer) ++ [:open_bracket], [])
      ")" -> token(t, accumulator ++ read_buffer(buffer) ++ [:close_bracket], [])
      " " -> token(t, accumulator ++ read_buffer(buffer), [])
      "\"" -> token(t, accumulator ++ read_buffer(buffer), [], :text)
      _ -> token(t, accumulator, buffer ++ [h])
    end
  end

  defp token(charlist, accumulator, buffer, :text) do
    [h | t] = charlist

    case h do
      "\"" -> token(t, accumulator ++ read_buffer(buffer), [], :common)
      _ -> token(t, accumulator, buffer ++ [h], :text)
    end
  end

  defp read_buffer([]), do: []

  defp read_buffer(buffer) do
    [buffer |> Enum.join("") |> String.to_atom()]
  end
end

Jest prawie dobrze. Czwarty parametr funkcji steruje nam trybem pracy. W podobny sposób możemy implementować kolejne tryby np. tryb komentarza. To, co jest jednak nie do końca dobre, to wzrost liczby parametrów. Z drugiej strony możemy wprowadzić jakąś strukturę, która będzie opisywać aktualny tryb pracy i na jej podstawie uruchamiać różne strategie obsługi znaków.

Podsumowanie

Na koniec kilka słów podsumowania. Po pierwsze kod jest w miarę zwięzły. Po drugie trochę inaczej obsługuję błędy, ale to już jest kwestia języka i tego w jaki sposób chcemy informować użytkownika, że napisał coś głupiego. Po trzecie cała ta zabawa zapoczątkowana przez Jarka i Wiktora jest tylko zabawą i nie ma na celu stworzenia jakieŋ poważnego produktu.
Dla mnie osobiście jest to ciekawe doświadczenie. Nigdy nie miałem okazji napisania kompilatora, a ten cykl pozwoli mi na podjęcie wyzwania. Kolejne wersje kodu będą dostępne w repozytorium

O stringów w postgresie porównywaniu

Pracując na jednym z projektów, trafiliśmy na „ciekawe inaczej” wymaganie. Chodziło o to, by dodać wyszukiwanie po adresie email. Prosta sprawa, ale nie do końca.

Gmail a wielkość znaków

W adresie e-mail co do zasady ważna jest wielkość znaków. No, chyba że jesteś gmailem, to wtedy nie. Gmail jest usługą, która próbuje być cwana. Po pierwsze gmail nie rozróżnia wielkości znaków. Po drugie gmail nie uznaje kropek w adresie. Ma to pewne konsekwencje dla osób, które są przywiązane do kropek.
Po trzecie, większość naszych klientów wykorzystuje gmaila jako swój główny adres w naszym systemie lub ichni system korpo-poczty wykorzystuje gmaila.
Tu pojawił się problem.

Zrobisz selecta i będzie dobrze

Najprostszym rozwiązaniem naszego zadania byłoby stworzenie kodu, który uruchamia pod spodem zapytanie jak to tutaj:

Listing 1. Rozwiązanie naiwne

Select * from accounts where email_address='admin@example.com'

Przy czym problem z tym zapytaniem leży w sposobie porównywania wartości. Dobrze ilustruje to poniższy test:

Listing 2. Test rozwiązania naiwnego

@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class AccountRepositoryTest {

	@Autowired
	AccountRepository repository;

	@Test
	@Order(1)
	void readAndWriteSameCase() {
		Account s = new Account("admin@example.com");
		repository.save(s);
		assertThat(repository.findById("admin@example.com")).isNotEmpty();
	}
	@Test
	@Order(3)
	void readAndWriteNotSameCase() {
		assertThat(repository.findById("Admin@example.com")).isNotEmpty();
	}

}

Pierwszy test, readAndWriteSameCase, zakończy się sukcesem. Drugi będzie czerwony. Dlaczego?

Postgres jest OK

W przeciwieństwie do MySQLa Postgres prawidłowo rozróżnia wielkość liter. Tym samym proste porównanie dwóch wartości typu VARCHAR będzie zwracało false jeżeli wartości te różnią się wielkością liter właśnie.
Zidentyfikowaliśmy więc nasz „problem”, którego źródło leży, w niezrozumieniu tego jak działa baza danych, której używamy. Należy zatem pomyśleć o możliwych rozwiązaniach.

Rozwiązania i „rozwiazania”

Poniżej przedstawię kilka rozwiązań, które mają różny poziom dziwności, poręczności i słuszności.

Po stronie Javy

Pierwszym, bardzo naiwnym, ale też często spotykanym rozwiązaniem będzie umieszczenie „gdzieś w logice” wywołania toLowerCase. Nie jest to złe. Nie jest to też jakoś mądre. Jest za to bardzo naiwne i powinno być używane w ostateczności.

Czysty SQL – lower

Kolejnym rozwiązaniem jest takie przemodelowanie naszych zapytań, by używać funkcji lower z SQLa. Nasze przykładowe zapytanie mogłoby wtedy wyglądać tak:

Listing 3. Użycie lower

Select * from accounts where lower(email_address)=lower('admin@example.com')

Podobnie jak poprzednie, to rozwiązanie jest bardzo naiwne. Nie sprawdzi się w przypadku, gdy używamy JPA. Chyba że ręcznie ogarniemy każde zapytanie, gdzie potrzebujemy tego rodzaju porównań albo potrafimy posłużyć się hibernetową adnotacją @Formula. Co też nie do końca jest proste i ma swoje wady.

Czysty SQL – operator ~*

Postgres posiada operator ~*, który służy do porównania dwóch ciągów za pomocą wyrażeń regularnych POSIX. Po lewej stronie jest ciąg znaków, a po prawej wyrażenie:

Listing 4. Użycie ~

Select * from accounts where email_address ~* 'admin@example.com'

Operator ten ma kilka wersji:

  • ~ – Porównanie za pomocą wyrażenia regularnego, biorące pod uwagę wielkość znaków
  • ~* – Porównanie za pomocą wyrażenia regularnego, niebiorące pod uwagę wielkości znaków
  • !~ – Porównanie za pomocą wyrażenia regularnego, biorące pod uwagę wielkość znaków, zwraca true jeżeli nie ma dopasowania.
  • !~* – Porównanie za pomocą wyrażenia regularnego, niebiorące pod uwagę wielkości znaków, zwraca true jeżeli nie ma dopasowania.

I podobnie jak w poprzednim przypadku użycie tego operatora wymaga albo użycia SQL wszędzie gdzie to konieczne, albo kombinowania z adnotacjami.

Czysty SQL – rozszerzenie citext

Rozszerzenie to wprowadza do Postgresa typ citext, który zachowuje się jak text, ale ignoruje wielkość znaków. To rozwiązanie jest najmniej inwazyjne w kontekście kodu. Należy jedynie dołączyć do naszego projektu bibliotekę hibernate-types, w której znajduje się już gotowe wsparcie dla tego typu.

Podsumowanie

Opisany problem jest, paradoksalnie, nieoczywisty. We współczesnych systemach dokłada się dość szybko silniki wyszukiwania albo „hackuje” ten problem we wczesnym stadium życia projektu. W ten sposób powstają niezbyt zrozumiałe konstrukcje, których zadaniem jest zbędne weryfikowanie danych wejściowych. Co prawda nie trudno jest sobie wyobrazić tego typu problemy w systemach, gdzie ręcznie wprowadza się sygnatury dokumentów. Z drugiej strony jesteśmy przyzwyczajeni do pisania całej masy walidatorów, których jedynym zadaniem jest naprawianie błędów, wynikających z naszej niewiedzy. Pytanie czy to dobre podejście?

Hackerzy, oddajcie mi moje 400mln

W ostatnich dniach pojawiła się ciekawa informacja. Grupa czeskich aktywistów, w trakcie hackatonu, stworzyli prototyp elektronicznego systemu sprzedaży winiet. O samym projekcie możecie poczytać tutaj. W dużym skrócie z bodajże 18 elementów specyfikacji w czasie imprezy zaimplementowano 16, a te, których nie zaimplementowano to elementy powiązane z dostępem do informacji niejawnych. I można by w tym miejscu krzyknąć heap heap hura i podskakiwać z radości. W końcu „nasi” pokazali tym państwowym darmozjadom, że można lepiej wydać 400 mln koron. Można do tego dołożyć jeszcze, że dokopano Asseco, co jest rzeczą godną pochwały. Tyle, że nie do końca…

Rodzaje projektów i co z tego podziału wynika

Inżynieria oprogramowania jest dość ciekawą działką w kontekście relacji państwo-oprogramowanie, ale jeszcze ciekawszą działką jest światek starupowo-korporacyjny. Jedną z zasad w tym świecie jest ta, która mówi, że oprogramowania nie powinno się pisać, a kupować.

Kupuj, nie pisz

Nie mówi tylko o jakie oprogramowanie chodzi. Państwo może spokojnie kupować oprogramowanie użytkowe. Wręcz powinno, bo ciężko mi sobie wyobrazić, by ktoś chciał rozwijać swój OS albo pakiet biurowy. Nawet Korea Północna nie stworzyła swojego państwowego OSa od zera, a jedynie zbudowała własne distro Debiana.

Ale przecież, można wziąć darmowego Linuxa?

Można. Można też wziąć Libre Office. Tyle tylko, że później takiego darmowego Linuxa trzeba utrzymywać.

To się biurwy nauczą!

Tak. Nauczą się dokładnie w tym samym momencie, gdy ty nauczysz się ich pracy. Coś za coś. Szeroko rozumiana branżunia ma niestety problem z postrzeganiem pracy z komputerem. Dla nas wszystko jest proste i można to szybko ogarnąć. Nawet skomplikowane koncepcje „same wchodzą”. Mamy wprawę w modelowaniu procesów i tłumaczeniu ich na programy. To jednak za mało, bo nie mamy głębokiej wiedzy, która idzie wraz z tymi procesami. Szczególnie jeżeli procesy te dotyczą np. procedury administracyjnej. Podsumowując, niech biurwy potrafią sprawnie biurwować, a nie klepać skrypty w bashu.

Kupujesz sprzęt i wsparcie

W idealnym przypadku państwo powinno kupować sprzęt oraz wsparcie do oprogramowania o otwartym kodzie. Do tego dochodzą szkolenia, które powinny jednak być specyficznie zaplanowane. Szkolenia powinny być realizowane wewnątrz organizacji przez osoby zatrudnione w tej organizacji. Niektóre z tych osób powinny szkolić się na zewnątrz (u twórców), a swoją wiedzę przekazywać innym szkoleniowcom. Dzięki temu państwo zachowuje znaczną niezależność od zewnętrznych szkoleń, a jednocześnie może wpływać na program szkolenia, tak by dostosować je do potrzeb pracowników.

Rozszerzaj to co kupiłeś

Drugim podejściem jest zakup oprogramowania, które jest najbliżej wymagań i jego modyfikacja. Część procesów i potrzeb, jakie ma państwo, można ogarnąć za pomocą modyfikacji dostępnego oprogramowania. Wynika to z faktu, że niektóre procesy realizowane przez państwo nie różnią się od typowych procesów biznesowych. Publikowanie dokumentów w BIPie, choć musi spełniać pewne reguły, nie jest niczym innym jak tworzenie treści za pomocą CMSa. Serio. To, co zrobili Czesi, to przecież sklep z winietami postawiony na otwartym CMSie.

Pisz rzeczy specyficzne

Takich rzeczy jest zazwyczaj niewiele. Ich specyficzność nie leży w procesach lub wymaganiach jako takich, ale w sposobie obsługi, czy dodatkowych cechach. Co prawda rejestr PESEL nie odbiega od typowego rejestru kadrowego. Ba można powiedzieć, że jest znacznie prostszy. Jednak jego specyfika leży w informacjach, które przechowuje oraz w sposobie dostępu do nich.
Osobną grupę stanowią procesy objęte klauzulami poufności czy tajności. W ich przypadku to wystarczy, by wyłączyć je z analizy „czy jest coś, co można dostosować”. Przynajmniej na poziomie oficjalnego przetargu.

Urzędy nie powinny pisać oprogramowania

Urzędy, a nie państwo. Dlaczego? Cóż urzędy jako takie mają dwa piony. Pion merytoryczny, który zazwyczaj składał się z kompetentnych ludzi i pion polityczny, który składa się z ludzi „dających twarz” takim, a nie innym decyzjom. Piony te przenikają się w mniejszym bądź większym stopniu. Przy czym w ostatnich 15 latach to raczej pion polityczny wpycha swoich w pion merytoryczny, a nie na odwrót.

// offtopic

Jeżeli uważasz, że kompetentny urzędnik, to oksymoron, to masz bardzo prymitywne postrzeganie świata. Serio.

//koniec offtopa

Urzędy pisać oprogramowania nie powinny, ponieważ nie mają do tego kompetencji. Byłem, widziałem. Rolą urzędów powinno być specyfikowanie wymagań dla oprogramowania, którego nie będzie się kupować w wersji pudełkowej.

Co może zrobić państwo?

Państwo ma kilka dróg, którymi może realizować swoje potrzeby w zakresie informatyzacji.

Tylko kupować

Państwo może kupować oprogramowanie w ramach jakiejś procedury. Czasami oznacza to, że kupi gotowce w pudełkach (systemy operacyjne, pakiety biurowe). Czasami oznacza to, że kupi zmodyfikowane oprogramowanie otwarte. Czasami rzeczywiście kupi coś napisanego od zera.
Wadą tego rozwiązania jest praktyczne zamknięcie kodu i specyfikacji. Wiele lat walki twórców Janosika z ZUSem o otwarcie specyfikacji systemów ZUS doskonale to ilustruje. To nie jest dobra droga.

Kupować gotowce, rozwijać samodzielnie

Ten model jest w pewnym sensie przeciwieństwem poprzedniego. Zakupy ograniczamy do minimum. Kupujemy tylko to, co nie będzie modyfikowane. Systemy operacyjne, pakiety biurowe, oprogramowanie do obsługi systemu „numerkowego” czy kadrowego. Cała reszta prac jest przeniesiona do odpowiednich ośrodków akademickich. Państwo samodzielnie rozwija potrzebne oprogramowanie w swoich firmach. Tam też są prowadzone prace w zakresie R&D czy badań podstawowych.
Taki model jest możliwy i działał przez wiele lat w USA. Zaletą takiego podejścia jest zabezpieczenie państwa przed problemami dostawców. Całość oprogramowania, które wymaga jakiejś pracy, a nie jedynie instalacji z pudełka, jest własnością państwa. Źródła nie są może powszechnie dostępne, ale też nie muszą. Wadą jest duża bezwładność takiego systemu i koszty. Jeżeli chcemy być na bieżąco, to trzeba inwestować w badania. Te nie są tanie. W dodatku złe decyzje projektowe będą się ciągnąć przez dziesięciolecia.

Kupować gotowce i Otwarta specyfikacja

Poza wspomnianymi już kilkukrotnie pudełkami cała reszta oprogramowania nie jest rozwijana przez państwo. Zamiast tego państwo definiuje, ogłasza i utrzymuje specyfikacje. Utrzymuje też infrastrukturę potrzebną do testowania zgodności oraz certyfikuje producentów w procedurze administracyjnej.
Tu mała uwaga, procedura administracyjna ma to do siebie, że nie bada „racji” stron, a jedynie sprawdza, czy spełnione są wymagania. Spełniasz wymagania, oto twój certyfikat i nie ważne, że napisałeś to w php4.
Zaletą tego modelu jest odpowiednia separacja odpowiedzialności pomiędzy biznes-państwo, a dostawców. Pozwala to też na uniknięcie sytuacji, gdzie dostawca nie chce ujawnić specyfikacji. Ta jest zawsze jawna. Eliminujemy w ten sposób monopolistów.
Oczywiście model ten ma też wady. Oprogramowanie nadal trzeba kupować. Nadal trzeba płacić za jego utrzymanie i rozwój. Pozostaje też problem jakości specyfikacji i jej szczegółowości. Specyfikacje też będą się starzeć z punktu widzenia technologii, co oznacza konieczność ich aktualizacji.

Hackatony nie są rozwiązaniem

Problem to sposób, w jaki państwo powinno zamawiać oprogramowanie. Na hackatonach można stworzyć bardzo dużo bardzo fajnego oprogramowania. Można pokazać, że taka czy inna koncepcja daje ciekawe możliwości albo zmaterializować dość mglisty pomysł. Takie imprezy to świetne miejsce by państwo i specjaliści IT wymieniali się pomysłami, ideami, wskazywali sobie wzajemnie problemy. To w takim miejscu powinna siedzieć fundacja Panoptykon, by móc mówić o problemach ochrony prywatności.
Hackatony jednak nie są miejscami, gdzie stworzy się konkretne rozwiązanie. Po imprezie w Czechach co prawda anulowano kontrakt, a władze chcą wdrażać rezultaty pracy uczestników. Jednak nie wyobrażam sobie, żeby teraz te czterdzieści kilka osób z dnia na dzień rzuciło pracę i ruszyło po kraju, by szkolić użytkowników. Albo, żeby rozdysponowało pomiędzy siebie kilka telefonów i ustaliło grafik dyżurów nocnych i weekndowych, bo system wymaga wsparcia 24/7. I co najważniejsze uczestnicy nie zrobią tego za darmo.
Media podając kwotę przetargu, rzucają konkretną liczbę. Nigdy nie starają się rozbić jej na poszczególne pozycje. Przemilczają też, ile trwa utrzymanie, czy cena obejmuje szkolenia i wdrożenie użytkowników. To wszystko kosztuje co najmniej tyle samo co „czyste programowanie”.

I o ile programowanie na hackatonie jest dobrą zabawą, to nikt nie będzie w tej formie utrzymywać oprogramowania.

Legendarny Osobomiesiąc – książka nie dla menadżerów

Okładka Legendarny Osobomiesiąc
Tytuł: Legendarny Osobomiesiąc. Opowieści o inżynierii oprogramowania. Wydanie II
Autor: Frederick P. Brooks Jr.
Rok: 2019 (1995 EN)
ISBN: 978-83-283-5090-8

Zgodnie z obietnicą krótka recenzja jednej z najciekawszych książek o zarządzaniu projektami IT, jaka kiedykolwiek powstała.

Szczypta historii

Autorem książki jest Frederick BrooksW, który był menadżerem projektu OS/360W z przyległościami. Zatem mamy tu do czynienia z człowiekiem, który walczył w okopach IT w czasach, gdy duża część z nas nie za bardzo miała jeszcze możliwość zaistnieć w jakiejkolwiek formie biologicznej. Pierwsze wydanie książki było w 1975, a drugie w 1995. I właśnie z tym wydaniem „na dwudziestolecie” mamy do czynienia. Polskie tłumaczenie ukazało się w 2019 roku.
Książka jest stara i ma to przeróżne konsekwencje. Siadając do lektury, należy o tym pamiętać.

Konsekwencja pierwsza – język

Legendarny Osobomiesiąc jest napisany w specyficzny sposób. Stare książki już tak mają, że język, którym posługują się autorzy, jest trudniejszy w odbiorze. Jeżeli dodamy do tego swoistą manierę używania słów dłuższych niż konieczne oraz czasami osobliwą składnię, to efekt końcowy jest „skomplikowany”.
Tak też jest tutaj. Książka jest napisana w bardzo „barokowym stylu”, który odszedł do lamusa wraz z nastaniem blogów i popularyzacją krótszych form cyfrowych.

// offtopic

Szczytem językowych wygibasów tamtej epoki jest Domain Driven Design Erica Evansa z WSZECHOBECNYM nadużywaniem capslocka i dziwnych elementów składu.

// koniec offtopa

Dodatkowo wiele z poruszanych w niej problemów nie istnieje już w zbiorowej świadomości IT. Tym samym niektóre elementy zdają się dziwne i bezsensowne.
Język książki determinuje też sposób jej tłumaczenia. Niewdzięczna to praca, bo z jednej strony należy zachować oryginalny charakter, a z drugiej aż prosi się o uwspółcześnienie. Jednakże tłumacz ciała nie dał i jego pracę oceniam na całkiem, całkiem.

Konsekwencja druga – kontekst

Jeżeli damy tę książkę osobie, która nie ma pojęcia, jak wygląda współczesny cykl rozwoju oprogramowania, to bardzo szybko okaże się, że mamy poważny problem. Pojawią się pytania w tylu „ile czasu zajmuje ci debuggowanie”, „czy mam gotową osobną stację roboczą do debuggowania”, „ile linii kodu dostarczamy w ciągu roku”. Ciekawe, prawda? Brooks pisał książkę, gdy Intel pokazał światu procesor 8080W, czyli gdy wybuchła rewolucja mikrokomputerów. Drugie wydanie pojawiło się w roku premiery Windowsa’95. Zatem autor ma bardzo ograniczony zasób wiedzy w porównaniu do nas. Dzisiaj.
Oczywiście można podjąć próbę translacji pewnych aspektów inżynierii oprogramowania z dawnych czasów na nam współczesne. Jest to jednak trud nie tyle daremny ile bezsensowny.
Drugie wydanie zostało co prawda uzupełnione o 20 lat doświadczeń, a co za tym idzie m.in. dyskusję nad klasycznym już dzisiaj artykułem Brooksa pt. „There is no silver bullet”. Jednak nadal istnieje 25-letnia wyrwa w wiedzy pomiędzy współczesnością, a czasami drugiego wydania.

Konsekwencja trzecia – wnioski autora

To samo dotyczy wniosków, które przedstawia autor. Z dzisiejszej perspektywy wiemy, że wiele z zaproponowanych rozwiązań nie sprawdziło się, albo spowodowało poważne problemy. Mam tutaj na myśli przede wszystkim wprowadzenie „wszechwiedzącego” i niekodującego architekta, próby formalizacji na poziomie globalnym (dla danej organizacji), czy też założenie, że zespoły programistów nie są w stanie sprawnie się komunikować.
Oczywiście Brooks miał rację w kilku kwestiach. Programowanie wizualne nie zawojowało rynku. Podobnie jak automatyczne tworzenie kodu na podstawie specyfikacji.
Nadal kluczową rolę w procesie tworzenia oprogramowania odgrywa przygotowanie specyfikacji. Nadal mamy problemy z sensownym testowaniem naszych programów, choć proces debuggowania zastąpiliśmy metodykami w rodzaju TDD. Nadal też nie mamy idealnej metody komunikacji ani niczego co pozwoliłoby na precyzyjne planowanie czasu pracy nad zadaniami.
W końcu w książce tej mamy sformułowane Prawo BrooksaW wraz z późniejszą jego dyskusją i weryfikacją empiryczną, która jest wspomniana w drugim wydaniu. Jest to też największa wartość tej książki, która nie ulegnie dezaktualizacji.

Dla kogo ta książka?

Na pewno nie dla młodych menadżerów. Z racji wieku nie będzie to książka, która cokolwiek im da. Jednocześnie każdy doświadczony menadżer powinien ją przeczytać. Pozwoli mu to zrozumieć, w jaki sposób jego decyzje mogą wpływać na zespół. Jaka jest ich rola w zespole. Dlaczego pojawiają się problemy?
Czy jednak jest to książka dla programistów?

Książka historyczna

Chcąc zrozumieć współczesność, należy przeanalizować przyczyny. Te mają swoje źródła w historii. Książka Brooksa jest pod tym względem idealna. Jako że jej autor uczestniczył w jednym z najbardziej skomplikowanych projektów w historii informatyki, a następnie spisał swoje przemyślenia, to wnioski z tamtych wydarzeń stały się podstawą do analizy i opracowania metodyk wytwarzania oprogramowania. Metodyki te stały się standardem na kolejnych 30 lat. W dodatku wrosły one w branżę i nadal są ważnym jej elementem. Szczególnie że pokolenie starszych menadżerów uczyło się tworzenia oprogramowania m.in. na tej książce.
Dlatego jest to lektura obowiązkowa dla wszystkich programistów, którzy nie chcą ślepo przeć do przodu, nie zważając na otoczenie. W rozdziałach dodanych w drugim wydaniu Brooks z entuzjazmem odnosi się do programowania obiektowego. Entuzjazm ten podobny jest do tego, jaki towarzyszy obecnie programowaniu funkcyjnemu. Czy za 25 lat nie będziemy mieli podobnego „moralniaka”, mówiąc o FP?

Warto.