Posts from 2010

Przekierowania do innych URL-i

2010-10-09 18:18

Potrzeba automatycznego przekierowania strony WWW do innego adresu URL pojawia się całkiem często. Dość typowe są na przykład serwisy typu download, gdzie faktyczne ściągnięcie pliku poprzedzone jest krótkim oczekiwaniem i odpowiednim komunikatem. Służy to głównie temu, by użytkownik omyłkowo nie rozpoczął procesu ściągania więcej niż raz.

Przyjrzyjmy się dzisiaj sposobom na dokonywanie takich przekierowań. Jeśli bowiem chwilę się nad tym zastanowić, to okazuje się, że można je przeprowadzać na różnych poziomach protokołów HTTP i nie tylko. Wśród możliwych metod można wyróżnić choćby takie:

  • Javascript i window.location. Ustawienie tej właściwości na adres URL powoduje automatyczne wykonanie przekierowania. Do działania wymaga to naturalnie obsługi Javascriptu po stronie klienta, więc może służyć do odfiltrowania tych klientów, którzy nie spełniają tego warunku. Dotyczy to na przykład przeglądarek tekstowych, takich jak Lynx.
  • Znacznik <meta>. HTML przewiduje możliwość odświeżenia strony po określonym czasie, z opcjonalnym przekierowaniem do innego URL-a. Korzysta się w tym celu ze znacznika &lt;meta>:
    1. <meta http-equiv="Refresh" content="10; http://other.url.com">

    Właśnie ta metoda jest zwykle wykorzystywana do implementacji wspomnianego na początku opóźnionego ściągania plików.

  • Kody 3xx odpowiedzi HTTP. Protokół HTTP przewiduje możliwość zwrócenia przez serwer kodu odpowiedzi, powodującego (przynajmniej w założeniu) przekierowanie do innego adresu (przekazanego w nagłówku Location). Istnieje cała grupa takich kodów o numerach zaczynających się od 300, jak na przykład 301 (Moved Permanently), będący trwałym przekierowaniem, czy 302/307 (Found/Temporary Redirect), wskazujące na mniej lub bardziej tymczasowe odesłanie do innego adresu.
  • Rekordy CNAME w DNS. Jeszcze niżej w warstwie sieciowej położone są “przekierowania” z użyciem systemu DNS (Domain Name System) i rekordów CNAME (lub DNAME), które to można ustawiać dla swoich domen. Gdy są obecne i wykorzystywane przez serwer DNS, wówczas klient pytający o adres IP związany z domeną (np. www.a.com) będzie poinstruowany, by użył innej nazwy (np. foo.b.net) i ponowił zapytanie. Takie “przekierowanie” nie będzie raczej widoczne dla użytkownika w wysokopoziomowych programach takich jak przeglądarki WWW, a co najwyżej w narzędziach sieciowych typu ping.

Jeśli przypadkiem zastanawiamy się, który rodzaj przekierowania powinniśmy zastosować, to spieszę z odpowiedzią, że prawie na pewno dobrym wyborem będzie któryś z dwóch środkowych elementów listy. Uzasadnieniem jest tu głównie chęć współpracy z robotami wyszukiwarek internetowych. Nie powinniśmy mianowicie spodziewać się, by uruchamiały one kod Javascript na przeglądanych przez siebie stronach, bo kosztowałoby to je zbyt dużo zasobów. Z drugiej strony istnieje też duża szansa, że nie będą one “świadome” “przekierowań” dokonywanych na poziomie systemu DNS, przez co mogą one potraktować dostępność tych samych treści z dwóch różnych adresów jako świadome duplikowanie contentu i próbę ich oszukania.

W grę wchodzą naturalnie jeszcze kwestie praktyczne. Nie każdy dysponuje własną domeną lub możliwością zmiany jej rekordów DNS. Nie każdy może też zmieniać ustawienia serwera HTTP, na którym funkcjonuje jego serwis. Wtedy przydatne są przekierowania dokonywane po stronie klienta np. za pomocą tagów HTML.

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

lonu pilno be loi sucta bei tu’a la lojban.

2010-10-05 23:01

Używanie abstrakcji w lojbanie.

W jednej z wcześniejszych notek na temat lojbanu wspominałem o tym, że często posługujemy się w nim konstrukcjami, które określa się jako abstrakcje. Pojawiały się one kilka razy nawet tutaj. Osobiście uważam je za jedną z ciekawszych cech języka, dlatego też chciałbym dzisiaj opisać ją nieco dokładniej.

