corum blog

Twój nowy blog

To nie jest pierwszy raz, kiedy pisze o tym projekciku, ale wyglada na
to, ze ta notka zamkne ten temat na dobre. Jak moga zauwazyc uzytkownicy
przegladarek obslugujacych JavaScript – na blogu pojawila sie zielona, pozioma
belka (po lewej), ktora rozwija sie w okienko chatu po kliknieciu. (Edit:
nieprawda; nie mialem na to jeszcze czasu, bedzie wkrotce) Instrukcja obslugi:
wybrac jeden z dostepnych serwerow (ktory – to zalezy od tego, gdzie akurat
jestem) a potem juz tylko wesolo rozmawiac sobie z innymi uzytkownikami,
polaczonymi z tym samym serwerem.

Oczywiscie, trafienie na kogokolwiek poza mna graniczy z
cudem, ale nie o to chodzi – zeby sie przekonac, ze chat rzeczywiscie
dziala, wystarczy otworzyc ta strone rownolegle w dwoch zakladkach,
rozwinac dwie instancje chatu i cos wpisac. Przy odrobinie szczescia
wpisany w jednym okienku tekst pojawi sie takze w drugiej zakladce.

Jasne, ze ten program nie jest jakos specjalnie imponujacy -
wymagalby mnostwa pracy, na ktora nie mam ani czasu, ani ochoty, by
stac sie uzytkowa aplikacja. Nie mozna sobie wybrac nicka, nie mozna
wybrac koloru, w ktorym wyswietlane sa nasze wiadomosci, nie ma
prywatnych wiadomosci ani praktycznie nic, czego bysmy sie spodziewali
po takiej appce. Hell, nawet data/czas nadeslania wiadomosci sa zle
formatowane. Jaki wiec jest sens pokazaywania jej i jaki byl sens ja
pisac?

Pewien sens tych zabiegow, jak sadze, zawiera sie w kilku
sformulowaniach: asynchronous message passing, ajax long polling,
JSONP, HTTP server i jeszcze paru im podobnym. Piszac ten program
sporo sie nauczylem o tych zagadnieniach – co dla mnie juz jest
dostatecznym uzasadnieniem dla tych kilkunastu (moze
dwudziestu-kilku, ale chyba nie wiecej, niz trzydziestu) godzin
spedzonych nad tym kodem w pociagach i po nocach. Teraz chce sie ta
wiedza podzielic – wiec pokazuje dzialajacy kod, mam nadzieje
efektywnie zabezpieczajac sie przed komentarzami w stylu a co ty
tam wiesz.

Wybierajac technologie, w ktorych implementowalem swoj
blogchat (taki codename, no co?) kierowalem sie calkiem
swiadomie i z premedytacja ku rejonom, ktorych nie znalem dotad za
dobrze – czesc serwerowa aplikacji moglbym z latwoscia zrealizowac w
Pythonie (przy uzyciu Twisted albo gevent, na przyklad – i to
nawet przy zalozeniu asynchronicznosci), co byloby dla mnie
wielokrotnie latwiejsze. Wybralem jednak Erlanga, zeby z jednej strony
ugruntowac sobie jego znajomosc, a z drugiej byc zmuszonym do uzywania
prawie czysto funkcyjnego stylu programowania. Dzieki podjeciu sie
implementacji prawdziwego programu musialem przeczytac duzo
erlangowego kodu, do ktorego nigdy bym nie dotarl wykonujac tylko
cwiczenia z tutoriali.

Jesli chodzi o owoce tego wysilku, to mam mieszane odczucia. Z
jednej strony asynchroniczny workflow jest w przypadku Erlanga
naturalny jak oddychanie oraz wyjatkowo elegancki (czytelnicy znajacy
JS moga z niedowierzaniem pytac, czy to w ogole mozliwe – jak widac,
owszem), z drugiej jednak Erlang zdecydowanie nie zostal stworzony do
obslugi protokolow tak wysokiego poziomu, jak HTTP. Problemy z
obsluga stringow i koniecznosc pisania wlasnych funkcji realizujacych
rzeczy zazwyczaj dostarczane przez biblioteki byly odrobine
rozczarowujace. Najwiekszy chyba jednak zawod sprawil mi Erlang -
erlangowe podejscie do programowania funkcyjnego – w kwestii
czytelnosci kodu. Nie mowie o kodzie wlasnym – nie dosc, ze jest
stosunkowo nieskomplikowany, to jeszcze dolozylem wszelkich staran, by
byl dobrze obkomentowany i tak czytelny, jakim tylko dalem rade go
uczynic. Chodzi mi o biblioteki i projekty, ktorych zrodla czytalem w
poszukiwaniu funkcji na przyklad do odpowiedniego escape’owania znakow
niedozwolonych w URLach – ich kod jest juz dostatecznie skomplikowany,
by byc nieczytelnym. Mysle, ze glownym czynnikiem przeszkadzajacym w
pisaniu czytelnego kodu w Erlangu jest fakt, ze zmiennym mozna
przypisywac wartosc tylko raz, co prowadzi do tworzenia szeregow nazw
typu Var1, Var2, Var3 i tak dalej. W porownaniu jednak ze Scheme
(praktycznie write once, never read jezykiem) Erlang wciaz wypada
lepiej.

Edit: popracowalem w Erlangu jeszcze troche i stwierdzam, ze
nie jest tak zle; to jednak w duzej mierze kwestia przyzwyczajenia, troche jak
w przypadku jezykow naturalnych, gdzie trzeba sie *osluchac* z danym jezykiem,
co zajmuje zazwyczaj wiecej czasu niz sama nauka podstaw.

Ostatecznie jednak pisanie w Erlangu bylo na tyle przyjemne,
ze drobne niedogodnosci nie zniechecily mnie do tego jezyka. Wciaz nie
jestem przekonany, czy gdybym mial projektowac jakis duzy system
chcialbym pisac go w calosci w Erlangu – ale z cala pewnoscia te jego
elementy, ktore powinny byc mozliwie niezawodne, skalowalne i wydajne
nadawalyby sie do implementacji w nim.

Drugim elementem ukladanki jest Java-, a wlasciwie
CoffeeScript. Te dwa jezyki roznia sie od siebie skladnia, CS
dostarcza kilku wygodnych elementow (destructuring assignment,
array comprehensions) ktorych JS nie posiada, ale generalnie jest
wciaz tym samym jezykiem – dzieki temu moglem skorzystac z
przeznaczonej dla JS biblioteki Backbone, z ktora zetknalem sie po
raz pierwszy w pracy. O ile JS i CS nie byly mi obce, o tyle Backbone
(w czasie, gdy pisalem frontend blogchata) byl czyms kompletnie nowym.

Backbone jest reklamowany jako sposob na strukturyzowanie kodu
pisanego w JS w taki sposob, zeby zamiast wielkiego bloba handlerow,
logiki i prezentacji miec czytelnie rozgraniczone elementy
Model-View-Controller. Trzeba przyznac, ze to, co obiecuje, Backbone
dostarcza – ale nie moge w tym miejscu nie wspomniec, ze jest on tylko
jednym ze sposob na strukturyzowanie kodu w JS i ze nie spodobal mi
sie jakos szczegolnie. W trakcie pracy nad blogchatem poznalem
wewnetrzna architekture Backbone i musze przyznac, ze sam nie bardzo
wiem, jak moznaby dostarczane przez biblioteke abstrakcje inaczej
zaimplementowac lub jakimi innymi je zastapic, wiec moja krytyka jest
dosc slaba: to po prostu poczucie, ze „cos jest nie tak”. Ostatnio
przygladam sie bibliotece Knockout, ktora wyglada dosc obiecujaco. Z
drugiej strony Spine jest pisane w CoffeeScripcie i tez przykulo moja
uwage – ale to zagadnienia na pozniej.

Ciezko podsumowac strone frontendowa jednym zdaniem chocby
dlatego, ze takie podsumowanie niczego nie zmieni – innego sposobu
realizacji klienta (w przegladarce, pomijajac applet/flash) obecnie
nie ma. Co jednak moge powiedziec z cala pewnoscia to to, ze
CoffeeScript rzeczywiscie dziala (kod w nim zapisany jest o 40%
krotszy od JSowego) i ze nie korzystanie z jakiejs biblioteki
porzadkujacej kod JSowy to samobojstwo przy wiekszych projektach.

Tyle tytulem wstepu, ogolnego wprowadzenia. Teraz przedstawie
architekture obu czesci aplikacji (klienta i serwera), przyjety
protokol, by potem pokazac szczegoly implementacyjne.


Zeby napisac jakikolwiek kawalek oprogramowania, konieczne
jest wczesniejsze zdefiniowane problemu, jaki chcemy rozwiazac czy zadania,
jakie chcemy wykonac. W przypadku blogchata chcialem umozliwic
uzytkownikom odwiedzajacym rownoczenie te sama strone komunikacje
(tekstowa) miedzy soba. Ze strony uzytkownika najbardziej podstawowa
sesja z aplikacja powinna wygladac tak:

