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.
Wczoraj miałem okazję wziąć udział w imprezie Google I/O Extended 2011. W dużym skrócie polegała ona wspólnym oglądaniu (i późniejszej dyskusji) live streamu z konferencji Google I/O w San Fransisco, na której tytułowa firma prezentuje nowe rozwiązania technologiczne, mające pojawić się w powszechniejszym użyciu w najbliższych miesiącach. Nie będę specjalnie rozwodził się na temat treści tych prezentacji, bo można się do nich z łatwością dostać, czytając niusy z dowolnego serwisu technologicznego. Wspomnę tylko o ciekawostkach, które szczególnie zwróciły moją uwagę, a są to:
Całość imprezy Google I/O Extended 2011 była organizowana przez Google Technology User Group (GTUG) i było to pierwsze poważne wydarzenie pod auspicjami tej grupy. Skorzystam w tym miejscu z okazji i pozwolę sobie na zachęcenie wszystkich mieszkających w jednym z trzech miast z GTUG-iem (Warszawa, Kraków, Poznań) do zainteresowania się działaniami grup i eventami, które będą przez nie organizowane. Zawsze można się czegoś ciekawego dowiedzieć, a może też wyjść z jakimiś fajnymi gadżetami ;-)
Wśród niezbędnych gadżetów programistycznych często wymieniany jest kubek kawy. Trzeba jednak przyznać, że zazwyczaj chodzi tutaj o jego zawartość: szybko wypijaną i pospiesznie uzupełnianą. Nie twierdzę oczywiście, że pusty kubek nosi jakiekolwiek znamiona przydatności podczas kodowania. Stanowi on jednak przykład na to, że na biurku programisty typowo znajdzie się zawsze coś więcej niż tylko klawiatura, myszka i ekran.
Może być tam chociażby mała, kauczukowa piłeczka. Model standardowy jest zwykle w jednolitym kolorze, ma średnicę około sześciu centymetrów i oznaczony jest mniej lub bardziej znajomym logo, wskazującym na jego pochodzenie. To oczywiście nie jest przypadek, ponieważ najprostszym sposobem na wejście w posiadanie tego wyrobu gumowego jest wzięcie udziału w spotkaniu, wykładzie, konferencji czy innego rodzaju evencie, gdzie takie fanty rozdawanego są darmo.
Po co jednak koder miałby trzymać pod ręką coś takiego? Otóż dlatego, że – jak zdołałem stwierdzić – piłeczka taka ma całe mnóstwo zastosowań bezpośrednio związanych z programowaniem. Mówiąc bardziej zrozumiałym językiem, jej funkcjonalność jest niezwykle bogata, gdyż obsługuje ona szeroki wachlarz różnych przypadków użycia. Oto niektóre z nich:
Widzimy zatem, że kauczukowa piłeczka posiada niezaprzeczalne zalety i jest wybitnie użytecznym narzędziem programistycznym. Jeśli więc będziemy mieli okazję wejścia w jego posiadanie, zalecam skorzystanie z niej – zwłaszcza, że rzecz jest bardzo często rozprowadzana jako freeware :)
Będąc w zgodzie z podzielanym przez siebie poglądem o kluczowej a często niedocenianej roli “małych” algorytmów, dzisiaj wezmę pod lupę funkcję do łączenia napisów, znaną większości jako join
. Ta przydatna operacja występuje w wielu językach i bibliotekach, a jej brak w pozostałych jest zwykle wyraźnie odczuwalny (tak, Java, o tobie mówię). Dobrze użyty join
– zwłaszcza w połączeniu z pewnymi specyficznymi mechanizmami językowi – potrafi zapobiec pisaniu niepotrzebnych pętli, znacząco redukując code bloat.
Ale po kolei. join
to operacja polegająca na złączeniu kolekcji napisów w jeden, odpowiednio sklejony łańcuch. Łączenie polega na tym, że w wyniku pomiędzy elementami kolekcji wstawiony jest pewien określony ciąg (“klej”). Najlepiej widać to na przykładzie:
Łatwo zauważyć, że join
jest w gruncie rzeczy przeciwieństwem funkcji split
, którą nieprzypadkowo kiedyś już opisywałem :)
W czym przejawia się przydatność tej operacji? Przede wszystkim rozwiązuje ona “problem ostatniego przecinka” przy wypisywaniu list. Tradycyjnie obchodzi się go mniej więcej tak:
for (int i = 0; i < (int)strings.length(); ++i) {
std::cout << strings[i];
if (i + 1 < (int)strings.length()) std::cout << ", ";
}[/cpp]
Instrukcja if
w tej pętli nie jest oczywiście szczytem elegancji. Gdybyśmy mieli tu funkcję join
wszystko byłoby o wiele czytelniejsze:
std::cout << join(", ", strings);[/cpp]
Drugą zaletą join
a jest jego dobra współpraca z modnymi ostatnio, funkcyjnymi rozszerzeniami wielu języków, pozwalająca w zwięzły sposób przetwarzać kolekcje obiektów. Jeśli na przykład mamy słownik (tudzież mapę/hash), to zamiana go na tekstowy odpowiednik klucz=wartość jest prosta:
Oczywiście jest tak wówczas, gdy na widok słowa kluczowego lambda
nie uciekamy z krzykiem ;-)
Na koniec tej krótkiej pogadanki wypadałoby jeszcze zaprezentować przykładową implementację omawianej funkcji. Ponieważ – jak napomknąłem wcześniej – doskwierał mi ostatnio jej brak w Javie, więc kod będzie w tym właśnie języku:
Z dokładnością do szczegółów generycznych kolekcji i operacji na stringach, powyższą implementację powinno się dać łatwo przetłumaczyć także na C++.
Sprzętowe odkrycie zeszłego roku – tablety – to wynalazek, którego użyteczność wciąż wymykała się moim zdolnościom poznawczym. Ponieważ jednak staram się zwykle być jak najdalej od negowania czegoś, czego do końca mogę nie rozumieć, chciałem już od dość dawna przyjrzeć się sprawie nieco bliżej. Za bliższe poznanie nie może niestety uchodzić kilkadziesiąt minut raczej bezładnego mazania po ekranie iPada (w obu wersjach), zatem postanowiłem na czas świątecznego weekendu wypożyczyć z pracy inne urządzenie tego rodzaju: Motorolę Xoom. Po kilku dniach korzystania z tego gadżetu sądzę, że już mniej więcej rozumiem zamysł stojący za tego typu sprzętem.
I właśnie tym cennym odkryciem chciałem się dzisiaj podzielić :)