Przypomnijmy, że “zdanie” w lojbanie – czyli bridi – to pewna relacja określona na swoich argumentach, zwanych sumti:

lo bolci cu gunro fa’a le vorme
Piłka toczy się w kierunku bramki.

Ta relacja może opisywać jakieś zdarzenie, fakt, właściwość, itp. Dopóki jej argumentami są proste pojęcia (takie jak piłka czy bramka), do jej wyrażenia wystarczają równie nieskomplikowane konstrukcje gramatyczne. Jednak nie zawsze tak jest. Nierzadko chcemy mówić nie tyle o konkretnych obiektach, co właśnie o zdarzeniach, faktach lub właściwościach, które w lojbanie należą do grupy pojęć zwanych abstrakcjami.
Aby to zrobić, należy jedno bridi uczynić argumentem innego. Jest to możliwe przy pomocy tzw. abstraktorów, z których najczęściej używanym jest nu. Zaaplikowanie go do bridi daje w wyniku zdarzenie, które owo bridi opisuje. Zapewne najłatwiej zobaczyć to na przykładach takich, jak poniższe:

mi klama lo zarci — Idę to sklepu.
ti nu mi klama lo zarci — To jest zdarzenie mojego pójścia do sklepu.
lo nu mi klama lo zarci cu pluka mi — Pójście do sklepu sprawiło mi przyjemność.

Ostatnie wyrażenie prawdopodobnie przetłumaczylibyśmy raczej jako: “Cieszę się, że poszedłem do sklepu”, czyli używając pewnego rodzaju zdania złożonego. W lojbanie to samo wyrażamy nieco inaczej, odnosząc się do ‘pójścia do sklepu’ (lonu mi klama lo zarci) w ten sam sposób, w jaki wcześniej odwoływaliśmy się do bardziej konkretnych obiektów. Ten trochę inny schemat myślenia jest według mnie całkiem interesujący.
Oczywiście przy bardziej skomplikowanych wypowiedziach konieczne może się okazać wielokrotne zagnieżdżanie – tak jak w poniższym przykładzie potencjalnej dyskusji po meczu pewnej znanej i lubianej gry strategicznej:

lonu mi fliba cu jalge lonu lerci fa lonu mi zbasu le .ekspo
Moja przegrana była wynikiem tego, że spóźniłem się z budową expa.

Jak widać to, co lojban wyraża jednolicie za pomocą abstraktora nu, w języku polskim wymaga fraz typu ‘to, że’ albo rzeczowników pochodzących od czasowników – jak ‘przegrana’ czy ‘budowa’.

Jak można się domyślić, nu nie jest jedynym abstraktorem, a zdarzenia nie są jedynym rodzajem abstrakcji w lojbanie. Innym często spotykanym słówkiem tego typu jest du’u, którego używamy, dyskutując o faktach, opiniach czy wiedzy, gdyż jest to abstraktor stwierdzenia:

mi djuno lodu’u la berlin. raltca lo dotco — Wiem, że Berlin jest stolicą Niemiec.
la robert jdice lodu’u litru lo fraso — Robert zdecydował się na podróż do Francji.

W końcu jest też abstraktor ka, wskazujący na właściwości lub cechy. Jego użycie jest już trochę bardziej wyrafinowane, ale typowym przykładem są porównania analogiczne do stopniowania przymiotników. Wówczas właściwość jest “skalą” lub kryterium porównawczym:

do zmadu mi loka nelci lo grute — Lubisz owoce bardziej niż ja.
mi sisku loka xunre lo’i karce — Szukam czerwonego samochodu (wśród innych).

Jak widać skalą może być “czerwoność” (loka xunre), “lubienie owoców” (loka nelci lo grute) i w zasadzie cokolwiek, co tylko możemy sobie wymyślić i jest poprawnym bridi. Możliwości są tu więc bardzo, bardzo duże.

Istnieją też oczywiście inne, rzadziej używane abstraktory, jak ni czy si’o. Zainteresowanych odsyłam do odpowiednich źródeł :)

Tags: ,
Author: Xion, posted under Culture » 1 comment

Maksimum, minimum

2010-09-30 18:52