1. Otworzyc adres w przegladarce.
2. Wpisac jakas wiadomosc w okienko – ta wiadomosc zostaje wyswietlona
u wszystkich innych uzytkownikow ogladajacych ta strone.
3. Jesli ktorykolwiek z innych uzytkownikow wpisze jakas wiadomosc, to
pojawi sie ona w okienku.

Do tego postanowilem dolozyc kilka zdroworozsadkowych
ograniczen na sposob implementacji:

1. Opoznienie pomiedzy wyslaniem przez jednego, a otrzymaniem przez
pozostalych uzytkownikow wiadomosci powinno byc jak najmniejsze.
2. Liczba zapytan widocznych w przegladarce (w Firebugu, na
przyklad) powinna byc jak najmniejsza.
3. Nie powinno miec znaczenia, gdzie znajduje sie (geograficznie i
logicznie) ktory element aplikacji.

Majac tak zdefiniowany problem zabralem sie za projektowanie
wlasciwego rozwiazania. Jakkolwiek kuszaco nie brzmialoby utworzenie
sieci p2p miedzy klientami (uzytkownikami ogladajacymi strone) nie
jest to niestety mozliwe bez pisania rozszerzenia dla przegladarek, z
jakich korzystaja. Jedyna wiec mozliwa architektura okazala sie
klasyczna architektura klient-serwer, z jednym centralnym serwerem
przyjmujacym i rozsylajacym wiadomosci do klientow. Zadania klienta
moze wtedy opisac tak:

1. Polacz sie z serwerem, nasluchuj nowych wiadomosci.
2. Jesli uzytkownik cos napisze, to wyslij to serwerowi jako nowa
wiadomosc.

A dzialanie serwera tak:

1. Nasluchuj dwoch rodzajow polaczen: get i post.
2. Polaczenie get obsluz dodajac je do listy oczekujacych na
wiadomosci.
3. Polaczenie post obsluz wysylajac jego tresc do wszystkich polaczen
oczekujacych na liscie.

Zaczalem od – z tego co pamietam – zaimplementowania…

Ciag Dalszy (prawdopodobnie) Nastapi :)


Czytam sobie czasem Slashdota – moze niekoniecznie jest to
wylacznie „stuff that matters”, ale rzeczywiscie odsetek newsow
przykuwajacych moja uwage jest na tym serwisie znacznie wyzszy, niz na
wielu innych. Co jakis czas zdarzaja sie jednak dyskusje, ktorych
lektura wywoluje u mnie obrzek twarzy (od facepalmow) i conajmniej
niestrawnosc.

Ostatnim tego przykladem jest ten
oto wpis
, w ktorym jakis czlowiek pyta, co ma zrobic, zeby
sluzbowego laptopa wykorzystac – bez konsekwencji – do prywatnych
celow. W komentarzach zas – dziesiatki osob niemalze jednym glosem
odradzaja robic cokolwiek, bo to w koncu wlasnosc firmy i tak
dalej.

Nie jestem w stanie opowiedziec mojego zdumienia, kiedy to
przeczytalem. Jak to – pomyslalem sobie – to laptop sluzbowy moze byc
jakos „zablokowany”? To istnieja na tym swiecie firmy, ktore tak
scisle kontroluja co dzieje sie z ich sprzetem, ze az daja pracownikom
komputery z zablokowana opcja instalowania czegokolwiek?

Kiedy zaczalem pracowac dla 10clouds kolezanka wreczyla mi pudlo z
komputerem i plyte z Ubuntu: „masz, zainstaluj sie gdzies”. Kilka
tygodni pozniej stwierdzilem, ze nie chce sobie psuc nerwow Linuksem,
a ze wlasnie premiere mialo FreeBSD 9 – zainstalowalem je sobie. Nie
pamietam, czy komus o tym mowilem. Pojawil sie problem – zintegrowana,
intelowska grafika nie jest jeszcze przez FBSD obslugiwana (wymaga
solidnych zmian w kernelu – nie zdarzyli z mergem przed 9.0).

Przynioslem wiec swojego laptopa, z ktorego przez jakis czas
korzystalem jako z serwera X Window – programy uruchamiane na moim
sluzbowym komputerze wyswietlaly sie na monitorze laptopa i przy
okazji na zewnetrznym monitorze, ktory przeciez nie musial byc
podlaczony do skrzynki pod biurkiem.

Trwalo to jakis czas – az w koncu firma zafundowala mi karte
graficzna – jakas nvidie – ktora FBSD obsluzyl bez problemu.
Przestalem przynosic laptopa, za to przynioslem swoj, drugi, monitor
(TwinView, wspaniala sprawa). Poniewaz rozdzielczosci tych monitorow
sie roznily, na mniejszym brakowalo kilkudziesieciu pikseli od dolu…
Wiec ostatecznie dostalem tez drugi monitor!

Przez caly ten czas nie uslyszalem ani jednego zlego slowa, nikt
nie pytal „co ja znow wymyslam”, nikt nie mowil, zebym nic nie robil
ze sprzetem, bo popsuje – przeciwnie, wszyscy, z ktorymi o moich
(nazwijmy to) problemach rozmawialem byli nadzwyczaj pomocni.

Czytajac komentarze na /. zdalem sobie sprawe, ze mam po
prostu niesamowite szczescie, trafiajac w zyciu na ludzi sensownych,
takze przelozonych, ktorzy rozumieja na czym polega moja praca i sa
sklonni uszanowac moje przyzwyczajenia. Dwa monitory to nie jest szpan
- mam kiepski wzrok i kiedy ustawie sobie czcionki odpowiednio duze na
jednym ekranie miesci sie jeden dokument. FreeBSD to tez nie fanaberia
- administrowalem tym systemem przez lata i zbudowanie sobie
srodowiska do pracy w nim zajelo mi znacznie mniej czasu, nizbym mial
sie uzerac z obcymi konfigami jakiegos Linuxa

Ludzie innych zawodow byc moze inaczej korzystaja z komputerow -
nawet na pewno – ale dochodza mnie sluchy, ze takze wsrod programistow
praca w kompletnie zablokowanym srodowisku jest popularna. Wynikalo to
tez z komentarzy na slashdocie. Ja wiem, ze bym tak nie potrafil -
moje srodowisko developerskie ewoluuje, rozwija sie, rosnie. Wszystkie
tweaks, ktorych dokonuje – to przeciez nie jest sztuka dla sztuki, to
jest zawsze sposob na przyspieszenie pracy. W 10clouds wszyscy to
rozumieja. A reszcie, no coz, moge tylko wspolczuc.

Przy okazji – moj konfig do VIMa jest tu ;-)

Right, bym zapomnial. Nie mialem netu, za to sporo jezdzilem pociagiem, wiec moj projekcik chata w tandemie technologii Erlang/JavaScript ma sie bardzo dobrze. Generalnie juz dziala, aczkolwiek pretty please – nie uzywajcie go jeszcze! W kazdym razie, jesli sami nie poprawicie kwestii escapeowania kodu w przesylanych wiadomosciach! Obecnie jesli jeden uzytkownik chatu napisze „<script>alert(‚fuck’);</script>” – kod ten dotrze niezmieniony do wszystkich pozostalych uzytkownikow i zostanie wykonany!!!

Kod zyje sobie pod
http://bazaar.launchpad.net/~klibertp/junk/blogchat/files
; zeby go uruchomic trzeba miec Erlanga w dowolnej nowozytnej wersji i juz. Zeby sie nim pobawic, warto miec kompilator CoffeeScript (choc ofc manipulacja samym jsem tez da rade).

Jak zalatam wspomniana dziure – i kilka innych drobiazgow i ficzersow zaimplementuje – chat bedzie dumnie eksponowany na blogu. Patche i uwagi (w kodzie, o kodzie, do kodu) jak zawsze oczywiscie mile widziane!

Na poczatek dwie sprawy – ostatnie trzy tygodnie bylem niesamowicie
zajety – przeprowadzalem sie do innego miasta, nie mialem w nowym
mieszkaniu dostepu do Internetu, w pracy natomiast raczej pracowalem,
niz robilem cokolwiek innego. Stad tez uzbieralo mi sie mase
zaleglosci; obiecuje, ze wszyscy ktorzy cos do mnie wyslali doczekaja
sie odpowiedzi juz niebawem. Tymczasem – bo wymaga to najmniej wysilku
- odpowiem „hipotetycznemu”, ktory skomentowal moja poprzednia notke
(dzieki, ze zadales sobie trud skomentowania!).

Hipotetyczny napisal:

Jak odpowiesz ‚typowemu sprzedawcy frytek’, gdy powie:

I tutaj wymagane jest pierwsze wyjasnienie – dwie poprzednie notki
zostaly napisane w tonie raczej prowokacyjnym, ze wzgledu na rozmowe
toczona w owym czasie z moim znajomym. Zdaje sobie sprawe, ze uzylem
mocnych sformulowan, ktore mogly niektorych urazic i przepraszam za
to. Posluzylem sie jednak nimi w celu zilustrowania mojej tezy i ten
zamiar udalo mi sie zrealizowac, wiec z mojej strony o „sprzedawcach
frytek” juz nikt nie uslyszy. A teraz do rzeczy.

Co do wszystkiego to do niczego.

