Czytelne wywołania funkcji

2011-05-23 21:22

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:

  1. # posortowanie listy łańcuchów od najdłuższego
  2. sorted(strings, key = len, reverse = True)

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):

  1. hWindow = CreateWindowEx(NULL,                   // rozszerzony styl
  2.                           windowClass.c_str(), // klasa okna
  3.                           "My Window",       // tekst na p. tytułu
  4.                           WS_OVERLAPPEDWINDOW,   // styl okna
  5.                           20,         // współrzędna X
  6.                           20,         // współrzędna Y
  7.                           600,         // szerokość
  8.                           500,         // wysokość
  9.                           NULL,                  // okno nadrzędne
  10.                           NULL,                  // menu
  11.                           hInstance,             // instancja aplikacji
  12.                           NULL);                 // dodatkowe dane

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:

  1. const char* windowTitle = "My Window";
  2. POINT pos = { 20, 20 };
  3. SIZE size = { 600, 500 };
  4. DWORD style = WS_OVERLAPPEDWINDOW;
  5. hWindow = CreateWindowEx(NULL, windowClass.c_str(), windowTitle,
  6.        style, pos.x, pos.y, size.cx, size.cy,
  7.        NULL, NULL, hInstance, NULL);

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.

Tags: , , ,
Author: Xion, posted under Programming » 6 comments

Drugi problem, czyli API do wyrażeń regularnych

2011-05-19 22:54

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:

Tekstową 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ć:

  1. boost::regex exp("\\w+"); // kompiluje wyrażenie \w+

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:

  1. IPV4_REGEX = re.compile(r"^([12]?\d{1,2}\.){3}[12]?\d{1,2}$")
  2. def is_ipv4_addr(text):
  3.     return bool(IPV4_REGEX.match(text))

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:

Obiekt 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.

  1. import re
  2.  
  3. # wyrażenie dopasowujące tagi BBCode, np. [b]foo[/b]
  4. BBTAG_RE = re.compile(r"\[\s*(\w+)\s*\](.*)\[/\s*\1\s*\]")
  5.  
  6. # funkcja zamieniająca pojedynczy tag BBCode na HTML
  7. SIMPLE_BBTAGS = { 'b': 'strong', 'i': 'em', 'u': 'u' }
  8. def _bbtag_to_html(match):
  9.     tag = match.group(1).lower()
  10.     content = match.group(2)
  11.      
  12.     if tag in SIMPLE_BBTAGS.keys():
  13.         html_tag = SIMPLE_BBTAGS[tag]
  14.         return "<%s>%s</%s>" % (html_tag, content, html_tag)
  15.     if tag == 'url':
  16.         return '<a href="%s">%s</a>' % (content, content)
  17.     if tag == 'img':
  18.         return '<img src="%s" alt="">' % content
  19.      
  20.     return ""
  21.  
  22. # właściwa funkcja
  23. def bbcode_to_html(text):
  24.     return BBTAG_RE.sub(_bbtag_to_html, text)

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ć.

Tags: , , , , ,
Author: Xion, posted under Programming » 2 comments

Indeksowanie w Pythonie

2011-05-15 16:34

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:

  1. int tab[N];
  2. // ...
  3. assert( tab[i] == *(tab + i) );

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 [i; j). 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:

  1. tab[1:] # tablica bez pierwszego elementu
  2. tab[:-1] # tablica bez ostatniego elementu
  3. tab[:n] # co najwyżej n początkowych elementów tablicy
  4. tab[-n:] # n > 0 końcowych elementów tablicy
  5.  
  6. # początkowy ciąg aż do wystąpienia znaku @
  7. username = email[:email.index('@')]
  8.  
  9. # końcowy ciąg począwszy od ostatniej kropki
  10. extension = filename[filename.rindex('.'):]

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.

Tags: , , ,
Author: Xion, posted under Programming » 6 comments

Google I/O Extended 2011 w Warszawie