Programiści grafiki czasu rzeczywistego wiedzą, że gdy w obliczeniach potrzebne są wartości sinusa i cosinusa dla tego samego kąta, należy obliczać je razem. (O ile oczywiście ich nie tablicujemy). To, o czym chciałbym napisać dzisiaj, jest kwestią bardzo podobną, ale możliwą do stosowania w sytuacji spotykanej przez właściwie każdego programistę.

Chodzi o znajdowanie największego i najmniejszego elementu w dowolnej kolekcji. Uzyskanie każdej z tych wartości osobno jest proste niczym Hello world, nawet jeśli nasz język nie ma od tego żadnej wbudowanej funkcji (są jeszcze takie?). Okazuje się jednak, że oddzielne szukanie minimum i maksimum jest nieefektywne. Jeśli bowiem potrzebujemy obu wartości, to należy szukać ich jednocześnie.
Jak? Całkiem prosto. Przechodząc po zawartości pojemnika, trzeba zajmować się naraz nie jednym, lecz dwoma elementami. Zaczynamy jednak od porównania ich ze sobą, a dopiero potem z aktualnym maksimum (większy z dwóch) i aktualnym minimum (mniejszy). Implementacja tego pomysłu dla pojemników STL może wyglądać na przykład tak:
template void minmax(const C& c, T* min, T* max)
{
typename C::size_type s = c.size();
if (s == 0) return;
if (s == 1) { *min = *max = c[0]; return; }

// inicjalizacja wartości początkowych min i max
T m, M;
typename C::size_type i;
if (s & 1u) { m = M = c[0]; i = 1; }
else
{
if (c[0] <= c[1]) { m = c[0]; M = c[1]; } else { m = c[1]; M = c[0]; } i = 2; } T x, y; /* x >= y */
for ( ; i < s; i += 2) { if (c[i] <= c[i + 1]) { y = c[i]; x = c[i + 1]; } else { x = c[i]; y = c[i + 1]; } if (x > M) M = x; if (y < m) m = y; } *min = m; *max = M; }[/cpp] Ponieważ liczba elementów może być nieparzysta lub parzysta, konieczna jest odpowiednia inicjalizacja. W pierwszym przypadku bierzemy pierwszy element jako początkowe maksimum i minimum, zaś w drugim dokonujemy porównania dwóch pierwszych elementów. Resztę kolekcji możemy już przetwarzać dwójkami. Zysk wydajnościowy z takiego podejścia to teoretycznie około 25%. Wynika to z faktu, iż na dwa elementy "zużywamy" tutaj tylko trzy porównania, czyli o jedno mniej niż dla oddzielnych wywołań min i max. W praktyce osiągi te psuje bardziej czasochłonny start funkcji, więc opłaca się ją stosować głównie dla większych zbiorów elementów.

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

position w CSS

2010-09-27 15:52

Niniejsza notka ma specyficzne przeznaczenie. Chociaż nie zdziwiłbym się, gdyby dla wielu osób okazała się interesująca i przydatna, to jej głównym celem jest przechowanie dla mnie pewnej wiedzy, której ogarnięcie zajęło mi całkiem sporo czasu. Tak, chodzi właśnie o tytułowy atrybut position z CSS i jego działanie.