Swieta prawda! Jakikolwiek jezyk, jakakolwiek technologia, ktora
obiecuje, ze sprawdzi sie doskonale w kazdej sytuacji, musi byc
zwyklym shitem. Kazda z bardzo, bardzo wielu istniejacych technologii
posiada swoje mocne i slabe strony; jednym z najtrudniejszych wyzwan w
pracy architekta oprogramowania i programisty to umiejetnosc dobrania
wlasciwych narzedzi do rozwiazania danego problemu. Oczywiscie, w
dalszym ciagu jest to latwiejsze, niz najtrudniejsze zadanie, jakie
przed programistami stoi, czyli koniecznosc uprzedniego zdefiniowania
problemu! – ale to dygresja.

To tyle, jesli patrzymy na problem jako stricte techniczny. Jesli
wliczymy czynnik ludzki, well – spojrzmy dalej.

Ja skupiam się na jednym języku/technologi, aby być w tym pro.

Pomijajac juz wszystko inne – ilez mozna? Ile czasu trzeba
poswiecic, zeby byc „pro” w jednej tylko technologii? Rok? Dwa, byc
moze, ale to wszystko – potem albo zaczynasz ta technologie
wspoltworzyc (jesli sie da) albo ja zmieniasz, bo jestes smiertelnie
znudzony i nie chcesz miec z nia nic wiecej wspolnego.

Zreszta, poczytaj wypowiedzi programistow w Sieci – tych, ktorzy
maja kilka dekad pracy za soba. Niemal kazdy z nich twierdzi, ze zna
kilkanascie jezykow programowania (najczesciej od COBOLa po C# – pelen
przekroj przez historie informatyki) i dziesiatki technologii, w
ktorych w pewnym momencie swojej kariery tworzyli.

To podejscie jest swoja droga tak zle, na tak wielu plaszczyznach,
ze nie bardzo wiem, co jeszcze napisac. Poznanie LISPa moze uczynic
Twoj kod w JavaScripcie o cale nieba lepszym; poznanie Eifella moze
sprawic, ze Twoj kod w PHP przestanie ssac z wielomlaskiem. Narzedzia
sa rozne, ich znajomosc pozwala patrzec na problem z roznych
perspektyw, ale przeciez problem pozostaje ten sam!

W końcu firmy potrzebują speców, a nie pajaców od wszystkiego.

Mylisz sie – firmy potrzebuja realizowac projekty jak najtaniej, to
znaczy przy uzyciu jak najmniejszej ilosci ludzi. Tak to mniej wiecej
rozumiem. Spojrz ile jest ogloszen, w ktorych pracodawcy poszukuja (w
mojej dziedzinie) ludzi wylacznie od CSSow – a ile takich,
gdzie potrzeba znajomosci Pythona, jakiegos frameworka, JS i jQuery,
HTMLa i CSSow oraz na koniec Postgresa albo MySQL.

Ciekaw jestem, skad Ci sie wzielo w ogole takie przekonanie – ze
lepiej sie nie rozwijac, lepiej sie zasklepiac zamiast otwierac na
nowosci – i ze lepiej jest byc ograniczonym bardziej, niz mniej. Dla
mnie to kompletnie niezrozumiale – tyle razy sie przeciez slyszy (ja
slyszalem, w kazdym razie) o placzu developerow, ktorzy utopili czas w
nauke i zwiazali sie z jakas platforma, ktora akurat kilka miesiecy
pozniej poszla na dno (gdyby znali nie tylko ta jedna, wzruszyli by
ramionami i kodowali dalej). Tyle razy widzialem ludzi wylewajacych
swoje zale gdzies na forum publicznym, ze zostali zwolnieni – firma
zmienila technologie, na ktorej sie opieralo jej funkcjonowanie, a oni
tej nowej ani nie znali, ani chcieli poznac…

Nvm, bo juz mi to troche za duzo czasu zjadlo: jesli chcesz sie
skupic na jednej tylko rzeczy i byc w niej prawdziwym pro – to niech
to bedzie programowanie. Zobaczysz, ze po kilku latach jezyk, ktorego
wlasnie uzywasz stanie sie tylko jednym z wielu narzedzi w Twoich
rekach, a miedzy rozmaitymi technologiami bedziesz przebieral jak
malarz w swoich pedzelkach albo slusarz w wytrychach ;-)

Nota bene: pewien kolega z mojej obecnej pracy wlasnie tym mi
zaimponowal; powiedzial, ze „trzeba bylo cos z tym zrobic, no to sie
nauczylem Backbone [framework w JS] i zrobilem”. Right! O to chodzi!
Im wiecej umiejetnosci, tym lepiej; fakt, ze soft ktory napisal wciaz
sluzy dziesiatkom ludzi jest dla mnie koronnym dowodem.

To nie tak, ze dopiero teraz to zauwazylem, ale rzeczywiscie
przygotowujac notki i opatrujac swoj kod zdecydowanie wieksza iloscia
komentarzy niz zazwyczaj mialem okazje glebiej zastanowic sie nad dziwna
mieszanka angielskiego i polskiego, ktorej uzywam programujac. Skad
wzial sie ten moj (czy osobliwy?) zwyczaj – i czy nie jest aby
szkodliwy?

Uzywam naprzemiennie angielskiego i polskiego praktycznie wszedzie
- w komentarzach, w nazwach funkcji i zmiennych, w nazwach plikow i w
dokumentacji. Bardzo czesto czytajac swoj kod, nawet po dluzszym
czasie, nie zauwazam tych przeskokow – oba jezyki wydaja mi sie
calkowicie naturalne i nie mam problemow z rozumieniem ktoregokolwiek
z nich. Tutaj mala dygresja: jesli chcesz zajac sie programowaniem na
powaznie – musisz, ale to absolutnie musisz znac angielski. Nie znajac
go ograniczasz sie do obrzydliwie skapej literatury w jezyku polskim,
pozbawiasz sie mozliwosci kontaktu z wieloma wspanialymi ludzmi i
jeszcze fajniejszymi spolecznosciami. Jesli korzystasz z polskiej
wikipedii albo z jakiegos klona StackOverflow, zamiast oryginalow, to
wlasciwie nawet nie powinienes startowac w tej samej konkurencji, co
ludzie angielskojezyczni. No ale to wszyscy wiedza, wiec wrocmy do
tematu.

Skoro czytanie takiego kodu nie jest dla mnie – i nie powinno byc
dla nikogo innego, przynajmniej sposrod powaznych ludzi – problemem,
to wlasciwie moznaby zadac pytanie: „a dlaczego nie?” i na tym
skonczyc rozwazania. Bardziej interesujace wydaje sie jednak pytanie
„czemu jednak tak?” i to na nie chcialbym sobie odpowiedziec.

Zaczalem swoje rozwazania od uswiadomienia sobie – czy moze
przypomnienia sobie po raz kolejny – ze nazwy sa wazne. Slowa sa
wazne. Oczywiscie, ze komputerowi – kompilatorowi, maszynie
wirtualnej, whatever – ksztalt kodu nie robi zadnej roznicy. Ale jak
wielokrotnie juz bylo powtarzane kodu nie piszemy dla komputerow – kod
piszemy dla innych ludzi. Kazdy kawalek kodu bedzie wielokrotnie
wiecej razy przeczytany, niz byl przepisywany – a zdarza sie, ze kod
jest czytany czesciej, niz jest wykonywany! Gdyby nie bylo zadnej
roznicy miedzy nazwami funkcji `W` a `wyznacznik_macierzy` (lub
rownowaznie `matrix_determinant`, niewazne) – wszyscy
programowalibysmy w brainfuck ;-)

Nic dziwnego, ze o konwencjach nazewniczych napisano juz niejedna
ksiazke – poza indentacja to wlasnie uzywane identyfikatory maja
najwiekszy wplyw na czytelnosc kodu. Problem z tym podejsciem jest
jednak taki, ze skupia sie na odbiorze juz ukonczonego produktu – kodu
- pomijajac etap jego tworzenia. A praktyka pisania kodu wyglada tak,
ze wymyslenie dobrej nazwy – takiej, ktora mozliwie zwiezle a przy tym
zrozumiale opisuje czym jest to „cos” opatrzone identyfikatorem -
zajmuje czas i nie jest wcale latwe!

Zwlaszcza, jesli prototypujemy – wtedy nie mamy tak naprawde
pojecia, co dokladnie bedzie robic funkcja, ktora wlasnie
piszemy. Jak w takiej sytuacji wymyslic jej dobra nazwe? Wydaje sie -
a doswiadczenie wydaje sie to potwierdzac – ze to jest rzecz
niemozliwa. To jeden z najwazniejszych powodow, dla ktorych
przykazanie refactor mercilessly jest takie wazne – wraz z
rozwojem prototypu mamy obowiazek zmieniac nazwy funkcji i (rzadziej)
zmiennych tak, by coraz lepiej opisywaly kod. A wiec pierwsza
rada:
jesli nie wiesz dokladnie, jak nazwac dana rzecz, uzyj
nazwy, ktora latwo bedzie potem zmienic poleceniem „Replace”
(s/old/new/g) twojego edytora.

