Zamieszczam slajdy z prezentacji, którą wygłosiłem dzisiaj w ramach Polidea Talks. Dotyczy ona androidowego wynalazku znanego jako Cloud to Device Messaging (C2DM) i będącego odpowiednikiem apple’owych Push Notifications z systemów iOS i OSX. Dla nie do końca zorientowanych spieszę z wyjaśnieniem, iż jest po prostu dość sprytny sposób na to, by urządzenie mobilne mogło “natychmiast” otrzymywać informacje ze zdalnego serwera – bez konieczności regularnej synchronizacji, która zużywa baterię i nie tylko.
Push! - Instant notifications on Android through C2DM (591.7 KiB, 1,974 downloads)
Slajdy są tym razem w klasycznym PDF-ie. Chwilowo nie mam żadnej lepszej alternatywy dla starego, dobrego PowerPointa :)
Branża IT oraz nauka zwana informatyką liczą sobie już ładnych kilkadziesiąt i wygląda na to, że dziedzina ta zaczyna w pewnym sensie “dorastać”. W dość nieprzyjemnym sensie, muszę dodać. Jej znani pionierzy zaczynają bowiem nas opuszczać, czego dobitym dowodem jest zeszły miesiąc, gdy kilku z nich pożegnaliśmy w ciągu stosunkowo krótkiego czasu.
Ponieważ dzisiejszy dzień jest dobrą okazją do takich wspomnień, pozwolę sobie zwrócić uwagę na jednego z nich. Tego, który bez wątpienia miał największy wpływ na to, w jaki sposób programujemy komputery i jak obecnie wygląda przemysł software‘owy.
Mam tu na myśli oczywiście Dennisa Ritchie.
Wpływ Ritchiego na kształt informatyki jest trudny do przeceniania, bowiem miał on ogromny udział w początkach rozwoju dwóch jej ważnych aspektów: języków programowania i systemów operacyjnych. Jego zasługi w pierwszej z tych dziedzin wyrażają się głównie w stworzeniu języka C – prawdopodobnie najszerzej wykorzystywanego języka programowania w historii IT. Nawet jeśli sami nigdy w C nie programowaliśmy, to istnieje bardzo duża szansa, że nasz ulubiony język programowania ma z C wiele wspólnego: począwszy od bezpośredniej historii (C++, Objective-C), poprzez składnię (C#, Java, JavaScript, D, Scala, Go, itp.) aż po kluczowe narzędzia w rodzaju interpreterów napisanych w C (Python, Ruby, Perl, PHP). W rzeczywistości trudno jest wskazać język, który nie miałby czegokolwiek wspólnego z C – z ważniejszych należą do nich chyba tylko wczesne warianty Lispa (którego twórca, notabene, również zmarł w zeszłym miesiącu…). Niełatwo jest więc przesadzić, mówiąc o decydującym wpływie Ritchie’ego na kształt narzędzi używanych obecnie przez programistów na całym świecie.
Podobnie jest zresztą z oprogramowaniem w ogóle. System UNIX – którego Ritchie był jednym z kluczowych twórców – w niezliczonych odmianach i pochodnych działa na sporej części istniejących komputerów i wyrobów komputeropodobnych. Dotyczy to zarówno superkomputerów, wielkich serwerowni (żeby nie użyć słowa na ‘ch’ ;]) oraz małych serwerów, ale też domowych PC-tów i laptopów, a nawet urządzeń mobilnych: telefonów i tabletów. Większość (albo przynajmniej duża część) z nich operuje pod kontrolą systemów wywodzących się z UNIX-a i używa przynajmniej części związanego z nim stosu oprogramowania, którego prawdopodobnie najważniejszym komponentem jest… dokładnie tak – kompilator C.
Nie ma oczywiście żadnej pojedynczej osoby, której moglibyśmy zawdzięczać obecną postać technologii obliczeniowych i informacyjnych. Jednak Dennis Ritchie jest bez wątpienia człowiekiem, bez którego wyglądałaby ona dzisiaj zupełnie inaczej. Dlatego też warto o nim pamiętać – nawet jeśli wskaźniki z C czy uniksowy terminal są dla nas strasznymi rzeczami, z którymi nie chcemy mieć nic wspólnego :)
(…) są dwa style pisania – pierwszy to: “popatrzcie jaki ja jestem mądry” – a drugi to: “popatrzcie jakie to proste”.
Jerzy Grębosz, Symfonia C++
Autor tego stwierdzenia miał na myśli przede wszystkim pisanie technicznej prozy, a więc wszelkiego rodzaju artykułów, książek, kursów i tutoriali (i blogów? ;]). Po namyśle stwierdzam jednak, że równie dobrze nadaje się ono i do poezji – czyli kodu. Podobnie bowiem przedstawia się kwestia wyższości drugiego stylu nad pierwszym, w większości przypadków.
“Większość” nie oznacza aczkolwiek “wszystkich”. Czasami górę biorą na przykład kwestie wydajnościowe, które niekiedy uzasadniają wyprodukowanie kodu wyglądającego jak przemyślnie zaszyfrowane zaklęcie. Klasycznym przykładem jest procedura szybkiego obliczania , wyciągnięta wprost ze źródeł Quake‘a III:
Funkcja ta (której autorstwo niesłusznie przypisuje się czasem Johnowi Carmackowi) doskonale pokazuje, jak ważna jest czytelność i zrozumiałość kodu. Robi to w najlepszy możliwy sposób, tj. nie posiadając żadnej z tych dwóch cech :) W zamian oferuje znacznie ważniejszą w swoim zastosowaniu jakość – czyli wydajność.
Obliczanie odwrotności pierwiastka kwadratowego to jedna z najczęściej używanych operacji w grafice 3D – zawiera ją np. każda normalizacja wektora. Sensowne jest więc jak największe zoptymalizowanie tej funkcji. Będzie ona przecież wywoływana setki czy tysiące razy podczas renderowania pojedynczej klatki.
W sumie daje to kilkadziesiąt tysięcy wywołań na sekundę, a to dość unikatowa perspektywa jeśli chodzi o wartościowanie szybkości, czytelności, elastyczności – i tak dalej. I właśnie dlatego wspominam o niej jako o oczywistym wyjątku. Parafrazując popularną ostatnio proporcję: w 99% przypadków podobny kompromis nie będzie musiał dotyczyć nawet 1% kodu :)
O wiele bardziej typową sytuacją jest bowiem zdecydowana wyższość klarowności, czystości i zrozumiałości. To jest właśnie ów drugi styl: “zobaczcie jakie to proste”. Bierze on pod uwagę oczywisty w gruncie rzeczy fakt, iż głównym odbiorcą kodu jest człowiek, a nie komputer. Jeśli przyszły czytelnik potrafi z łatwością zrozumieć intencje autora – bo są one wyrażone przejrzyście i jednoznacznie – to niemal równie łatwo przyjdzie mu modyfikacja, rozszerzanie i poprawianie programu. Nie wspominając już nawet o tym, że program, który daje się łatwo “wytłumaczyć” samym kodem z definicji nie może być zanadto skomplikowany. Syntaktyczna prostota przekłada się więc na semantyczną, która z kolei dobrze koreluje z innymi pożądanymi właściwościami – jak choćby niezawodnością.
Nie mam już wiele wspólnego ze światkiem World of Warcraft, ale akurat wieści o nowych dodatkach potrafią jeszcze wzbudzić we mnie zainteresowanie. Tak jest też z ogłoszonym (przed)wczoraj – zależnie od strefy czasowej – dodatkiem numer cztery, noszącym nazwę Mists of Pandaria.
Z pozoru nie zaskakuje on niczym nadzwyczajnym, jako że pod względem rozszerzeń WoW stał się już podobny do The Sims: ta sama idea z nowymi dekoracjami. Mamy więc tutaj wyższy limit poziomu postaci (90), nowy kontynent (Pandaria), nową rasę i klasę, świeżą dostawę instancji i raidów – i tak dalej. Krótko mówiąc: standard. W takiej sytuacji naturalnie jest poszukiwanie rozwiązań, które nie wpadają tak łatwo w dobrze znane szuflady i pozwalają domyślać się, w jakim kierunku gra będzie dalej podążać.
A według mnie jest on dość oczywisty. Otóż Blizzard chce dokonać czegoś, co jest “niemożliwe”, a zarazem absolutnie konieczne, biorąc pod uwagę obecne trendy w komputerowej rozrywce. Wyzwanie jest koncepcyjnie aż nazbyt proste, ale jego realizacja – jeśli w ogóle możliwa – wymaga balansowania na bardzo, bardzo cienkiej linie.
Co należy zrobić? Bagatela: połączyć Diablo z FarmVille – w przenośni, rzecz jasna.
Miałem ostatnio okazję wzięcia udziału w jednej z lokalnych edycji konferencji Google Developer Day. Na to wydarzenie składa się cykl wykładów prowadzonych przez przedstawicieli Google, traktujących o technologiach webowych, mobilnych i tym podobnych tematach. GDD, w którym akurat ja uczestniczyłem, odbywał się wczoraj w Pradze.
Przyznam, że nigdy wcześniej nie brałem udziału w podobnym wydarzeniu, więc było to bardzo interesujące doświadczenie. Większość z prezentowanych tematów wydawała się niezwykle ciekawa, a ze względu na to równoległy przebieg aż pięciu ścieżek wykładowych należało dokonać bardzo trudnego wyboru, których sesji chcemy posłuchać. Miejmy nadzieję, że wszystkie tak czy siak wkrótce trafią na YouTube :)
Od siebie dodaję kilka zdjęć. Ich jakość nie jest aczkolwiek powalająca, bo zostały wykonane sprzętem, którego główne przeznaczenie jest zgoła odmienne ;)
Update: Oficjalna galeria zdjęć z imprezy jest już dostępna.
W językach wspierających obiektowość zwykle mamy do czynienia z uniwersalną klasą bazową (Object
), od której wszystkie inne muszą dziedziczyć. Składniki tej klasy są więc wspólne dla wszystkich obiektów. Czasami ma to negatywne skutki (vide wait
i notify
w Javie), ale zazwyczaj jest przydatne, bo wspólne składniki – takie jak toString
– są często niezwykle użyteczne.
Istnieje jednak pewna metoda bazowa obiektów, której przeznaczenie nie musi być od razu oczywiste. Nazywa się ona dość podobnie w różnych językach: GetHashCode
w C#, hashCode
w Javie, zaś w Pythonie jest to po prostu __hash__
. Nietrudno się więc domyślić, że ma ona coś wspólnego z hashem obiektu :) O co jednak dokładnie chodzi?
Hash to kompaktowa reprezentacja pewnej porcji danych, uzyskana za pomocą określonego algorytmu. Takim algorytmem może być na przykład MD5, SHA1, SHA256 i inne. Podstawowym wymaganiem, jakie stawia się takim funkcjom, jest determinizm: dla identycznych danych powinny one zwracać dokładnie te same wyniki.
Do czego jednak przydaje się możliwość haszowania< dowolnych obiektów? Otóż pozwala to na tworzenie kontenerów haszujących, takich jak zbiory i mapy. W Javie na przykład chodzi tu o pojemniki HashSet
i HashMap
, będące jednymi z możliwych implementacji ogólnych interfejsów Set
i Map
. Takie kontenery używają hashy jako podstawy swojego działania, zakładając, że ich porównywanie jest szybsze niż branie pod uwagę całych obiektów. Dopiero równość ich hashy pociąga za sobą konieczność porównania samych obiektów.
Być może nasuwa się tu od razu słuszny wniosek, że hashe dla obiektów potrzebne są wobec tego jedynie wtedy, gdy implementujemy własny sposób ich porównywania. (Opisywałem kiedyś, jak to się robi w języku C#). Często powinniśmy wtedy zapewnić taką implementację ekwiwalentu metody hashCode
, która będzie spójna ze sposobem porównywania. Z grubsza chodzi o to, aby na hash wpływały pola, które bierzemy pod uwagę w metodzie equals
/Equals
/__eq__
– i tylko one.
W jaki sposób miałyby jednak to robić? No cóż, w teorii możemy zaprząc do pracy wymienione wyżej algorytmy i potraktować nimi połączone wartości pól obiektu (albo po prostu kawałek pamięci, w którym on rezyduje). W praktyce to bardzo kiepskie rozwiązanie (zwłaszcza wydajnościowo), bowiem wspomniane funkcje haszujące niezupełnie do tego służą. Istnieje bowiem różnica między kryptograficzną funkcją haszującą a zwykłą: ta pierwsza ma na celu przede wszystkim uniemożliwienie odtworzenia oryginalnych danych, jeśli znany jest tylko ich hash. W przypadku funkcji używanych wraz z pojemnikami haszującymi bardziej interesuje nas jednorodność, co (w uproszczeniu) oznacza, że hash obiektu powinien być wrażliwy na wartości poszczególnych pól tego obiektu. Z tego też powodu poniższe rozwiązanie:
jest do niczego, mimo że świetnie spełnia teoretyczne wymaganie, aby dwa równe obiekty miały równe hashe.
Dostępna jest oczywiście wyrafinowana wiedza na temat konstruowania dobrych (a nawet doskonałych) funkcji haszujących, ale dokładnie rachunki prawdopodobieństwa kolizji nieczęsto nas interesują – zwłaszcza, jeśli właściwie nie wiemy, w jakich pojemnikach i wśród jakich innych obiektów skończą te nasze. Na szczęście mamy też prostsze warianty. Wśród nich interesująco wygląda na przykład sposób pokazany w znanej książce Effective Java, który wygląda mniej więcej w ten sposób:
Zastosowanie go np. do prostej klasy punktu 3D wyglądałoby na przykład tak:
Wybór 23
jest raczej arbitralny, natomiast 31
ma tę zaletę, że mnożenie przez nią liczby x
jest równoważne przesunięciu bitowemu i odejmowaniu, tj. (x << 5) - x
. Analogicznie jest zresztą dla innych liczb o jeden mniejszych od potęg dwójki.
Porzekadło głosi, że w informatyce są tylko dwa trudne problemy: nazewnictwo, mechanizmy cache‘owania i pomyłki o jedynkę. Jeśli chodzi o jeden dwa ostatnie, to może przyjrzymy się im przy innej okazji… Dzisiaj chciałbym za to zająć się pierwszym z nich: dobieraniem odpowiednich nazw dla konstrukcji programistycznych, takich jak funkcje czy klasy.
Nie każda nazwa jest właściwa. O tej trywialnej prawdzie każdy pewnie przekonał się już dawno, zwłaszcza jeśli przechodził przez fazę zmiennych a
, b
, c
lub funkcji fun1
i fun2
. Zdaje się zresztą, że kiedyś była to powszechna przypadłość, co widać zwłaszcza w przypadku starych API *niksowych. Najwyraźniej jednak poszliśmy kolektywnie po rozum do głowy i dziś już nikt nie nazwie funkcji wait3
czy wait4
.
Nietrudno jest oczywiście wskazać podstawowy problem tego rodzaju nazw. Jakkolwiek jest on ściśle związany z długością, nie uzasadnia to automatycznie stwierdzenia, że wszystkie krótkie nazwy są złe. Żeby nie odchodzić daleko, wystarczy tylko spojrzeć na POSIX-owe, uniwersalne funkcje read
i write
. Ich nazwom nie brakuje dokładnie niczego; przeciwnie, próba dodania czegoś więcej wprowadzałaby tylko zamieszanie. readFromFileDescriptor
może i wskazywałaby wyraźnie na źródło danych, ale czy zupełnie poprawne użycie takiej funkcji na uchwycie sieciowego gniazda (socket) nie byłoby mylące? O jakim pliku
wtedy mówimy?
Naturalnie, w *niksach “wszystko jest plikiem” i należy o tym wiedzieć. Lecz skoro tak jest, to co zyskujemy przez dłuższą nazwę funkcji read
? Niby z czego innego niż z pliku mielibyśmy czytać?…
Dywagując na ten temat muszę koniecznie zaznaczyć, że nazwy zaśmiecone oczywistymi informacjami nie są wcale hipotetycznym problemem. Moim ulubionym przykładem – ze względu na swoją groteskową wręcz ekstremalność – jest poniższe wywołanie:
Ja wcale nie żartuję – tak w Objective-C (OSX/iOS) wygląda zastępowanie jednego ciągu w tekście innym. Zawsze chętnie wysłucham argumentów przekonujących o ekspresywności i opisowości podobnych nazw, ale nigdy nie uwierzę, że koderzy je rzeczywiście czytają. Zgaduję, że ich percepcja u statystycznego programisty polega na dostrzeżeniu “Repl
” lewym okiem i “ccurr
” prawym – albo coś w tym rodzaju. Jeśli mam rację, to nazwa ta jest znacząca w zaledwie 22 procentach; trudno to uznać za dobry stosunek sygnału do szumu.
Czy można było zrobić to lepiej? Zapewne – weźmy chociażby Javę:
Wygląda to całkiem dobrze. Okazuje się, że można użyć trzy razy krótszej nazwy i osiągnąć niemalże ten sam efekt, posługując się po prostu samą składnią języka (kolejnością parametrów i wartością zwracaną) zamiast długich tekstowych opisów.
Rozwlekłe nazwy – nawet jeśli camelCase czyni je znośnymi – nie zawsze są więc dobrą odpowiedzią. Czasami mogą one być równie nadmiarowe co niepotrzebne komentarze.