A zatem… Jak nazwa jednoznacznie sugeruje, atrybut ten ma dużo wspólnego z określaniem umiejscowienia elementu wyświetlanego dokumentu. Nie zawiera on jednak pozycji jako takiej, lecz sposób jej wyznaczania. Jest to więc atrybut wyliczeniowy, którego możliwe wartości są następujące:

  • static – domyślny sposób pozycjonowania. Element zostanie umieszczony i będzie wyświetlany w miejscu, które wynika z położenia jego znacznika w źródle dokumentu, czyli w ciągu otaczającego go tekstu. (Jeśli oczywiście sam ten tekst nie został jakoś fikuśnie wypozycjonowany). Atrybuty określające współrzędne (left, top, itd.) są ignorowane.
  • relative – bardzo podobne do powyższego. Różnica polega na tym, że tutaj wspomniane atrybuty są wykorzystywane i mają wpływ na to, gdzie element jest wyświetlany. Ich punktem odniesienia jest “normalna” pozycja elementu w ciągu tekstu. Znanym przykładem są “wciskane” linki, polegające na przesunięciu łącza odrobinę w prawo i w dół, by dać wrażenie jego wciśnięcia:
    1. a:active {
    2.     position: relative;
    3.     left: 2px;
    4.     top: 2px;
    5. }

    To, czego position:relative nie robi, to rzeczywista zmiana położenia elementu. Nawet jeśli jest on wyświetlany gdzie indziej, tekst będzie nadal łamany według jego normalnego położenia – tak samo jak przy position:static.

  • absolute – tzw. pozycjonowanie absolutne, które w rzeczywistości jest… względne. (Ah, ta logika webdesignu ;>). Przy tym ustawieniu element jest mianowicie wyjęty z normalnego ciągu tekstu, co czasem określa się jako stworzenie nowej warstwy (layer). Jego pozycja jest wówczas określana (atrybutami left itd.) względem elementu go zawierającego, którym to jest najbliższy element nadrzędny o pozycjonowaniu innym niż static… Proste i nieskomplikowane, prawda? ;-)
  • fixed – stała pozycja. Stałość polega na tym, że element nie zmienia swojego położenia podczas przewijania strony, bo punktem odniesienia jest dla niego okno przeglądarki. Tak jak w przypadku absolute, tutaj również tworzy on warstwę wyjęta z normalnego ciągu tekstu.

Tyle teoria. W praktyce mamy kilka typowych przypadków użycia. Prawdopodobnie najbardziej skomplikowanym z nich jest zastosowanie position:absolute ze względu na jego dziwne wymaganie odnośnie elementu zawierającego. Jeśli go nie uszanujemy, wówczas nasz ‘absolutnie’ wypozycjonowany element jako punkt odniesienia przyjmie cały obszar dokumentu, czyli element <body>. Aby temu zapobiec, należy otaczającemu go pojemnikowi ustawić position na relative:

  1. <div id="container" style="position: relative">
  2.     <div id="layer" style="position: absolute; top: 10px; left: 5px;">...</div>
  3. </div>

Wizualnie nic on się wtedy nie zmieni (jeśli sam nie zawiera atrybutów w stylu left, itp.), natomiast będzie on mógł być kontenerem dla elementów z position:absolute.

Tags: ,
Author: Xion, posted under Internet » 1 comment

Udekorowane funkcje

2010-09-22 16:46

Kilkanaście dni temu opisywałem błąd, który – jak się okazało – był efektem ubocznym pewnej cechy języka Python, uznanej przeze mnie za niemal zupełnie niepotrzebną. Dla równowagi więc dzisiaj przedstawię feature, który wydaje się być bardzo przydatny – żeby nie było, że potrafię tylko krytykować ;-)

Prawdopodobnie najlepiej jest pokazać go na prostym, acz obrazowym przykładzie. Miejmy pewną funkcję na tyle dla nas istotną, że chcemy logować wszystkie jej wywołania. Wydaje się, że nic prostszego:

  1. import logging
  2.  
  3. def vif(): # Very Important Function :)
  4.     logging.debug("Called function vif()")
  5.     # ...

Ciężko jednak nazwać takie rozwiązanie uniwersalnym. Nie chodzi tu jedynie o jawnie wpisaną nazwę funkcji, ale raczej o konieczność dodawania logging.debug(...) na początku każdej funkcji, jaką chcemy monitorować. Sprawa komplikuje się jeszcze bardziej, gdy interesują nas też wyjścia z funkcji; wówczas jedynym wyjściem jest chyba opakowanie całej treści w jeden wielki blok tryfinally. Rezultat na pewno nie będzie piękny :)

I tutaj właśnie z pomocą przychodzą dekoratory – ciekawa opcja języka Python, na pierwszy rzut oka przypominająca adnotacje z Javy. Podobieństwo jest jednak głównie składniowe. Udekorowana wersja naszej ważnej funkcji wygląda bowiem tak:

  1. @trace
  2. def vif():
  3.     # ...

Znak @ poprzedza tutaj nazwę dekoratora, czyli trace (ang. śledź – i bynajmniej nie chodzi o rybę ;]). Czym jednak jest ów dekorator? Otóż on sam również jest funkcją, mogącą wyglądać choćby tak:

  1. import logging
  2.  
  3. def trace(func):
  4.     def _func(*args, **kwargs):
  5.         logging.debug("Calling function %s()", func.__name__)
  6.         return func(*args, **kwargs)
  7.  
  8.     return _func