Nieco pozniej, w fazie dopracowywania kodu, zaczynaja sie prawdziwe
problemy. Powiedzmy, ze mamy 30 linijkowa funkcje – wywoluje pewnie z
10-15 innych funkcji i niewatpliwie cos robi. Mamy nadac jej
nazwe, ktora to „cos” okresli w miare dokladnie, ale mamy na
wymyslenie tej nazwy minute max i nie mamy pod reka slownika, zreszta
i tak nie zdarzylibysmy z niego skorzystac. Zastanawiajac sie nad tym
zorientowalem sie, ze to jest bezposrednia przyczyna mojego mieszania
jezykow w nazewnictwie – zamiast tracic czas korzystajac z tezaurusa i
naginajac swoj umysl do myslenia w jednym tylko jezyku, szukam nazw
rownoczesnie w dwoch zasobach slow. Prawdopodobnie, gdybybym znal
jeszcze jakis jezyk na odpowiednim poziomie zaawansowania, w moim
kodzie pojawilyby sie zmienne o nazwach zapozyczonych takze z
niego!

Zauwazylem takze, ze znacznie wiecej polskich nazw pojawia sie u
mnie we wczesnej fazie prototypowania – pod koniec pisania programu
juz raczej nie wystepuja. Mysle, ze wynika to ze specyfiki jezyka -
polski wydaje mi sie byc elastyczniejszy i czesto moge znalezc slowo,
ktore oznacza wystarczajaco duzo rzeczy na raz, zeby objac swoim polem
znaczeniowym wiekszosc z mozliwosci tego, czym funkcja moze sie w
przyszlosci stac. Wraz z postepem w kodowaniu – rozbijaniem kodu na
coraz mniejsze i coraz bardziej konkretne kawalki – angielskie nazwy
staja sie z kolei naturalniejsze, bo bardziej precyzyjne i
rownoczesnie krotsze. A zatem druga rada: nie dozwolisz
zmiennej/funkcji nazywac sie tak samo pomimo refactoringu. Od razu tez
trzecia rada: uzywaj takiej mieszanki wszystkich jezykow,
jakie znasz, ktore pozwola ci tworzyc nazwy jak najkrotsze a
jednoczesnie dokladnie opisujace to, co sie dzieje w kodzie.

Wszystkie te rozwazania powyzej sa – przynajmniej dla mnie -
sensowne, ale zakladaja, ze kod bedzie czytany przez ludzi znajacych
te same jezyki, co ty. Oczywiscie, jesli musisz zaprezentowac swoj kod
szerszemu audytorium, to wszystkie, ale to wszystkie nazwy i
komentarze musza byc angielskie; nie ma innej opcji i nie ma
wytlumaczenia dla kogos, kto przygotowujac swoj kod do publikacji poza
granicami swojego kraju (metaforycznymi) nie przetlumaczyl go w pelni
na ludzki. Bo, powracam tutaj do dygresji z poczatku notki, „po
ludzku” to znaczy po angielsku – kropka, dyskusji nie ma. Na przyklad
na StackOverflow kilka razy widzialem kod dolaczony do pytania z
nazwami po francusku lub niemiecku. Ten drugi nawet troche znam, ale i
tak dalem minusa! Wracajac kilka akapitow wyzej wzrokiem widze, ze juz
to napisalem – w kodzie liczy sie przede wszystkim czytelnosc, a ta
jest definiowana w duzym stopniu przeciez przez to, kto czyta!

To w ogole miala byc inna notka, inspirowana kodem, jaki przeslal
mi przyjaciel – kawalkiem z systemu, nad ktorym ma pracowac. Nie
chcialem pisac tylko na ten temat, bo notka bylaby wylacznie stekiem
przeklenstw; kawalek kodu odlozylem na pozniej. Teraz jednak bedzie
pasowal jak ulal, jako ilustracja tezy, ze czytelnosc jest wazna – ze
wazne sa nazwy, ze wazne sa slowa. Prosze, powiedzcie mi – nie
koloruje tego nawet, nie dodaje wciec, wklejam as is – WTF robi ten
kawalek kodu:

for (i = -1, l = (r = str.split(/rn|n|r/)).length; ++i < l; r[i] += s) {
for (s = r[i], r[i] = ""; s.length > m; r[i] += s.slice(0, j) + ((s = s.slice(j)).length ? b : "")){
j = c == 2 || (j = s.slice(0, m + 1).match(/S*(s)?$/))[1] ? m : j.input.length - j[0].length || 
c == 1 && m || j.input.length + (j = s.slice(m).match(/^S*/)).input.length;
}
}

Powiem szczerze, ze ja zgadlem dosc szybko: po regexpach, glownie,
ale tez dlatego, ze widzialem wielokrotnie podobny shit,
kiedy jeszcze pisalem w C. Autorze tego kodu, kimkolwiek jestes:
ssiesz. Jesli zalezalo ci na zminimalizowaniu ilosci znakow, to
googlowy closure compiler zrobilby to lepiej, a wlaczenie kompresji
gzipem po stronie serwera daloby zysk pewnie kilkunastokrotny wzgledem
tego. Jesli zalezalo ci na wydajnosci… przeciez i tak uzywasz
regexpow! Nie, ja wiem o co ci chodzilo: chciales pokazac, jaki fajny
jestes, ze umiesz zgwalcic petle `for` (tak, wszyscy wiedza,
ze we wszystkie jej czesci mozna wsadzic dowolna ilosc wyrazen, nie
tylko ty), zanieczyscic global namespace piecioma jednoliterowymi
zmiennymi i zajechac operatory logiczne tylko po to, zeby nikt inny
nie mogl tej funkcji przeczytac ani poprawic. Zamordowac to malo.

W kazdym razie – czytelnosc sie liczy – QED.

Przynajmniej raz na jakis czas – sugerowalbym raz na pol roku –
kazdy webdeveloper powinien napisac wlasny serwer HTTP. Nie
chodzi oczywiscie o robienie konkurencji Apache’owi czy Nginxowi,
tylko o lepsze zrozumienie technologii, na ktorej na codzien opieramy
swoje aplikacje. Dla mnie to doskonala okazja, zeby przecwiczyc nowy
jezyk programowania – bo, jak kazdy szanujacy sie programista, staram
sie dobrze opanowac przynajmniej jeden jezyk programowania na rok.
Ludzie, ktorzy tego nie robia, powinni sprzedawac frytki w
McDonaldzie.

Wiele jezykow tworzenie roznorakich serwerow ulatwia, oferujac
gotowe klasy czy frameworki. Choc pewnie wielu ze script
kiddies
, przez pomylke nazywanych programistami, momentalnie
rzuciloby sie na takie rozwiazania (zakladajac, oczywiscie, ze ktos by
ich zmusil do napisania wlasnego serwera), ale ja uwazam to za swego
rodzaju oszustwo. Z jednej strony biblioteki sa oczywiscie od tego,
zeby z nich korzystac, ale w sytuacji, kiedy chcemy cos przecwiczyc
jedynym efektem korzystania z takich skrotow jest pozbawienie sie
czesci cwiczen. Kiepski pomysl.

Celowo mowie o cwiczeniu, a nie poznawianiu -
webdeveloper, ktory nie rozumie czy nie zna w ogole tych
technologii (czyli socketow i HTTP) powinien umrzec ze wstydu
nastepnym razem, kiedy jako webdeveloper sie okresli…

Jakis czas temu zapowiedzialem, ze bede pracowal nad chatem,
ktory zamieszcze tutaj na blogu (chyba po lewej stronie? nie mam
pomyslu). Uwaga, to nie byla czcza pogrozka – prace rozpoczalem w
zeszly weekend i musze powiedziec, ze postepuja calkiem zgrabnie;
powinienem skonczyc w nastepny weekend.

Zalozylem, ze frontend napisze w CoffeeScript, a serwer w
Erlangu. Erlang to moj nowy-stary przyjaciel – poznalem go chyba ze
trzy lata temu i napisalem w nim kilka drobiazgow (kilka k loc[1], nie
wiecej), ale potem porzucilem go na rzecz innych jezykow. Wrocilem do
niego w zeszlym tygodniu i po krotkiej sprzeczce o interpunkcje (;,
. i , maja rozne znaczenia w roznych kontekstach) okazalo sie, ze
stara milosc nie rdzewieje – dogadalismy sie szybko i zabralismy do
roboty.

Pierwszy problem, o jaki sie potknalem, to same origin
policy
– przegladarki nie umozliwiaja zapytan AJAXowych do innych
niz obecny serwerow. Jak wiemy istnieja zasadniczo dwa rozwiazania tego
problemu: JSONP lub iframe. W tym drugim przypadku
musialbym jednak za duzo zakodowac recznie po stronie frontendu (a,
mowiac szczerze, frontend interesuje mnie srednio), tymczasem jQuery
bezposrednio wspiera JSONP. Wybor byl zatem oczywisty.

