W ostatnich latach rozproszone systemy kontroli wersji (w skrócie DVCS) stały się popularne, zwłaszcza w środowiskach związanych z open source. Warto je jednak znać nie tylko wtedy, gdy pracujemy nad projektami z otwartym źródłem. Jak bowiem pisałem wcześniej, mogą być one przydatne chociażby do małych jednoosobowych projektów. Ponadto bywają nierzadko używane w większych oraz mniejszych firmach. Ogólnie mamy więc duże szanse, aby prędzej lub później zetknąć się z takimi systemami.
Ten post jest adresowany przede wszystkim do osób, które nie miały wcześniej dłuższej styczności rozproszonymi systemami kontroli wersji. Ma on na celu wyjaśnienie wielu spośród tych tajemniczo brzmiących słówek, na jakie nieustannie można się natknąć, gdy pracujemy z DVCS-ami. Istnieje oczywiście szansa, że bardziej zaawansowani użytkownicy również wyciągną nowe wiadomości z lektury (albo chociaż przejrzenia) niniejszej notki. A jeśli nawet nie, to powtórzenie znanej sobie wiedzy na pewno im nie zaszkodzi ;)
Wiadomo powszechnie, że Java listenerami stoi i typowe jest używanie jest różnego rodzaju klas wewnętrznych, które są następnie podawane jako interfejsy do wywołań zwrotnych. W ten sposób obsługuje się różnego rodzaju zdarzenia, począwszy od interakcji użytkownika z programem aż po ważne notyfikacje pochodzące z systemu operacyjnego.
Gdy klasa zawierające takie handlery dostatecznie się rozrośnie, pojawia się oczywiście potrzeba jej zrefaktorowania i podzielenia na dwie lub większą liczbę mniejszych. W trakcie tego procesu czasami chciałoby się też wydzielić owe klasy wewnętrzne, obsługujące zdarzenia – i wtedy możemy napotkać pewien kłopot. Kłopocik właściwie ;)
Dotyczy on zależności między klasami wewnętrznymi a klasą je otaczającą. Ponieważ mówimy o niestatycznych niestatycznych klasach wewnętrznych, typowe jest odwoływanie się do składników klasy otaczającej z klasy zewnętrznej. Mówiąc bardziej po ludzku, implementacja np. zdarzenia kliknięcia przycisku może sięgać do obiektu okna/dialogu/itp., zawierającego tenże przycisk:
Przeniesienie całej powyższej klasy w inne miejsce nastręcza wówczas problem, gdyż odwołuje się ona do metody klasy ją otaczającej (czyli finish()
). Należałoby więc posiadać obiekt tej klasy, zapewnić aby rzeczona metoda była dostępna (co nie zawsze jest wskazane), aby wywoływać ją z właściwymi parametrami – które też trzeba jakoś przekazać – i tak dalej… Krótko mówiąc na naszej drodze do ładnie zorganizowanego kodu naraz pojawia się sporo przeszkód.
Czy nie dałoby się po prostu jakoś przekazać tego “kawałka kodu”, tych kilku czy kilkunastu instrukcji opakowanych w coś, co można podać w inne miejsce programu?… Okazuje się, że jak najbardziej, a rozwiązaniem na problem refaktorowanych klas wewnętrznych jest… więcej klas wewnętrznych ;-) Nic nie stoi bowiem na przeszkodzie, aby rzeczony fragment procedury zdarzeniowej zapakować w metodę klasy implementującej interfejs Runnable
. Tak, dokładnie ten interfejs który zazwyczaj kojarzy się z wątkami i klasą Thread
. W rzeczywistości reprezentuje on “cokolwiek co da się uruchomić”, więc jak ulał pasuje w charakterze rozwiązania problemu.
Aplikując je do powyższego przykładu, możemy wydzielić powyższy listener (potencjalnie wraz z kilkunastoma podobnymi) i kazać mu jedynie wywoływać metodę run
jakiegoś obiektu Runnable
. W niej zaś umieszczamy nasz pierwotny kod i przekazujemy do nowo wydzielonej klasy:
Znawcy tematu powiedzieliby, że w ten sposób utworzyliśmy domknięcie (closure), bo w inne miejsce programu przekazaliśmy fragment kodu wraz z jego kontekstem; w tym przypadku jest to chociażby referencja this
na rzecz której wywoływany jest finish
. Fani programowania funkcyjnego stwierdzą z kolei, że ta technika jest kiepską imitacją wyrażeń lambda i najprawdopodobniej też będą mieli rację.
Nie zmienia to jednak faktu, że jeśli programujemy w starej (nie)dobrej Javie, to technika ta może być po prostu użyteczna – niezależnie od tego, jaki paradygmat za nią stoi. Dlatego też chciałem dzisiaj się nią podzielić.
Dzisiejszy okrągły, 512-ty post to doskonała okazja, żeby pochwalić się nową produkcją… No, może nie do końca nową, ale z pewnością wartą zauważenia :) Chodzi o Taphoo – czyli moją całkiem udaną grę sprzed kilku lat – w wersji na platformę mobilną Android. Styl grafiki został przy okazji trochę zmieniony, a sterowanie jest oczywiście dostosowane do (znacząco innego) interfejsu urządzeń. Z opinii wczesnych testerów wynika na szczęście, że po tym przeniesieniu na mniejsze (ale za to dotykowe i przenośne) ekrany gra wciąż prezentuje się przynajmniej tak samo dobrze ;)
Jeśli więc mamy szczęście posiadać telefon, tablet, netbook, lodówkę lub suszarkę z Androidem, to zachęcam do przyjrzenia się tej produkcji (*). Zwłaszcza że jest ona – podobnie jak wersja pod Windows – całkowicie freeware :)
(*) W przypadku suszarek i lodówek mogą wystąpić problemy z wyświetlaniem grafiki i sterowaniem.
Za sprawą najnowszego komiksu z xkcd – a właściwie tekstu, który pojawia się po najechaniu myszką na obrazek – w Internecie robi karierę mem na temat rzekomej “zbieżności” artykułów Wikipedii do hasła ‘filozofia’. Zbieżność ta była zdefiniowana przez kolejne kliknięcia w pierwszy (z paroma wyjątkami) link w każdym artykule i kolejne powtarzanie tego procesu. W końcu powinniśmy trafić na wspomniany tekst o filozofii.
Zabawną cechą tego memu jest fakt, iż stosuje się do niego swego rodzaju zasada nieoznaczoności: samym swoim istnieniem (i popularnością) oddziałuje on na zjawisko, które zdaje się opisywać. W końcu mówimy tutaj o serwisie, którego zawartość edytują tysiące użytkowników, a dzięki całej tej plotce ich uwaga jest tym bardziej skupiona na tekstach leżących na ścieżce do tej nieszczęsnej ‘filozofii’… jeśli takowa faktycznie istnieje.
A czy istnieje? No właśnie :) Traktując to jako swego rodzaju pouczającą zabawę, postanowiłem zbadać sprawę i wysmażyłem odpowiedni program, który potrafi automatycznie wykonywać procedurę opisaną na początku, tj. kolejno przechodzić przez teksty w Wikipedii i “klikać” w pierwsze linki z ich treści. Szybko zorientowałem się wtedy, że twierdzenie o zbieżności do Philosophy jest tylko wycinkiem większej, bardziej interesującej całości.
Okazało się bowiem, że Wikipedia ma coś w rodzaju uniwersalnego atraktora, którego ‘filozofia’ jest częścią. Ma on postać cyklu zmieniającego się w czasie (z powodu edycji, oczywiście) i obejmuje interesujący zestaw pojęć, które semantycznie są połączone zależnością rekurencyjną. Obserwując kształt owego cyklu w dłuższym przedziale czasowym, można by zapewne wyodrębnić terminy zupełnie podstawowe, które są w nim obecne stale lub wypadają tylko na krótką chwilę. Kto wie, może w ten sposób udało by się dokonać jakiegoś fundamentalnego odkrycia z dziedziny filozofii, na przykład odpowiedzieć na pytanie o życie, Wszechświat i całą resztę? ;-)
Nieco mniej ambitnym wyzwaniem jest zbadanie, jak daleko od atraktora znajdują się pewne hasła. Innymi słowy, ile kliknięć dzieli nas od wpadnięcia w cykl zapętlających się pojęć, jeśli zaczynamy od danego artykułu? Jeśli zastanowimy się nad tym chwilę, to można dojść do wniosku, że odpowiedź byłaby miarą odwrotności “stopnia abstrakcji” danego pojęcia. Skoro bowiem w atraktorze znajdują się terminy bardzo ogólne, oddalanie się od niego powinno nas przybliżać do konkretów. Czy tak jest w rzeczywistości?…
Wygląda na to, że faktycznie coś w tym jest. Poniżej przedstawiam tendencyjne wyniki sprawdzenia pewnej liczby bynajmniej wcale nie losowo wybranych haseł. i obliczenia odległości każdej z nich od Czarnej Dziury Abstrakcji:
blog | 14 | Internet | 7 | programming | 8 |
compiler | 6 | Turing machine | 5 | Angry Birds | 18 |
Sun | 4 | velociraptor | 2 | Boeing 747 | 22 |
South Park | 28 | Darth Vader | 8 | Julius Caesar | 34 |
Nie jestem wprawdzie do końca pewien, co oznacza to, że Angry Birds są niemal dwa razy bardziej abstrakcyjne od Juliusza Cezara, ale na pewno musi to coś znaczyć ;]
Ktokolwiek, kto programował dłużej w Windows API zna bardzo dobrze klasyczną sekwencję instrukcji, składającą się na zarejestrowanie nowej klasy okna i jego utworzenie. Jej częścią jest między innymi wywołanie funkcji CreateWindow
lub CreateWindowEx
, które przyjmują odpowiednio 11 lub 12 parametrów. Mimo że nie są one rekordzistkami pod tym względem (bije je chociażby CreateFont
z 14 argumentami), to i tak mogą się “poszczycić” dużym potencjałem w zaciemniania kodu i czynienia go trudnym w zrozumieniu lub modyfikacji.
Niestety, takie lub nieco mniej drastyczne przypadki można spotkać w wielu językach, platformach i technologiach. Odchodzą one daleko od rozsądnego zalecenia, aby liczba parametrów funkcji nie przekraczała dwóch lub trzech, z ewentualnym uwzględnieniem this
/self
/Me
. Jak sobie z nimi radzić, aby wynikowy kod zawierający tak rozrośnięte wywołania był jeszcze w jakikolwiek sposób czytelny?…
Otóż należy postarać się, aby każdy z wielu argumentów był identyfikowalny czymś więcej niż tylko pozycją w ciągu oddzielonym przecinkami. Dobrze tutaj sprawdza się feature niektórych języków programowania zwany argumentami słownikowymi. Umożliwia on “przypisywanie” w wywołaniu wartości parametrów do ich nazw. Pozwala to na zmianę ich kolejności, ale przede wszystkim dodaje czytelną etykietę dla każdego argumentu. Przykład takiego słownikowego wywołania w Pythonie widać poniżej:
Teoretycznie podobny efekt można osiągnąć także w językach nieposiadających wspomnianej opcji. Podejrzewam zresztą, że sposób ten jest pierwszym, jaki większości przyszedł do głowy. Chodzi tu o zwyczajne opatrzenie każdego argumentu odpowiednim komentarzem. Wiele przykładów tak właśnie traktuje argumenty wspomnianej funkcji CreateWindow
(Ex
):
Ale rzeczywisty kod to nie przykład z tutoriala, a nadmiar kolorowych komentarzy niekoniecznie musi dobrze wpływać na przejrzystość całej instrukcji. W dodatku wciąż jesteśmy skazani na domyślną kolejność parametrów, a wszelkie rozbieżności między argumentami a ich opisem (bardzo mylące!) nie są wykrywane przez kompilator…
Co można zatem zrobić? Odpowiedź jest prosta: należy napisać kod, który sam się dokumentuje ;-) A rozwijając tę myśl do czegoś bardziej konkretnego: powinniśmy zauważyć, że absolutnie każdy język posiada możliwość opisywania nie tylko parametrów funkcji, ale ogóle jakichkolwiek wyrażeń. Nazywa się to… dokładnie tak – deklaracją zmiennych:
Przy takim rozwiązaniu niepotrzebne są już żadne dodatkowe wyjaśnienia, bo wszystko widać tu doskonale. Wywołanie stało się czytelne, bo każdy z parametrów jest po prostu swoją nazwą lub nieistotnym NULL
-em. Warto też zauważyć, że w typowym kodzie wiele z tych nazw byłoby już zdefiniowanych wcześniej, bo np. byłyby argumentami funkcji otaczającej to wszystko. Ilość dodatkowych deklaracji niekoniecznie musiałaby więc być zbliżona do długości listy parametrów wywołania.
Powyżej widać zatem, że nawet z wyjątkowo rozrośniętymi funkcjami można sobie całkiem nieźle poradzić. Nie traktujmy tego jednak jako zachęty do wydłużania list argumentów naszych własnych funkcji. Zdecydowanie lepiej jest użyć struktury (jak to robi się np. przy tworzeniu urządzenia DirevtX) czy nawet wzorca Builder bez jego abstrakcyjnej części.
Napotykając problem, niektórzy ludzie myślą: “Użyję wyrażeń regularnych!”
W rezultacie mają dwa problemy.Jamie Zawinski @ alt.religion.emacs
Ten słynny cytat jest, według mnie, lekkim niedoszacowaniem. Decydując się na skorzystanie z wyrażeń regularnych, z miejsca dostajemy bowiem dwa problemy z nimi samymi :) Pierwszych z nich jest sama składnia, która dla nieprzyzwyczajonego oka jest cokolwiek nietrywialna. To głównie ona jest wskazywana jako główna trudność w sprawnym i efektywnym używaniu regexów.
Dzisiaj jednak chciałem zwrócić uwagę na ten drugi, rzadziej zauważany problem. Otóż samo wyrażenie to nie wszystko, trzeba je jeszcze odpowiednio użyć w naszym kodzie. I tutaj mogą zacząć się schody, bo w różnych językach programowania sprawa ta wygląda często odmiennie. Na szczęście jest da się tu też wskazać podobieństwa i spróbować dokonać uogólnienia.
Podstawowym elementem interfejsu programistycznego do wyrażeń regularnych jest zwykle obiekt wzorca (pattern), czyli samego wyrażenia. Zawiera on jego postać skompilowaną, którym jest mniej lub bardziej skomplikowana (w zależności od składni) konstrukcja przypominająca automat stanów. Zbudowanie tej wewnętrznej reprezentacji jest konieczne, aby przeprowadzić jakąkolwiek operację (np. wyszukiwania czy dopasowania). Jeśli więc planujemy skorzystać z jednego wyrażenia w celu przetworzenia większej liczby tekstów, dobrze jest posługiwać się gotowym, skompilowanym obiektem.
Ten ogólny opis dobrze przenosi się na rzeczywiste języki programowania, w których możemy znaleźć takie klasy jak:
boost::regex
/wregex
w C++ z biblioteką Boost.RegexSystem.Text.RegularExpressions.Regex
w C#/.NETjava.util.regex.Pattern
w Javiere.RegexObject
w PythonieTekstową postać wyrażeń regularnych podajemy zwykle do konstruktorów wyżej wymienionych klas, względnie używamy jakichś statycznych lub globalnych funkcji z odpowiednich pakietów. Przy okazji warto też wspomnieć o problemie escape‘owania znaków specjalnych w wyrażeniach, który w mocno niepożądany sposób interferuje z analogicznym mechanizmem w samych językach programowania. Ponieważ w obu przypadkach używa się do tego znaku backslash (\), w wyrażeniach wpisywanych do kodu należy go podwoić:
W C# i Pythonie można tego uniknąć, stosując mechanizm surowych napisów (raw strings). Programiści C++ i Javy nie mają niestety tego szczęścia ;)
Gdy mamy już obiekt skompilowanego wyrażenia, możemy użyć go do jakichś pożytecznych celów. Jeśli są one proste – jak choćby sprawdzenie, czy jakiś ciąg ma formę określoną regeksem – to możemy zazwyczaj obejść się jednym prostym wywołaniem:
Bardziej skomplikowane jest wyszukiwanie wszystkich dopasowań wyrażenia w danym tekście, zwłaszcza jeśli przy okazji chcemy dobrać się do fragmentów znalezionych podciągów. Tutaj zaczynają objawiać się pewne różnice między poszczególnymi językami, ale ogólny schemat pozostaje ten sam. Opiera się on na skonstruowaniu odpowiedniej pętli przelatującej po kolejnych dopasowaniach i operowaniu na obiekcie, który takie dopasowanie (match) reprezentuje:
boost::match_results
w C++ z Boost.RegexSystem.Text.RegularExpressions.Match
w C#/.NETjava.util.regex.Matcher
w Javie (klasa ta kontroluje też iterację po kolejnych dopasowaniach)re.MatchObject
w PythonieObiekt dopasowania udostępnia zazwyczaj kilka przydatnych metod i właściwości, jak choćby zakres indeksów znalezionego ciągu. Są też tam fragmenty, które “wpadły” w podgrupy strukturalne (subsequences, subgroups, capture groups, itp.), na które nasze wyrażenie było podzielone. Chodzi tu o jego części ujęte w nawiasy okrągłe; to, jakie konkretne znaki zostały dopasowane do każdego z nich zostaje bowiem zapamiętane w obiekcie match.
Między innymi dzięki temu faktowi możliwe jest określanie bardzo ogólnych wzorców do wyszukania w tekście, a następnie przeglądanie tego, co udało nam się znaleźć i podejmowanie decyzji na podstawie jakichś znaczących elementów dopasowania. W ten sposób możemy przetwarzać teksty o stopniu skomplikowania znacznie przekraczającym to, co w teorii daje się opisać wyrażeniami regularnymi. Żeby nie pozostać gołosłownym, zaprezentuję na przykład prosty sposób na konwersję tekstu zawierającego często spotykane na forach znaczniki BBCode (takie jak [url]
czy [img]
) na jego odpowiednik HTML-owy, gotowy do wyświetlenia.
Najważniejsza jego część to wykonywane w funkcji _bbtag_to_html
przetwarzanie obiektu typu re.MatchObject
zawierającego dane o znalezionym, pojedynczym tagu. Pobieramy tam jego nazwę i zawartość, które zostały dopasowane jako odpowiednio: pierwsza i druga podgrupa wyrażenia. Samo przeglądanie tekstu w poszukiwaniu tagów i ich zastępowanie jest wykonywane wbudowaną funkcją re.RegexObject.sub
, która ukrywa szczegóły wspomnianej wcześniej pętli.
Mam nadzieję, że powyższy przykład dowodzi, że możliwe jest zastosowanie wyrażeń regularnych bez znaczącego wzrostu liczby problemów do rozwiązania :) Jakkolwiek dziwnie to zabrzmi, korzystanie z regeksów może bowiem niekiedy przyczynić się do wzrostu czytelności wynikowego kodu, przynajmniej dla bardziej doświadczonych programistów. Jest tak ze względu na duże podobieństwa nie tylko między różnymi wariantami składni wyrażeń, ale też między bibliotekami do ich obsługi w różnych językach programowania, które to dzisiaj starałem się przedstawić.
Przeglądając jakiś rzeczywisty kod w języku Python można często natknąć się na nietypowe wykorzystanie operatora nawiasów kwadratowych. Tradycyjnie znaki te służą do indeksowania tablic, co w językach kompilowanych bezpośrednio do kodu maszynowego równa się prostej operacji na wskaźnikach:
Ponieważ jednak Python nie jest takim językiem, jego twórcy pozwolili sobie na to, by zawarte w nim kilogramy warstw abstrakcji oferowały dodatkową funkcjonalność również przy tak trywialnym zagadnieniu. W rezultacie indeksowanie tablic (a właściwie list, w tym i łańcuchów znaków) jest tu operacją, która często ukrywa w sobie znacznie bardziej skomplikowaną logikę niż to widać na pierwszy rzut oka.
Zacznijmy od tego, że w dopuszczalnymi indeksami są nie tylko dodatnie, ale i ujemne liczby całkowite. Oznaczają one dostęp do końcowych elementów tablicy: -1
do pierwszego od końca, -2
do drugiego, i tak dalej. Być może nie wydaje się logiczne to, że elementy tab[0]
i tab[-1]
są na przeciwnych krańcach listy podczas gdy ich indeksy różnią zaledwie o jeden. Uzasadnieniem jest tu odniesienie do indeksowania od końca w innych językach, czyli tab[tab.length() - i]
. W Pythonie po prostu pomija się jawne zapisanie odwołania do długości tablicy.
Znacznie bardziej interesującym aspektem indeksowania jest użycie dwukropka (:
). W zasadzie to zamienia on wówczas całą operację na “krojenie” (slice) tablicy, bo pozwala on na na wybór nie jednego elementu, a całego przedziału. Dokładniej mówiąc tab[i:j]
oznacza fragment tablicy wyznaczony półotwartym zakresem indeksów . Kawałek ten zawiera więc
tab[i]
, ale pomija tab[j]
; jest to analogiczne chociażby do iteratorów begin()
i end()
w kontenerach STL.
To właśnie slicing jest tą nietypową operacją, która dla niewprawnego oka wygląda cokolwiek zagadkowo. Jest tak zwłaszcza wtedy, gdy wykorzystuje ona możliwość pominięcia jednego z krańców przedziału, który to jest wówczas “dociągany” do odpowiedniego krańca całej listy.
Łącząc wszystkie te zawiłości możemy już rozszyfrować większość często występujących przypadków użycia indeksowania w Pythonie:
Dwa ostatnie przykłady pokazują też, że tego rodzaju operacje są bardzo przydatne podczas przetwarzania łańcuchów znaków, które to “przypadkiem” są również swego rodzaju tablicami.