2011-05-11 9:47

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:

  • Usługa streamowania swojej własnej muzyki z “chrmury” na dowolne urządzenie (komputer, telefon, itp.), po uprzednim załadowaniu plików – czyli Google Music. Z jej najważniejszych cech należy wymienić przede wszystkim to, że jest dostępna wyłącznie w USA :P
  • Android Open Accessories i ADK – otwarta (przynajmniej w sensie androidowym) platforma programowo-sprzętowa, umożliwiająca tworzenie urządzeń, gadżetów i różnych innych akcesorii, które można następnie kontrolować za pomocą telefonów i tabletów z systemem Android (od 3,1 wzwyż). Otwiera to pole do automatyzacji wielu otaczających nas “interfejsów” technicznych, jak na przykład oświetlenie czy sprzęty kuchenne… w mniej lub bardziej odległej przyszłości ;)

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 ;-)

Tags: , , ,
Author: Xion, posted under Events, Thoughts » Comments Off on Google I/O Extended 2011 w Warszawie

Sprawcie sobie piłeczkę

2011-05-06 19:23

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.


Mój egzemplarz pochodzi ze
spotkania HTML5 w Chrome Web Store

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:

  • Piłeczkę można podrzucać. To pożyteczne zajęcie trenuje refleks i koordynację ruchową, a także pozwala w pożyteczny sposób zabić czas oczekiwania na ukończenie jakiejś dłuższej operacji. Krótko mówiąc, piłeczka jest narzędziem wspomagającym kompilację. Istnieje dowiedziona (przez amerykańskich naukowców) zależność między wielkością i stopniem skomplikowania projektów, nad którymi pracuje programista, a jego umiejętnościami żonglowania.
  • Piłeczkę można ściskać. Wspomaga to koncentrację i pozwala na łatwiejsze skupienie się podczas poszukiwania rozwiązań problemów, na jakie natrafiamy w trakcie kodowania. Nie bez znaczenia jest też fakt, że ćwiczenie to działa pozytywnie w profilaktyce RSI.
  • Piłeczką można rzucać o ścianę. Jest to rzecz jasna metoda wspomagająca debugowanie, której adekwatność jest skorelowana z czasem poświęconym na szukanie błędu. Oprócz absorbowania negatywnej energii piłeczka odbijająca się od ściany pozwala też na chwilowe oderwanie się od przeczesywania kodu w poszukiwaniu usterki, co z kolei często umożliwia uświadomienie sobie pomyłki, która legła u jej podstaw.
  • Piłeczka toczy się i spada z biurka. Ta jej cecha to idealny przykład pozornego buga, pod którym tak naprawdę kryje się użyteczny feature. Jeśli bowiem piłeczka spadnie nam z biurka i potoczy się w odległe miejsce, konieczne będzie doprowadzenie jej do porządku. Tego nie da się raczej zrobić zdalnie, więc będzie musieli wstać z krzesła i zrobić kilka kroków, a to zawsze jest pożądane.

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 :)

Tags: , ,
Author: Xion, posted under Programming » 9 comments

Funkcja join()

2011-04-29 21:24

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:

  1. array = ["Ala", "ma", "kota"]
  2. text = str.join(" ", array)
  3. assert text == "Ala ma kota"

Ł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ą joina 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:

  1. import os
  2. def join_dict(d):
  3.      # os.linesep to separator wierszy właściwy dla systemu
  4.     return str.join(os.linesep, map(lambda item: "%s=%s" % item, d.items()))
  5.  
  6. data = { "fullscreen": 1, "width": 800, "height": 600 }
  7. print join_dict(data)
  8. # fullscreen=1
  9. # width=800
  10. # height=600

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:

  1. public static String join(final Collection<?> s, final String delimiter) {
  2.  
  3.     final StringBuilder builder = new StringBuilder();
  4.     final Iterator<?> iter = s.iterator();
  5.     while (iter.hasNext()) {
  6.         builder.append(iter.next());
  7.         if (!iter.hasNext()) break;
  8.         builder.append(delimiter);
  9.     }
  10.     return builder.toString();
  11. }

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++.

Tags: , , , ,
Author: Xion, posted under Programming » 7 comments

Xoom, czyli tablety mają swój urok

2011-04-24 18:02

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ć :)

 


© 2023 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.