Naklepalem sobie bardzo prosta funkcje testujaca, czy moj
serwer prawidlowo odpowiada na zapytanie – mniej ze wzgledu na
trzymanie sie zasad TDD, bardziej ze wzgledu na fakt, ze piszac po
stronie serwera na poziomie socketow i tak potrzebowalem kilku
przykladowych pakietow HTTP, a w ten sposob moglem je sobie czytac bez
irytujacego klikania w ikonke Firebuga, stawiania proxy itp. Rzecz
zajela mi chyba pietnascie minut, a cala zabawe sponsorowal wujek
CoffeeScript i ciocia jQuery:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ping = (addr, alive, down) ->
    err = (req, status, thrown) ->
        #cialo log juz pokazywalem, zreszta kazdy pisze wlasne i tak
        log "#{addr} ping/error handler: #{status}, #{req}, #{thrown}"
        down(status)

    ok = (data, status, req) ->
        log("#{addr} ping/success handler: #{data}")
        alive()

    options =
        url : addr
        dataType: "jsonp"
        data : {"omg":"omg!"}
        timeout : 6000
        error : err
        success : ok

    $.ajax options

$ ->
    ping "http://217.113.232.224:803/", (->), (->)
    ping "http://corum.blog.pl/", (->), (->)

Musze przyznac, ze lubie te technologie, tzn. jezyk i
biblioteke. Dodajac do nich Underscore.js, ktore zalatwia to,
czego nie da sie zrobic w CS za pomoca for expressions
otrzymujemy srodowisko godne prawdziwego programisty. Bardzo
wspolczuje ludziom, ktorzy jeszcze nie dali rady wejsc w XXI wiek i
wciaz pisza w czystym JS. Nvm.

Kazdy powinien wiedziec, co sie dzieje przy wywolaniu $.ajax
z takimi opcjami – w skrocie: jQuery dodaje na strone element
<script src="options.url?callback=jakas_nazwa"> i czeka, az
funkcja jakas_nazwa zostanie wywolana. W miedzyczasie przegladarka
wykonuje zapytanie GET na podany adres. W tym miejscu (bo podany
adres i port celuja w moja instancje erlanga) zaczyna sie robic
interesujaco – zadanie trzeba przyjac, przetworzyc i na nie
odpowiedziec.

Teraz tak: Erlang, oczywiscie, udostepnia zarowno w swojej
standardowej bibliotece, jak i w dodatkowych modulach, kilka roznych
serwerow http. Nawet wiecej niz kilka – zwyczajnie pisanie serwerow, w
dodatku wydajnych, w Erlangu jest bardzo proste. Z tego tez powodu
zignorowalem wszystkie te mozliwosci – korzystam wylacznie z modulu
gen_tcp z stdliba (no ok, pozyczylem sobie troche kodu obslugujacego
JSON i do parsowania query stringa).

Erlang jest jednak (po co ja to pisze? aha, sa ludzie zbyt
leniwi, zeby spojrzec na wiki…) jezykiem dosc specyficznym, gdzie
praktycznie wszystko opiera sie na przekazywaniu komunikatow miedzy
procesami, wiec jesli spodziewacie sie klasycznego kodu w takim stylu:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# pisze z biegu, nawet nie sprawdzam, czy gdzies nie zrobilem
# literowki, zreszta to tylko przyklad i na pewno wszyscy widzielismy
# taki lub b. podobny kod setki razy

import socket

ADDR = ('localhost', 803)

s = socket.socket()
s.setsockopt(socket.SO_REUSEADDR, 1)
s.bind(ADDR)
s.listen(1)

conn = s.accept() # blokuje

out = ""
i = "ok"

while i
    i = conn.recv(10)
    if i: out += i

conn.send(out[::-1])
conn.close()
s.close()

To generalnie zycze powodzenia :-). Scisle rzecz ujmujac
pisanie w ten sposob jest mozliwe, ale uznawane za bardzo
nieerlangowe. Naturalnym, erlangowym stylem jest tworzenie socketow w
trybie {active, true} (btw: skladnia {a, b, ...} to krotka), ktore
zamiast pasywnie czekac, az ktos na nich wykona recv, same wysylaja
komunikaty do procesu, ktory je wystartowal.

To jest zreszta drobny problem, ktory musialem obejsc – zaraz
o nim powiem, ale najpierw pierwsza wersja glownej petli serwera:

1
2
3
4
5
6
7
#Skrypt testowy
import socket
s = socket.socket()
s.connect(('localhost', 809))
s.send("cokolwiek")
print s.recv(9)
s.close()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
get_options() -> [
        {active, true},
        {reuseaddr, true},
        {packet, raw},
        list
    ].

spawn_server(Port) ->
    Options = get_options(),

    case gen_tcp:listen(Port, Options) of % listen == bind + listen 
        {ok, Sock} ->
            spawn(fun () -> server_loop(Sock) end),
            true;
        Error ->
            log("Blad przy gen_tcp:listen(): ~p.", [Error]),
            false
    end.

server_loop(Listen) ->
    case gen_tcp:accept(Listen) of % blokuje
        {ok, Socket} ->
            log("Otrzymalismy polaczenie ~p.", [w(Socket)]),
            
            % nowy proces zajmie sie dalszym nasluchiwaniem
            spawn(fun () -> server_loop(Listen) end),
            
            % handler zostaje w tym procesie, poniewaz socket jest "active" i
            % bedzie wysylal komunikaty do procesu, ktory wywolal
            % "accept" - czyli obecnego
            read(Socket);
        
        {error, closed} ->
            % nasz nasluchujacy socket wzial i zdechl (prawdopodobnie
            % w wyniku jakiegos bledu w handlerze)
            log("Gracefully exiting...")
    end.


read(Socket) ->
    receive
        {tcp, Socket, Data} ->
            log("Przyszlo: ~p.", [Data]),
            ok = gen_tcp:send(Socket, lists:reverse(Data)),
            read(Socket);
        {tcp_closed, Socket} ->
            log("Klient sie rozlaczyl.")
    end.

% Wyjscie po uruchomieniu skryptu powyzej:
% 1> c(test), test:spawn_server(809).
% true
% <0.43.0> mowi: Otrzymalismy polaczenie "#Port<0.2038>".
% <0.43.0> mowi: Przyszlo: "cokolwiek".
% <0.43.0> mowi: Klient sie rozlaczyl.
%
% Wyjscie z drugiej strony:
% D:> python .pyt.py
% keiwlokoc

Jesli ktos mial jakas stycznosc z programowaniem funkcyjnym,
to natychmiast zauwazy brak petli while – zamiast tego mamy
oczywiste w tym kontekscie tail-recursion (w read).
Skladnia jest raczej jasna – trzeba tylko wiedziec, ze operator = to
nie jest przypisanie, lecz pattern-matching. To, co
zasluguje na uwage to dwa mechanizmy, stanowiace jadro Erlanga -
wbudowana funkcja spawn() oraz konstrukcja receive...after...end.

W duzym skrocie spawn wykonuje podana mu jako parametr
funkcje w nowym procesie i natychmiast zwraca sterowanie do miejsca, w
ktorym zostalo wywolane. receive z kolei czeka, az do obecnego
procesu dotrze jakis komunikat – kiedy to nastapi, wykona sie
pattern-matching w ciele receive, na takiej samej zasadzie, jak
przy case.

Wracajac do problemu wspomnianego powyzej – Socket zwrocony
z gen_tcp:accept() jest aktywny, to znaczy ze bedzie wysylal
komunikaty do procesu, w ktorym zostal utworzony. Dlatego wlasnie
dalsze nasluchiwanie i obsluge kolejnych polaczen przenosimy do
nastepnego, swiezo spawnietego procesu, a read wykonujemy w procesie
obecnym. No dobrze, ale jak w takim razie wylaczyc nasz
serwer?

Moglibysmy w funkcje server_loop wstawic receive (z
timeoutem), ktora sprawdzalaby, czy nie wyslalismy do niej sygnalu w
rodzaju zgin. Problem jest taki, ze o ile za pierwszym polaczeniem
mniej wiecej wiemy jeszcze, jaki jest Pid (identyfikator) procesu -
mozemy go sobie zwrocic ze spawn_server – o tyle kolejne polaczenia
sa juz oblugiwane w procesach, ktorych identyfikatorow nie znamy!
Sorry, zadnego globalnego stanu w Erlangu – nie zapiszemy sobie
nigdzie „obecnego Pid glownej petli” (daloby sie – odpalajac kolejny
proces, do ktorego wysylalibysmy obecny Pid, a ktory loopowalby i
obslugiwal msg get_pid… Ok, moze innym razem).

Musialem troche pogrzebac w dokumentacji, ale w koncu okazalo
sie, ze Sockety da sie przekazac innemy procesowi. W ten sposob mozna
zrealizowac troche bardziej klasyczny schemat: glowna funkcja/petla
nasluchuje na jakims porcie i w momencie polaczenia odpala nowy
proces, ktoremu przekazuje Socket, razem z prawami wlasnosci do niego.

Ale to nie ostatni problem! Gdyby Socket zaczynal jako
aktywny, moglby wyslac jakies komunikaty do glownego procesu zanim
udaloby sie go przekazac procesowi handlera – taki race
condition
to nic dobrego, nawet w tak malutkiej appce. Musialem
wiec nie tylko zmienic wlasciciela, ale tez zmienic stan aktywnosci
socketa – i to juz po przekazaniu go do nowego procesu. Nowy kod to
pokazuje:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
-define(Main, main_listening_loop).

send_main(Msg) ->
    ?Main ! Msg.