Jej jedynym argumentem jest w założeniu funkcja, zaś rezultatem wywołania jest… również funkcja :) A zatem nasz dekorator potrafi przekształcić jedną funkcję w drugą, a dokładniej w jej udekorowaną, “opakowaną” wersję. To opakowanie definiowane jest wewnątrz dekoratora i polega, jak widać, na poprzedzeniu wywołania oryginalnej funkcji zapisem do loga.
Oczywiście za to, aby wywołanie funkcji opatrzonej dekoratorem było tak naprawdę odwołaniem do jej udekorowanej wersji odpowiada już sam język. Nie jest to zresztą skomplikowane, bo w istocie cały mechanizm jest tylko cukierkiem składniowym. Jest to jednak bardzo smaczny cukierek, który potrafi wydatnie podnieść czytelność kodu – jeśli stosuje się go właściwie.

Do czego jednak – oprócz logowania wywołań – dekoratory mogą się przydać? Ano przede wszystkim do upewniania się, że pewne konieczne warunki wstępne dla funkcji są spełnione na jej wejściu (i ewentualnie wyjściu). Może to być na przykład:

  • @connected – sprawdzenie połączenia z serwerem zanim spróbujemy wymieniać z nim dane i nawiązanie go w razie potrzeby
  • @authorized – określenie uprawnień wymaganych u aktualnie zalogowanego użytkownika przed wywołaniem funkcji wykonującej potencjalnie niebezpieczną operację
  • @synchronized – zabezpieczenie wywołania funkcji semaforem lub sekcją krytyczną

Wspólną cechą takich dekoratorów jest to, że są one swoistymi pseudodeklaracjami, nieodległymi koncepcyjnie zbyt daleko od komentarzy w rodzaju:

  1. # Ta funkcja wymaga połączenia z serwerem!
  2. def do_something():
  3.     # ...

Ich przewagą jest jednak rzeczywiste sprawdzanie, czy wymagania zostały spełnione – i to w sposób automatyczny i przezroczysty. Według mnie to właśnie stanowi o ich sporej przydatności.

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

Zabezpieczenie przed NULL-em

2010-09-18 22:36

Defensywne programowanie wymaga, by zabezpieczać się przed różnymi niepożądanymi sytuacjami. Jedną z częstszych jest próba odwołania się do obiektu czy wartości, która nie istnieje – czyli np. dereferencja wskaźnika pustego czy użycie odwołania zawierającego null. Stąd bardzo częste ify w rodzaju:

  1. if (!p) return false;
  1. if (arg == null) throw new ArgumentNullException("arg");

Kiedy jednak sprawdzenie nullowatości jest tylko częścią warunku, wtedy w ruch idzie zwykle “sztuczka” z leniwą ewaluacją (lazy evaluation):

  1. if (p && p->key == k) return p;

Większość języków ją dopuszcza, jako że jest ona przy okazji pewną formą optymalizacji. Jeśli bowiem pierwszy argument operatora logicznego daje informację o prawdziwości/fałszywości całego wyrażenia, nie trzeba już wyliczać drugiego.

Inną typową sytuacją jest zamiana nulla na jakąś inną wartość domyślną:

  1. string str = s != null ? s : "N/A"; // C++/C#/Java
  1. s = s if s != None else "N/A" # Python

Tutaj C# oferuje specjalny operator ??, pomyślany właśnie na tego typu okazje:

  1. string str = s ?? "N/A";

Działa on dobrze z typami Nullable, czyli specyficznym rodzajem typów pochodnych, które dopuszczają wartość null tam, gdzie typ macierzysty jej nie przewiduje:

  1. int? x = null; // liczba typu int lub null
  2. int y = x ?? -1; // ustaw y = -1; jeśli x == null

Operator ?? przydaje się wtedy do konwersji na typ bazowy z określoną wartością domyślną.

Tags: , , ,
Author: Xion, posted under Programming » Comments Off on Zabezpieczenie przed NULL-em

Cebulowy postęp

2010-09-15 17:48

Co pewien czas można natknąć się na porównania odnośnie mocy obliczeniowej komputerów bardzo dawnych i tych dzisiejszych. Takim dość typowym, często powtarzanym stwierdzeniem jest na przykład to, iż komputer sterujący misją Apollo 11 miał moc porównywalną z dzisiejszym kalkulatorem. Podobne ciekawostki służą czasami ukazaniu gigantycznego postępu, jaki dokonał się w ciągu ostatnich kilku dekad jeśli chodzi o sprawność jednostek obliczeniowych.