get_options() -> [
        {active, false},
        {reuseaddr, true},
        {packet, raw},
        list
    ].


start() ->
    start(803).
start(Port) ->
    main(Port).

stop() ->
    send_main(zgin).


main(Port) ->
    {ok, ListenSocket} = gen_tcp:listen(Port, get_options()),
    Pid = spawn(fun () -> link(ListenSocket),
                          listen_loop(ListenSocket) end),
    register(?Main, Pid),
    log("Server started at port ~p, pid: ~p.", [Port, Pid]).



listen_loop(Listen) ->
    receive
        zgin ->
            log("Shutttting down..."),
            exit(zgon);
        Any ->
            log("~p", [Any]),
            listen_loop(Listen)
    after 0 ->
        case gen_tcp:accept(Listen, 200) of
            {error, timeout} ->
                listen_loop(Listen);

            {ok, Sock} ->
                spawn_handler(Sock),
                listen_loop(Listen);

            Else ->
                log("Inny duzy blad: ~p", [Else]),
                exit(zgon)
        end
    end.

spawn_handler(Sock) ->
    Pid = spawn_link( fun () -> handle_request(Sock) end ),    
    gen_tcp:controlling_process(Sock, Pid),
    inet:setopts(Sock, [{active, true}]),
    log("Spawned and linked ~p.", [Pid]).


handle_request(Sock) ->
    log("Request"),
    case read_request(Sock) of
        invalid -> 
            log("Invalid request"); 
        Data ->
            log("~p", [Data]),
            mm:dispatch(Sock, Data)
    end.


read_request(Socket) ->
    read_request(Socket, []).

read_request(Socket, SoFar) -> % invalid | is_list(Msg)
    receive
        {tcp, Socket, Chunk} ->
            case is_complete(Chunk) of
                false ->
                    read_request(Socket, [Chunk|SoFar]);
                Msg ->
                    string:join(lists:reverse([Msg|SoFar]), "")
            end;
        {tcp_closed, Socket} ->
            invalid
    after 200 ->
        log("Client took too long to send request, aborting."),
        invalid
    end.

Taka architektura odpowiada mi znacznie bardziej, niz
poprzednia: zamiast dodatkowego procesu rejestrujacego obecny Pid
glownej petli, mam tylko jeden Pid, w dodatku zarejestrowany jako
globalny. Jesli chce wyslac jakis komunikat do glownej petli, moge
skorzystac po prostu z send_main(zgin) (powinno chyba byc
send_to_main?). Do tego dodane gdzieniegdzie wywolania link
sprawiaja, ze na przyklad kiedy umrze glowna petla – nasluchujacy
Socket polegnie wraz z nia.

To by bylo wlasciwie wszystko, co jest potrzebne, zeby
zbudowac wielowatkowy, asynchroniczny serwer w Erlangu. Moze nie jest
to malo kodu (a moze to tylko moj styl pisania?), ale wydaje sie byc
przejrzysty, czego nie da sie powiedziec o wielowatkowych czy
wieloprocesowych programach pisanych w innych jezykach. Po tych dwoch
czy trzech godzinach spedzonych przy pisaniu szkieletu moglem juz bez
przeszkod zajac sie logika – w mm:dispatch() – ale o tym innym
razem :-).

  • tysiecy linii kodu, ty wstretny ignorancie!

Spotkalem przed chwila na
pewnym blogu
notke idealnie korespondujaca z moim niedawnym wpisem
o interfejsach i sposobach interakcji miedzy komputerami a ludzmi. Nie
tylko znalazlem w niej (kolejne, zreszta) potwierdzenie opinii, ze to
command line jest najwydajniejszym interfejsem, ale dodatkowo
kilka fantastycznych pomyslow na wycisniecie z konsoli jeszcze lepszej
wydajnosci!

Nie moge sie doczekac, kiedy przeloze pokazane tam skrypty z duetu
Bash/PERL na PowerShell/Python (o ile Python w ogole bedzie
potrzebny!).

Co prawda niezbyt czesto (bo tylko przez ludzi rzeczywiscie
zainteresowanych tematem), ale jednak zdarza mi sie byc pytanym, w
czym funkcyjny styl programowania jest lepszy od imperatywnego, do
czego nadaje sie lepiej, kiedy go wykorzystywac?

Problem z takimi pytaniami jest taki, ze nie ma jednej,
scislej definicji „programowania funkcyjnego” czy tez „funkcyjnego
stylu programowania”. To, co w niektorych jezykach i dla niektorych
ludzi bedzie typowo funkcyjnym rozwiazaniem, dla innych ludzi,
korzystajacych z innych jezykow, bedzie oblesnie imperatywne i
niegodne chocby poprawiania. Jezyki programowania zapewniaja rozne
poziomy wsparcia dla funkcyjnego stylu programowania, ale poza bardzo
nielicznymi wyjatkami (najbardziej znanym – ale nie jedynym – jest
Haskell) wiekszosc nawet „mocno funkcyjnych” jezykow
dopuszcza rowniez imperatywne konstrukty. I tak zreszta pomijam w tym
miejscu jezyki takie, jak OCaml i Scala, ktore
calkiem swiadomie lacza i mieszaja elementy imperatywne, obiektowe i
funkcyjne.

Moim zdaniem zaden z tych stylow nie jest w jakis oczywisty
sposob lepszy od pozostalych; mysle, ze kazdy kompetentny
programista powinien znac i stosowac wszystkie te style czy
paradygmaty, wybierajac pomiedzy nimi na podstawie rozwazan
dotyczacych wydajnosci, czytelnosci i przyszlej pielegnacji danego
kawalka kodu. Krotko mowiac na nas, jako programistach, spoczywa
obowiazek wybrania the right tool for the job – zarowno jezyka
programowania, jak i stylu, jakim sie poslugujemy.

Zeby jednak swiadomie dokonac takiego wyboru musimy wiedziec
nie tylko jakie sa mozliwosci, ale tez dokladnie czym sie roznia i -
faktycznie – jakie rodzaje problemow (zazwyczaj) latwiej sie
rozwiazuje przy pomocy ktorego stylu.

Zacznijmy od bardzo prostego zadania, polegajacego na
przyjeciu stringa i zamianie sasiadujacych ze soba znakow miejscami:
majac na wejsciu "abcde" mamy zwrocic "badce". Najpierw
rozwiazanie imperatywne:

1
2
3
4
5
6
7
8
sin = "abcdef"
sout = ""

for i in range(0, len(sin), 2):
    sout += (sin[i+1] if i+1 < len(sin) else "") + sin[i]
    
print sin
print sout

W piatej linii uzylem ternary operator zamiast
bardziej pythonicznego try: ... except IndexError: ... zeby podkreslic, ze
jest to rozwiazanie mozliwe do przetlumaczenia wprost na dowolny jezyk
imperatywny – moze to byc Pascal, C czy Java, kod wygladalby bardzo
podobnie. Oczywiscie, roznica wynikajaca z immutability
stringow w Pythonie nie zawsze jest trywialna, pokaze wiec rowniez
wersje z mutowalnym stringiem, tak w Pythonie, jak i w C (wersje w C
nalezy kompilowac z -std=c99 pod gcc):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
mstr = bytearray( "abcde", "ascii" )
print mstr

for i in range(0, len(mstr), 2):
    if i+1 < len(mstr):
        tmp = mstr[i]
        mstr[i] = mstr[i+1]
        mstr[i+1] = tmp

print mstr
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <string.h>
#include <stdio.h>

int main(int argc, char *argv[]){
    char str[] = "abcde";
    char tmp;

    printf("%sn", str);
    for(int i = 0; i < strlen(str); i += 2){
        if( i+1 < strlen(str)){
            tmp = str[i];
            str[i] = str[i+1];
            str[i+1] = tmp;
        }
    }
    printf("%sn", str);
}

Jak widac – chocby po uzyciu petli for oraz przypisania -
jest to wersja imperatywna, w dodatku praktycznie identyczna w obu
jezykach (bytearray mozna uznac za odpowiednik char []).
Niewatpliwie rozwiazanie to dziala i dla wycwiczonego w imperatywnych
idiomach oka wynik tego dzialania jest dosc oczywisty. Mysle, ze
glowna cecha pokazanego tutaj imperatywnego stylu jest skupienie sie
na opisie tego jak osiagnac rezultat; gdybysmy jednak nie
programowali w imperatywnych jezykach przez pol zycia, mysle ze nie
moglibysmy na pierwszy rzut oka stwierdzic, co bedzie
wynikiem dzialania tego kodu. Musielibysmy rozwinac, tzn. myslowo
wykonac kazda linijke, zanim moglibysmy stwierdzic jaka bedzie koncowa
postac stringa.

Programowanie funkcyjne mniej akcentuje proces obliczeniowy,
skupiajac sie bardziej na wynikach, jakie chcemy osiagnac. Zobaczmy,
jak wygladaloby funkcyjne przedstawienie tego problemu, najpierw w
Pythonie:

1
2
3
4
5
6
7
8
def swap_pairs( s ):
    if len(s) in (0,1):
        return s
    return s[1] + s[0] + swap_pairs(s[2:])
    
sin = "abcde"
print sin
print swap_pairs(sin)

To rozwiazanie niewatpliwie jest funkcyjne: widzimy tutaj nie
tylko pierwsza do tej pory definicje funkcji, ale rowniez rekurencje i
brak mutowalnego stanu. Rekurencja w tym przypadku nie tylko zastepuje
petle (co dzieje sie dosc czesto), ale rowniez upraszcza nam cialo
funkcji swap_pairs – na pierwszy rzut oka widac, ze otrzymujac
stringa zwracamy jego dwa pierwsze znaki zamienione miejscami – plus
jakas reszte. Zdefiniowalismy tez explicite warunek zakonczenia
rekurencji, podobnie jak we wczesniejszych petlach – roznica jest
jednak taka, ze w petli musielismy znac dlugosc lancucha (co chcialem
podkreslic, nieefektywnie wywolujac len i strlen w kazdym kroku
petli powyzej), tymczasem tutaj dlugosc lancucha nie ma znaczenia!

Jak zwykle jednak z rekurencja bywa, mamy z nia problem -
odpowiednio dlugi lancuch spowoduje przepelnienie stosu (w pythonie:
RuntimeError: max recursion depth exceeded – to jak to jest z ta
dlugoscia? ;-)) i to nawet w jezyku, ktory wspieralby wspominane juz
wczesniej tail-call elimination – zwyczajnie ta wersja
funkcji swap_pairs nie jest przypadkiem rekurencji ogonowej.
Za moment pokaze, jak ja przeksztalcic, na razie jednak chce pokazac
inne, rowniez funkcyjne, podejscie do rozwiazania tego problemu:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from itertools import *
from operator import add
s = "abcde"

print s

# wezmy dwa iteratory po liscie par: [("a", "e"), ("b", "o"),...]
even, odd = tee(izip(s, cycle("eo")))
# zadeklarujmy funkcje, ktora odfiltruje z podanego iteratora pary z
# podanym jako parametr drugim elementem i zwroci liste samych
# pierwszych elementow wlasciwych par
filter_and_extract = lambda type: lambda lst: imap(lambda (a,b):a, ifilter(lambda (a,b):b==type, lst))
even, odd = filter_and_extract("e")(even), filter_and_extract("o")(odd)

# polaczmy obie listy odfiltrowane powyzej w liste par, tylko ze w
# kolejnosci odwrotnej, niz dzielilismy; dostaniemy: [("b", "a"),
# ("d", "c"), ...]
combined = izip_longest(odd, even, fillvalue="")

# najpierw polaczmy elementy wszystkich par w jednego stringa,
# otrzymujac liste stringow [ "ba", "dc", ...] a potem "zsumujmy"
# elementy tej listy
print reduce(add, imap(lambda (a,b): a+b, combined))

Fajne, nie? :-D Ten przyklad – jesli juz o niczym innym -
powinien zaswiadczyc o jednym z niebezpieczenstw programowania w stylu
funkcyjnym: mianowicie o problemach z czytelnoscia, jakie moga sie
pojawic, jesli zechcemy „przegiac” i pisac kod „za madry”. Jest jednak
cos, co przemawia za zastosowaniem tego konkretnego kawalka kodu: jest
to wydajnosc. Nie ma tutaj ani rekurencji, ani nie sa tworzone
posrednie listy (jak za kazdym wywolaniem s[2:] wczesniej), a dalej
nie ma mutowalnego stanu. Niemniej jednak nie wyglada to dobrze i
zapewniam, ze w zadnym jezyku nie wygladaloby znaczaco lepiej; list
comprehensions
moglyby nieco poprawic czytelnosc (ale nie wszedzie
sa) a kilka tzw. funkcji wyzszego rzedu odrobine skrocic ten
kod, ale nie wydaje mi sie, zeby to wiele zmienilo.

Przechodzac dalej: obiecalem pokazac w jaki sposob
przeksztalcic ladna rekurencyjna postac funkcji swap_pairs powyzej
na nieco mniej ladna, ale za to tail-recursive wersje.
Bedziemy do tego potrzebowali jezyka, ktory wspiera tail-call
optimization (inaczej przeksztalcenie nie mialoby sensu). Pomysl
polega na przekazywaniu w kazdym wywolaniu dodatkowego argumentu, w
ktorym…

Aha, zapomnialem. Rekursja ogonowa jest wtedy, kiedy
rekurencyjne wywolanie funkcji jest ostatnim, co jest w danej funkcji
wykonywane. swap_pairs nie bylo tail-recursive, bo po wywolaniu
rekurencyjnym nastepowala jeszcze konkatenacja stringow.

…przekazywana bedzie dotychczas obliczona wartosc. Ta
technika czesto nazywana jest accumulator-passing style i
wyglada tak (w F#):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let swap_pairs (str : string) =
    let char_list = Array.toList <| str.ToCharArray () in (* szczegol techniczny :-) *)
    (* wewnetrzna funkcja, gdzie realizowana jest tail-recursion *)
    let rec _inner acc rest =
        (* przyklad pattern-matchingu: tak naprawde to tylko if/switch *)
        match rest with
        | [] -> acc
        | [x] -> acc @ [x]
        (* to wlasnie tutaj mamy to rekurencyjne wywolanie - zauwazmy, ze jest
         * ono ostatnia rzecza, jaka dzieje sie w funkcji. pierwszy argument to
         * wspomniany "akumulator", czyli dotychczas obliczony wynik, drugi to
         * to, co pozostalo do obliczenia.
         * znak @ oznacza konkatenacje list (not tail-recursive!) *)
        | a :: b :: tail -> _inner (acc @ [b; a]) tail
    in
    (* wywolujac te funkcje musimy pamietac o podaniu poczatkowej wartosci
     * akumulatora, najczesciej jest to jakis "element neutralny" - w tym
     * przypadku pusta lista. *)
    let result = _inner [] char_list in
        (* podobnie jak w pierwszej linii funkcji, tak i na koniec musimy
         * dokonac konwersji listy znakow na napis - szczegol *)
        new System.String (List.toArray result);;

printfn "%A" (swap_pairs "abcde");;

W ten sposob omijamy pulapke prostej rekurencji, ktora moglaby
doprowadzic do przepelnienia stosu dla bardzo dlugich stringow.


No i co – ktory styl jest lepszy? Wydaje mi sie, ze w tym
momencie moje stanowisko jest juz jasne: oba sa rownie dobre, jesli
tylko nie przesadza sie z optymalizacjami czy zaciemnianiem kodu.
Rozwiazanie rekurencyjne traci sporo ze swojej elegancji, kiedy
przeksztalcimy je na rekursje ogonowa (ta nazwa jest beznadziejnie
smieszna :-)), rozwiazanie imperatywne zas (moim zdaniem) nadmiernie
skupia sie na opisaniu sposobu, w jaki ma byc osiagniety dany
rezultat, zamiast po prostu pokazac, jaki ma on byc.

Ja w swoim kodzie chetnie i czesto uzywam typowo funkcyjnych
konstrukcji, ale nie mam rowniez oporow przed stosowaniem
imperatywnych technik (samo to, ze wiem o istnieniu bytearray w
Pythonie moze o tym swiadczyc ;-)). Mam to szczescie, ze moj glowny
jezyk wspiera oba paradygmaty i czesto mam wybor: ale Python nie jest
wyjatkiem, podobne stanowisko zajmuja JavaScript, Ruby czy PERL.

Techniki programowania funkcyjnego warto znac, a ja bede do
nich jeszcze wielokrotnie wracal, co juz zapowiadalem, miedzy innymi
na przykladzie Erlanga oraz juz pokazanych F# i Scheme. Osobna notke
poswiece ksiazce (i przykladom w niej zawartym) pt. Text Processing
in Python
(dostepna tutaj),
co bedzie przynajmniej czesciowa odpowiedzia na pytanie, do
rozwiazywania jakich problemow FP nadaje sie wyjatkowo dobrze. Jakos w
miedzyczasie postaram sie opracowac i postnac bibliografie, z ktorej
sam sie uczylem; juz od jakiegos czasu sie nosze z takim zamiarem,
mysle ze to moze byc pomocne dla innych.

Aha – nastepna notka moze sie troche opoznic, bo chyba jednak
dam szanse wspomnianemu nizej zadaniu z RosettaCode! :-)

RosettaCode to jedna z moich
ulubionych stron zwiazanych z programowaniem. Bardzo podoba mi sie
idea, na jakiej jest oparta: prezentuje rozwiazania roznych problemow
w wielu jezykach naraz. Mnie akurat wlasnie w ten sposob najszybciej
idzie poznawanie nowych jezykow – poprzez odniesienie do jezykow,
ktore juz znam, porownanie z nimi i wylapanie roznic. Mowiac szczerze
duzo czesciej roznic miedzy jezykami jest mniej, niz podobienstw, wiec
i latwiej jest je zapamietac.

Kilka dni temu pojawilo sie nowe zadanie – dosc ciekawe i nieco
skomplikowane – na ktore wlasnie trafilem. Chce je tutaj zareklamowac,
po pierwsze dlatego, ze nie ma jeszcze zadnego rozwiazania, wiec nie
ma skad sciagac, a po drugie dlatego, ze wprawidlowo wykonane mogloby
sie przysluzyc rozwojowi Rosetty. Opis zadania znajduje sie tutaj
- to wciaz jeszcze draft, ale ogolne zalozenia juz sie raczej
nie zmienia. Zachecam do prob rozwiazania tego problemu, w dowolnym
jezyku!