Jednak nierzadko służą one także kwestionowaniu kierunku zmian w szeroko pojętej technologii komputerowej, które były możliwe właśnie dzięki tak wielkiemu wzrostowi mocy obliczeniowej procesorów. Bez zbędnych słów można te argumenty streścić w postaci następującego pytania: Skoro 40 lat temu “kalkulator” o częstotliwości 1 Mhz mógł dowieźć ludzi na Księżyc, to dlaczego obecnie potrzebujemy tysiąc razy większej mocy obliczeniowej, aby napisać prosty dokument tekstowy?… Aż chciałoby się dodać: co poszło nie tak? :) Ale spokojnie, wszystko jest w jak najlepszym porządku. Takie postawienie sprawy to bowiem nic innego, jak chwytliwy slogan, który przedstawia ją w sposób zdecydowanie zbyt uproszczony. Krótko mówiąc, zionie on przerażającą ignorancją.

Nie chodzi tu już nawet o wyraźne przecenienie stopnia skomplikowania zadań, przed jakimi ów sławetny komputer pokładowy stawał i jakości rozwiązań, które dla nich znajdował (ich częścią był chociażby “interfejs użytkownika”, składający się z komunikatów-liczb, których odcyfrowanie wymagało sporej książki). Bardziej irytuje mnie sugestia, że w dzisiejszych aplikacjach cała ta olbrzymia moc obliczeniowa jest marnowana na parę w gwizdek. No, czyli właściwie na co?… Odpowiedź brzmi: na mnóstwo “oczywistych” rzeczy, na które normalnie nie zwracamy uwagi albo o których nie musimy nawet myśleć. Jest ich dużo, bardzo dużo – i ich lista cały czas się powiększa.

Przykłady? Ależ proszę; nie trzeba ich wcale daleko szukać. Czy ktoś może chociażby uczciwie powiedzieć, że dokładnie wie, jak działa Internet? Jak to się dzieje, że dane mogą zostać przesłane z jednego końca świata na drugi, mijając po drodze kilometry kabli, setki routerów, dziesiątki różnie skonfigurowanych podsieci, a pewnie i kilka satelitów, i być odczytane na komputerze nie tylko działającym pod kontrolą całkiem innego systemu operacyjnego, ale składającym się być może z zupełnie różnych podzespołów?… Totalna magia :)
Ale to jest właśnie ów gwizdek. Taka karkołomna operacja jest możliwa tylko dlatego, że występujące po drodze ogniwa dysponują wystarczającą mocą obliczeniową i przepustowością łączy, by w rozsądnym czasie dokonać wielokrotnego rozpakowywania i ponownego pakowania danych w poszczególne warstwy komunikacji. Każda z nich: Ethernet, IP, TCP, HTTP (a na tym pewnie Unicode, XML/JSON/itp., RPC i w końcu protokół własny aplikacji) służą temu, aby – paradoksalnie – coś uprościć i… przyspieszyć. Tym czymś jest oczywiście tworzenie aplikacji – to, co trwa najdłużej, jest najkosztowniejsze i nie poddaje się prostemu skalowaniu tak, jak możliwości elektroniki.

Rozwój IT polega więc (w dużym stopniu) na dodawaniu kolejnych warstw abstrakcji do coraz większej cebuli. Nie jest to jednak przyczyna, a skutek coraz lepszych możliwości technicznych sprzętu. Możemy sobie po prostu na to pozwolić. I bardzo dobrze.
Nie zapominajmy bowiem, że komputer Apollo 11 programowany był w archaicznym asemblerze przez ładnych kilka lat przez sztab wybitnych fachowców z NASA. Czy nie powinniśmy doceniać faktu, że program wykonujące równoważnie skomplikowane zadania może dzisiaj stworzyć niemal każdy w znacznie krótszym czasie i o wiele przyjemniejszy sposób? Jeśli ktoś twierdzi, że to nie jest postęp, to doprawdy nie wiem, co mu odpowiedzieć :P

Tags: ,
Author: Xion, posted under Computer Science & IT, Internet, Thoughts » 6 comments
 


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