Ech, jest jedna rzecz, ktora mnie w RC drazni, tak przy okazji
podziele sie tym zalem: wiekszosc kawalkow kodu tam nie jest prawie w
ogole obkomentowanych. Sam jestem winien, bo moich rozwiazan nie
dokumentuje: problem jest taki, ze piszac w jezyku, ktory sie zna,
rozwiazania wiekszosci problemow sa banalnie proste, ale kiedy spojrzy
sie na rozwiazanie w J, APL czy nawet stosunkowo
czytelnym FORTH… Coz, wtedy potrzeba dokumentowania
przykladow wydaje sie znacznie bardziej palaca. Oczywiscie,
rozwiazujac kolejne zadanie juz sie o tym nie pamieta ;-)

W kazdym razie: po pierwsze zachecam do odwiedzania RC kazdego, kto
chce byc lepszym programista; po drugie zachecam do rozwiazania tego
konkretnego zadania kazdego, kto czuje sie na silach i ma troche
czasu!

Zgodnie z obietnica, koncze dzisiaj notke o kontynuacjach
przykladami implementacji CPS w JavaScript.

Continuation Passing Style (CPS)
is a style of programming in
which control is passed explicitly in the form of a continuation. A
function written in continuation-passing style takes as an extra
argument an explicit „continuation” i.e. a function of one argument.
When the CPS function has computed its result value, it „returns” it
by calling the continuation function with this value as the argument.

Ta definicja jest akurat calkiem znosna, przytaczam ja wiec w
calosci. Slowo wyjasnienia: polskojezyczna wiki zwyczajnie nie posiada
tych hasel, a samemu wole nie tlumaczyc definicji. Jesli ktos poda mi
sensowne tlumaczenie tej i poprzednich definicji, to chetnie je
podmienie.

Przypomnijmy z poprzedniej notki: mamy jakies zadanie, ktore
wykonuje sie bardzo dlugo, blokujac inne zadania. Przykladem funkcji
reprezentujacej takie zadanie moze byc taki kod:

1
2
3
4
5
6
7
display_upper = (lst, display) ->
    for i in lst
        try
            display i.toUpperCase()
        catch error
            display null
    true

Jesli zalozymy, ze przekazywana lista lst jest bardzo,
bardzo dluga i w dodatku kazdy jej element jest olbrzymim blobem
tekstu – ta funkcja ma szanse zamrozic nam wykonywanie innych
skryptow, a czasem nawet sama przegladarke.

Co tu moze byc ciekawe – sposob wyswietlenia kazdego elementu
listy nie jest wazny dla samego przejscia przez liste i transformacji
jej elementow, dlatego funkcja display_upper przyjmuje kolejna
funkcje, display, ktora zajmie sie wlasciwym wyswietlaniem. Moglaby
ona byc cieniutkim wrapperem wokol console.log, albo moglaby pisac
do jakiegos diva, albo… whatever.

Istotne jest jednak to, ze dopoki glowna petla sie nie
zakonczy nie wykona sie zaden inny kod. To znany problem, najlepiej go
zilustrowac na przyklad takim kodem:

1
2
3
4
5
6
7
8
ping = () ->
    alert "pong"

for i in [0,1,2,3,4,5,6,7,8,9]
    if i == 0
        setTimeout ping, 0
    else
        alert i

Gdzie ping wykona sie dopiero po wyswietleniu ostatniego
alert i (w kazdym razie w Operze: w FF jakims cudem ping wykonuje
sie miedzy i == 1 a i == 2 – nie zamierzam w to wnikac :-)). Jak
mozna sobie z tym poradzic? Trzeba dlugie obliczenia podzielic na
mniejsze kawalki, oczywiscie. Tylko jak?

Po pierwsze: imperatywne for niespecjalnie pasuje do
funkcyjnego stylu, a zwlaszcza do CPS, zatem pierwszym krokiem powinno
byc przerobienie funkcji display_upper na rownowazna jej wersje
rekurencyjna. To akurat dosyc proste:

1
2
3
4
display_upper = (lst, display) ->
    display lst.shift()
    if lst.length > 0
        display_upper lst, display

To nie jest rozwiazanie – to tylko forma przejsciowa. Gdybysmy
rzeczywiscie mieli bardzo dluga liste na wejsciu, ta wersja
display_upper prawdopodobnie wywalilaby sie ze wzgledu na brak tzw.
tail-call optimization[1] i w
konsekwencji przepelnienia stosu.

Z definicji na samej gorze wiemy, ze w CPS kazda funkcja
przyjmuje dodatkowy argument, w naszym przykladzie oznacza to funkcje
display, ktora w koncu musze pokazac. Na razie nie bedzie to pelna
transformacja na CPS – na przyklad display_upper nie bedzie
przyjmowac kontynuacji, a kontynuacja przekazana do display nie
bedzie przyjmowac argumentu – wrocimy jeszcze do tego. Tymczasem
zdefiniujmy wreszcie display:

1
2
3
4
display = (el, kontynuacja) ->
    alert el      # robimy cos z argumentem
    kontynuacja() # zamiast zwrocic sterowanie, przekazujemy je
                  # do kontynuacji

Ok, w tym momencie jedyna watpliwosc, jaka powinna sie ostac
dotyczy funkcji (widzimy, ze to jest funkcja po sposobie wywolania)
kontynuacja. Skad sie bierze i czym jest? Stanie sie to jasne, kiedy
przeksztalcimy odpowiednio display_upper:

1
2
3
4
5
6
7
8
display_upper = (lst, display) ->
    first = lst.shift()

    kontynuacja = () ->
        if lst.length > 0
            display_upper lst, display
    
    display first, kontynuacja

Jak widac zmiana w porownaniu z poprzednia wersja nie jest
wielka – jedyne, co robimy inaczej, to zamiast wywolac display_upper
rekurencyjnie z niej samej, zamykamy to wywolanie w domknieciu i
przekazujemy do display. Kontynuacja jest zatem rekurencyjne
wywolanie display_upper – i juz. Niespecjalnie skomplikowane,
prawda?

W dlaszym ciagu grozi nam jednak przepelnienie stosu przy
bardzo dlugich listach. Poza tym nie zrobilismy jeszcze nic w kwestii
blokowania innych zadan… Na szczescie w tym momencie rozwiazanie obu
tych problemow jest juz banalnie proste – caly kod bedzie wygladal
tak:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
display = (el, kontynuacja) ->
    alert el     
    setTimeout kontynuacja, 20 # przez te 20 ms beda dzialac inne
                               # zadania

display_upper = (lst, display) ->
    first = lst.shift()

    kontynuacja = () ->
        if lst.length > 0
            display_upper lst, display
    
    display first, kontynuacja

$ ->
    display_upper ["raz", "dwa", "hyc, hyc, hyc!"], display

Ten przyklad jest bardzo prosty, ale – mam nadzieje -
ilustruje zasade dzialania CPS. W nieco bardziej skomplikowanej wersji
mozna CPS wykorzystywac do uproszczenia obslugi zapytan AJAXowych i
generalnie wszystkiego, co dzieje sie asynchronicznie, wymaga
callbackow lub jakiegos rodzaju sekwencyjnego laczenia ze soba pewnych
operacji – kontynuacja moglaby przyjmowac argument – wynik dzialania
funkcji, ktora „kontynuuje” – a nawet dodatkowa kontynuacje.
Pamietajmy tez, ze kontynuacja moze zostac gdzies zapisana,
obliczenie przerwane – i wznowione w dowolnym momencie pozniej.

Nie jest to pelna sila first-class continuations
pokazanych w poprzedniej notce, ale w wiekszosci zastosowan daje
podobne rezultaty, co jest sporym osiagnieciem jak na tak uposledzony
jezyk, jak JavaScript ;-) Aha, oczywiscie pisalem w CoffeeScripcie -
skladnia JS mi sie nie podoba, jest jak dla mnie zbyt rozwlekla, ale
CS kompiluje sie bezposrednio do JSa i nawet jesli ktos ma problemy
(gdzie, czemu?!) z przelozeniem sobie kodu na klasyczny JavaScript, to moze
go wkleic w okienko na
stronie autora jezyka
.

Pokazalbym jeszcze kilka przykladow – juz troche bardziej
zaawansowanych – wykorzystania CPS, ale niestety nie mam juz czasu;
moze w przyszlosci wroce do tego zagadnienia. Mam w planach opisanie
monad, wiec przy okazji powinna sie pojawic
ContinuationMonad, ale to juz bedzie prawdopodobnie Python i
OCaml, nie JavaScript. Zobaczymy jeszcze – tymczasem milej zabawy z
kontynuacjami! :-)

  • lub inaczej „tail-call
    elimination”
    ; rzecz dosc czesto spotykana w jezykach funkcyjnych,
    niestety niezbyt popularna w jezykach imperatywnych

  • RSS

Polecamy

Nie masz jeszcze bloga? Załóż bloga

Polecamy