Posts from 2010

Zasada gołębiej dziury

2010-06-10 19:42

Dzisiejsza notka będzie o pewnej matematycznej ciekawostce, która pokazuje, że nawet pozornie oczywiste stwierdzenie mogą mieć daleko idące konsekwencje, jakich nie da się na pierwszy rzut oka przewidzieć. Mamy mianowicie pewną kombinatoryczną regułę, mówiącą (w jednej z wersji), że:

Jeżeli n obiektów umieścimy w m < n pudełkach, to znajdzie się pudełko zawierające więcej niż jeden obiekt.

Zaskakujące, prawda? ;-) Wydaje się, że niewiele jest rzeczy bardziej oczywistych. A jednak powyższe twierdzenie doczekało się swojej własnej nazwy – i to nawet takiej, która uwzględnia nazwisko odkrywcy! W języku polskim mówimy mianowicie o zasadzie szufladkowej Dirichleta, a analogią jest podobno układanie skarpetek w szufladach. W angielskim z kolei używa się do tego… gołębi, a reguła jest znana jako pigeonhole principle – w dosłownym tłumaczeniu ‘zasada gołębiej dziury’.

Co jest niezwykłego w tak prostej obserwacji? Otóż jej wyjątkową cechą jest to, że stosuje się do wręcz nieograniczonego spektrum zjawisk różnego typu i wielu dziedzin, pozwalając udowadniać prawdziwość mnóstwa czasami zaskakujących stwierdzeń. Ich ciężar gatunkowy może się wahać od zabawnych, aczkolwiek pouczających, aż do zupełnie kluczowych dla niektórych dziedzin nauki. Oto przykłady:

  • Struktury danych oparte na haszowaniu zawsze muszą uwzględniać metody rozwiązywania kolizji. Ten znany fakt jest konsekwencją tego, iż liczba możliwych kluczy n przekracza (zazwyczaj znacznie) rozmiar pojemnika m, więc nawet przy najlepszej funkcji haszującej znajdą się takie dwa klucze, które zostaną przyporządkowane w to samo miejsce.
  • W pudełku jest 5 czarnych i 5 białych kul. Ile co najmniej kul musimy wybrać, żeby wśród nich znalazła się para tego samego koloru? Nie, wcale nie sześć – wystarczą trzy. Skoro mamy m = 2 kolorów, to już n = 3 kul gwarantuje, że będzie wśród nich para tego samego koloru.
  • Wśród wszystkich obecnie żyjących ludzi co najmniej dwójka wypowiedziała w ciągu swojego dotychczasowego życia identyczną liczbę słów. Nawet gdyby ktoś paplał 24 godziny na dobę z prędkością jednego słowa na sekundę, w ciągu jednej doby nie powie ich więcej niż 86.400. Najstarszy człowiek miał/ma około 130 lat, w związku z czym ilość możliwych wartości dla wielkości ‘liczba wypowiedzianych słów’ to ok. 86.400 * 365,25 * 130, czyli nieco ponad 4 miliardy. Ponieważ na Ziemi żyje obecnie ponad 6 miliardów ludzi – czyli więcej – na mocy zasady szufladkowej wnioskujemy, że musi istnieć co najmniej dwójka, która wypowiedziała w ciągu życia tę samą liczbę słów.
  • Jeśli algorytm kompresji bezstratnej rzeczywiście kompresuje (tj. zmniejsza objętość przynajmniej jednego pliku), to istnieje dla niego taki plik, który po “kompresji” będzie większy niż przed nią. Innymi słowy, nie ma doskonałych metod kompresji i nawet rosyjscy hakerzy nic na to nie poradzą :) Jest to bowiem bezpośrednia konsekwencja zasady ‘gołębiej dziury’.
    Powiedzmy, że najmniejszy plik P faktycznie kompresowany (tj. zmniejszany) przez nas algorytm ma początkowy rozmiar n, zaś po kompresji jest to już k, gdzie k < n. Istnieje skończona liczba plików rozmiaru k, którą oznaczymy N_k. (Jeśli na przykład jest to rozmiar w bajtach, to N = 256^k). Z założenia żaden z tych plików nie zmienia rozmiaru podczas kompresji, więc ich wielkość po skompresowaniu to również k. Oznacza to jednak, że tych N_k plików to wyniki kompresji N_k+1 plików, bo także P (większy niż k) jest kompresowany do jednego z tych plików. Ponieważ N_k + 1 > N_k, na mocy zasady szufladkowej istnieją dwa takie pliki – mianowicie P i jakiś plik rozmiaru k – które są kompresowane do tego samego pliku wyjściowego. Jeśli tak jest, to algorytm nie może być bezstratny – czyli mamy sprzeczność, Q.E.D. :-)

Całkiem sprytne, nieprawdaż? Jak na zupełnie “oczywistą oczywistość”, zasada szufladkowa wykazuje niezwykłą użyteczność w wielu zaskakujących momentach. Warto o niej pamiętać nie tylko podczas poszukiwań pary pasujących skarpetek :)

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

C# i zbiory

2010-06-08 16:39

Dłuższy czas temu popełniłem notkę o tym, że w C# (lub szerzej: w .NET) brakuje pewnych struktur danych, które niekiedy bywają przydatne. Jedną z nich jest bardzo prosty rodzaj pojemnika: zbiór.
Zbiory (w programowaniu) to kontenery, które przechowują elementy niepowtarzające się i umożliwiają szybkie sprawdzenie, czy jakaś wartość do danego zbioru należy. ‘Szybkie’ oznacza tu złożoność logarytmiczną (względem rozmiaru pojemnika) lub lepszą. Podstawowa różnica w stosunku do zbiorów matematycznych jest natomiast taka, iż te drugie mogą zawierać elementy różnych rodzajów, podczas struktura danych o tej nazwie przechowuje obiekty jednego typu.

W C++ zbiór implementuje STL-owa klasa std::set. W C# z kolei – jak napisałem na początku – jej odpowiednika nie ma. Takowy trzeba by dopiero napisać, co w przypadku tego języka jest raczej zaskakujące :) Tym niemniej da się to zrobić w miarę prosto, używając do tego… klasy Dictionary. Pomysł polega na tym, żeby całkowicie zignorować tę jej część, która kluczom w słowniku pozwala przypisywać wartości. Zamiast tego interesują nas wyłącznie same klucze jako elementy naszego zbioru. Zarys klasy opartej o tę ideę może wyglądać choćby tak:

  1. public class Set<T> : ICollection<T>, IEnumerable<T>
  2. {
  3.     private Dictionary<T, object> set;
  4.  
  5.     public Set() { set = new Dictionary<T, object>(); }
  6.  
  7.     public void Add(T item) { set.Add(item, null); }
  8.     public void Clear() { set.Clear(); }
  9.     public bool Contains(T item) { return set.ContainsKey(item); }
  10.     public void CopyTo(T[] array, int arrayIndex)
  11.         { set.Keys.CopyTo(array, arrayIndex); }
  12.     public int Count    { get { return set.Count; } }
  13.     public bool IsReadOnly  { get { return false; } }
  14.     public bool Remove(T item) { return set.Remove(item); }
  15.  
  16.     public IEnumerator<T> GetEnumerator()
  17.         { return set.Keys.GetEnumerator(); }
  18.     IEnumerator IEnumerable.GetEnumerator()
  19.         { return (set.Keys as IEnumerable).GetEnumerator(); }
  20. }

Skorzystanie ze słownika (klasy Dictionary) sprawia, że kluczowa operacja Contains (sprawdzenie przynależności) jest bardzo szybka. MSDN podaje, że wydajność odpowiadającej jej operacji słownikowej ContainsKey “zbliża się do O(1)”. Mamy tu też oczywiście niezbędne metody do dodawania i usuwania elementów, a także kilka innych związanych z implementowanymi interfejsami ICollection i IEnumerable, które pozwalają na przykład na iterowanie po zbiorze pętlą foreach.
Można naturalnie jeszcze ulepszyć tę klasę, dodając nowe konstruktory, metody i implementując następne interfejsy (na przykład ISerializable). Nie jest to trudne, bo polega głównie na wywoływaniu odpowiadających metod obiektu Dictionary lub jego kolekcji kluczy. Dopiero operacje matematyczne – jak suma i iloczyn zbiorów – wymagałoby nieco większej ilości kodu.
Ale przecież dla chcącego nic trudnego :)

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

Szablony klas i funkcje zaprzyjaźnione

2010-06-04 21:18

Chcę dzisiaj napisać o pewnego rodzaju “ciekawostce”, związanej z dwoma pojęciami z tytułu notki, na którą natknąłem się jakiś czas temu. Po zredukowaniu nadmiaru szczegółów można przyjąć, że rzecz dotyczy sytuacji, w której mamy szablon klasy oraz zadeklarowaną wewnątrz niego funkcję zaprzyjaźnioną – na przykład przeciążony operator strumieniowy <<:

  1. #include <iostream>
  2.  
  3. template <typename T> class Foo
  4. {
  5.     private: T x;
  6.     public:
  7.         explicit Foo(T _x) : x(_x) { }
  8.         friend std::ostream& operator << (std::ostream& out, const Foo<T>& foo);
  9. };

Funkcja jest zadeklarowana, lecz nie jest od razu zaimplementowana wewnątrz bloku class. Zamiast tego umieszczamy jej kod osobno:

  1. template <typename T> std::ostream& operator << (std::ostream& out, const Foo<T>& foo)
  2.     { return out << foo.x; }&#91;/cpp]
  3. co jest może trochę osobliwe, ale wydaje się poprawne. No właśnie; wydaje się. Próba użycia tak zdefiniowana operatora:
  4. &#91;cpp]Foo<int> foo(42);
  5. std::cout << foo << std::endl; // bum[/cpp]
  6. nie przeszkadza wprawdzie kompilatorowi, jednak linker ma poważne zastrzeżenia. Okazuje się, że funkcja <code>operator &lt;&lt;</code> w wymaganej postaci nie jest zdefiniowana - czyli mamy zwyczajny <em>unresolved external</em>, aczkolwiek o niecodziennych przyczynach.
  7.  
  8. Przyznać muszę teraz, że nie jestem do końca pewien co do ich natury, jako że konsultacja z zaufanymi źródłami (w postaci książki <a href="http://helion.pl/ksiazki/c_szablony_vademecum_profesjonalisty_david_vandevoorde_nicolai_m_josuttis,cpszav.htm"><em>C++. Szablony</em></a> panów Vandevoorde'a i Josuttisa) nie dała jednoznacznej odpowiedzi, co tutaj tak naprawdę się dzieje. Przypuszczam, że ma tu miejsce pewien dziwnej interakcji między konkretyzacją szablonu klasy a deklaracją <code>friend</code>, która przy okazji jest też deklaracją funkcji <code>operator &lt;&lt;</code>. Mimo że nie stanowi ona części szablonu, owej konkretyzacji <strong>podlega</strong>, czego skutkiem jest stworzenie <strong>zwykłej</strong> (nieszablonowej) deklaracji:
  9. [cpp]std::ostream& operator << (std::ostream& out, const Foo<int>& foo);

Nie ma więc ona nic wspólnego z szablonem funkcji operator <<, który jest zdefiniowany później! W rzeczywistości wygląda zresztą tak, że ów szablon jest całkowicie pominięty przy kompilacji jako niewykorzystywany; to zapewne z powodu reguł przeciążania funkcji w C++, nakazujących najwyraźniej wybranie raczej funkcji nieszablonowej (o powyższej deklaracji) zamiast szablonu funkcji. A że przypadkiem funkcja ta nie ma implementacji… Cóż, kompilator nie ma obowiązku się o to martwić :)

Tyle moja teoria. Niezależnie od tego, czy jest ona prawdziwa czy nie, dobrze byłoby wiedzieć, jak z problemu wybrnąć. Naturalnym wyjściem jest przenieść kod funkcji tam, gdzie jego miejsce – czyli z deklaracji funkcji w klasie (opatrzonej słówkiem friend) uczynić też jej definicję. Faktycznie, to działa; najwyraźniej powstaje wtedy nieszablonowa funkcja o nagłówku danym wyżej, razem ze swoją implementacją – czyli wszystko jest w porządku.
Drugie rozwiązanie to opatrzenie deklaracji friend klauzulą template:

  1. template <typename T>
  2.     friend std::ostream& operator << (std::ostream& out, const Foo<T>& foo);

W tej wersji – jak sądzę – następuje zaprzyjaźnienie każdej specjalizacji klasy Foo z odpowiadającą jej specjalizacją metody operator <<. Oba szablony podlegają niezależnym konkretyzacjom, ale linker potrafi prawidłowo rozwiązać relację między nimi.

Uff, trochę to skomplikowane. Jak widać, z zaprzyjaźnianiem bywają problemy – i to nie tylko dlatego, iż “In C++ friends touch each other’s private parts.” ;) Dziwne efekty związane z szablonami i konkretyzacją potrafią bowiem skonfudować nawet doświadczonych programistów.

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

Dwukierunkowe funkcje i obiekty proxy

2010-06-01 16:07

Jedną z pierwszych rzeczy poznawanych w trakcie nauki programowania jest sposób działania funkcji. Mam tu na myśli zupełnie elementarny fakt, iż funkcje przyjmują zero lub więcej argumentów na wejściu i produkują co najwyżej jeden rezultat na wyjściu. Niezachwiana pewność w tę własność funkcji może być potem mocno nadwątlona, jeśli poznamy języki o bardziej egzotycznym sposobie działania, jak na przykład Prolog; tam wszystkie parametry funkcji mogą przekazywać dane w obu kierunkach, dzięki czemu np. za dzielenie i łączenie ciągów lub list odpowiada jedna i ta sam funkcja.
Nie trzeba jednak uciekać tak daleko od starego dobrego programowania imperatywnego, żeby zaobserwować odstępstwa od reguły. Parametry mogą przecież służyć do zwracania wartości – dzieje się to przy pomocy słów kluczowych ref/out w C# lub zwykłych wskaźników/referencji w C++. Często zdarza się wtedy, że “normalny” rezultat zwracany przez funkcję służy jedynie przekazaniu informacji o ewentualnym błędzie.

Rzadsza sytuacja polega na tym, że wynik funkcji staje się jej argumentem wejściowym. Technicznie nie jest nawet możliwe, ale można tak traktować sytuacje, gdy rezultatem jest l-wartość (l-value), tj. obiekt, do którego można przypisywać:

  1. vector<int> v(10);
  2. v.at(5) = 25;

Typowo jest to referencja (tutaj int&), a powyższa konstrukcja – poprzez swojej podobieństwo do indeksowania zwykłej tablicy – nie należy do wielce zaskakujących. Co jednak można powiedzieć o tej:

  1. Dim S as String = "Ala ma kota"
  2. Mid(S, 4, 2) = "nie ma" ' Ala nie ma kota

poza widocznym od razu faktem, że pochodzi ona z języka, którego rozwlekłość składni przyprawia o niestrawność? :) Mianowicie tutaj nie ma już oczywistych przesłanek co do tego, czym jest lewa strona. Widać bowiem, że możemy jej przypisać dłuższy podciąg niż oryginalny i zostanie on mimo wszystko poprawnie zastąpiony – nie jest to więc żaden prosty “wyrób wskaźnikopodobny”. Może więc to po prostu feature akurat tego języka, którego nie da się zreplikować?…

Odpowiedź jest – jak można się domyślać, skoro o tym piszę – oczywiście negatywna :) Ażeby podobny efekt osiągnąć w C++, konieczne jest jednak zastosowanie techniki znanej jako obiekt pośredniczącyproxy. Powinien on zachowywać się jak zwykły rezultat funkcji, ale w razie potrzeby dawać również możliwość przypisywania do siebie. Naturalnie efekt takiego działania jest specyficzny dla konkretnej funkcji, która swoją drogą może być często zredukowana do samego tworzenia obiektu proxy:

  1. inline Mid_Proxy Mid(std::string& s, size_t off, size_t len)
  2.     { return Mid_Proxy(s, off, len); }

Minimum, jakie rzeczony obiekt musi zapewniać, to operator przypisania oraz jakiś operator rzutowania, który pozwoli na “wyciągnięcie” rezultatu funkcji, gdy jest ona użyta w zwykły sposób:

  1. class Mid_Proxy
  2. {
  3.     private:
  4.         std::string& s;
  5.         size_t off, len;
  6.     public:
  7.         Mid_Proxy(std::string& _s, size_t _off, size_t _len)
  8.             : s(_s), off(_off), len(_len) { }
  9.  
  10.         void operator = (const std::string& str)
  11.             { s.replace (off, len, str); }
  12.         operator std::string () const
  13.             { return s.substr(off, len); }
  14. };

Widzimy tutaj, że przy takim proxy nasza funkcja Mid użyta w zwykły sposób zachowuje się jak metoda substr. Gdy jednak umieścimy jej wywołanie po lewej stronie przypisania, zadziała metoda replace, służąca do zastępowania podciągu innym. W sumie więc poniższy kod:

  1. std::string s("Ala ma kota");
  2. Mid(s, 4, 2) = "nie ma";

będzie działał analogicznie do prezentowanego wyżej fragmentu w języku Visual Basic.

Opisana tu sztuczka nie jest oczywiście doskonała. Do pełni możliwości potrzebna jest jeszcze wersja read-only funkcji Mid, przyjmująca stałą referencję do stringa i wywołująca substr bezpośrednio, z pominięciem obiektu proxy. To jednak da się łatwo zauważyć.
Mniej widoczny jest natomiast fakt, że obiekt proxy, dodając kolejną warstwę niejawnych konwersji (tutaj: z siebie na string) może spowodować kłopoty tam, gdzie jedna niestandardowa konwersja jest już wykorzystywana. Dobry przykład to interakcja ze strumieniem:
std::cout << Mid(s, 0, 3) << std::endl;[/cpp] która nie powiedzie, bo operator strumienowy << nie posiada wersji dla lewego prawego argumentu będącego std::stringiem, a jedynie dla const char* (co swoją drogą jest dość dziwne). Rozwiązaniem jest napisanie takowego dla samego obiektu proxy.

Zależności między projektami w Visual Studio

2010-05-29 0:40

Zdarza się, że pracuje nad złożonym systemem, na który składa się kilka osobnych projektów. IDE znają dobrze takie przypadki i potrafią je obsługiwać – stąd chociażby pojęcie solution (dawniej workspace) w Visual Studio. Dla pojedynczych aplikacji i bibliotek wydaje się ono zbędne, jednak staje się nieodzowne wtedy, gdy nasze projekty zależą od siebie.

Typowa sytuacja to wspólna biblioteka (framework, engine czy co jeszcze kto woli) rozwijana razem z programami, które z niej korzystają. (W najprostszym przypadku to może być po prostu jakaś aplikacja testowa). Wówczas pojawiają się zależności między projektami na etapie ich budowania: wynik szeroko pojętej “kompilacji” jednego jest wejściem do procesu budowania innego. Jeśli nie poświęcimy temu faktowi należytej uwagi, to mogą nas czekać kłopoty. W najlepszym razie jest to konieczność wciskania F7 (Build Solution) więcej niż raz, aż do zbudowania wszystkich projektów. W gorszym – uruchamianie (i debugowanie!) aplikacji korzystającej z nieaktualnej, bo nieprzekompilowanej wersji biblioteki.

Zależności między projektami w procesie budowania da się na szczęście określić. W Visual Studio służy do tego opcja Project Dependencies z menu – a jakże – Project. Możemy w niej określić dla każdego projektu, z jakimi innymi projektami z tego samego solution jest on powiązany, czyli które z nich powinny być już wcześniej od niego zbudowane. Na podstawie tak podanej sieci zależności da się następnie określić właściwą kolejności “kompilacji” dla wszystkich projektów w danym solution. VS oczywiście to czyni, używając do tego zapewne sortowania topologicznego w analogiczny sposób jak dla kompilacji jednego projektu składającego się z wielu plików.

Tags: , , ,
Author: Xion, posted under Applications, Programming » 1 comment

Typ std::bitset i flagi logiczne

2010-05-27 15:52

Napiszę dzisiaj o nierzadko zapominanej klasie z biblioteki standardowej C++, czyli o tytułowym zbiorze bitówstd::bitset. Działania na pojedynczych bitach nie są raczej czymś, co wykonujemy codziennie, ale zdarzają się sytuacje, gdy stanowią one najrozsądniejsze rozwiązanie w danej chwili. Takim typowym przypadkiem jest zbiór flag logicznych: kilku opcji działających jak przełączniki, których możliwą wartością jest tylko true albo false. Wszelkie API doskonale zna takie konstrukcje, czego przykładem może być metoda Clear urządzenia DirectX:

  1. dev->Clear (0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
  2.     D3DCOLOR_XRGB(0, 255, 0), 1.0f, 0);

D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER jest właśnie kombinacją flag bitowych, a dwie występujące tu stałe zostały odpowiednio zdefiniowane, by dało się je łączyć operatorem sumy bitowej |.

Używając std::bitset możemy nieco uprościć obsługę takich opcji. Zamiast deklarowania specjalnych wartości, możemy użyć zwykłego enuma, np.:

  1. enum FontStyle { Bold, Italic, Underline, FontStylesCount };

bitset jest wtedy łatwy w użyciu:

  1. void FormatText(const std::string& text,
  2.     const std::bitset<FontStylesCount>& style)
  3. {
  4.     if (style.any())  // czy jakikolwiek bit został ustawiony
  5.     {
  6.         // sprawdzenie poszczególnych flag
  7.         if (style[Bold]) { /* ... */ }
  8.         if (style[Italic]) { /* ... */ }
  9.         if (style[Underline]) { /* ... */ }
  10.     }
  11. }

Zachowana jest przy tym “kompatybilność wsteczna” w stosunku do ręcznie definiowanych stałych, jeśli chcielibyśmy z nich korzystać:

  1. #define OPT(x) (1<<(x))
  2. FormatText ("Hello World!", OPT(Bold) | OPT(Italic));&#91;/cpp]
  3. lub jeżeli jakieś zewnętrzne API, w którym programujemy, korzysta z wartości bitowych zapisywanych w typach liczbowych:
  4. &#91;cpp]std::bitset<32> lp = lParam;
  5. lp.set(30).reset(31);
  6. SendMessage (hWnd, WM_KEYDOWN, VK_UP, lp.to_ulong());

Na koniec wspomnę tylko, że aby korzystać z klasy bitset do tych lub innych zastosowań, należy wpierw dołączyć standardowy nagłówek o nazwie… bitset :)

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

Standardy kodowania jako choroba dziedziczna

2010-05-25 18:38

Ponieważ programowaniem zajmuję się już od dość długiego czasu, mam w tym nieco doświadczenia praktycznego. Przez ten czas zdążyłem też zmienić część swoich poglądów na niektóre rzeczy z tym związane. Mówiąc trochę górnolotnie: stałem się bogatszy o wiedzę z życia wziętą :)
Jedną z takich nauczek życiowych jest to, by nie ufać zbytnio standardom, zaleceniom i innym tzw. dobrym praktykom podpowiadających (a czasem wręcz nakazujących), jak powinien wyglądać pisany przez nas kod. Dotyczy to na przykład instrukcji rzekomo złych “z natury” i innych tego rodzaju przesądów. Tak, uważam określenie ‘przesądy’ za uprawnione w wielu przypadkach.

Nie jestem ponadto odosobniony w tych poglądach – ba, wśród swoich kolegów po fachu mógłbym wskazać wielu kilku, którzy są pod tym względem nawet bardziej… radykalni :) Jak już jednak wspomniałem na początku, nie zawsze tak było. Kiedyś miałem bardziej rygorystyczne i pryncypialne podejście do kwestii tego, jak kodować należy lub nie należy. Skąd brałem dla niego uzasadnienie, jeśli nie z praktyki, której wtedy zbyt wiele jeszcze nie miałem?…
Ano właśnie – tu dochodzimy do sedna. Przekonanie to brało się głównie – jeśli nie wyłącznie – ze wszelkiego typu materiałów pomocniczych do nauki programowania: książek, kursów, artykułów, czasem dokumentacji. Choćby nie wiem jak techniczne i merytoryczne były te teksty, w każdym – co do jednego, z moim skromnym dziełem włącznie – znajdą się rady, zalecenia, wskazówki i inne “metainformacje” odnośnie tego, jak programować należy – a nie tylko o tym, jak można. Czy ktoś widział w kursie programowania opis instrukcji goto (że pozwolę sobie użyć najbardziej typowego przykładu), który nie zawierał chociaż śladowej wzmianki o tym, iż.. no, w zasadzie to nie należy jej używać?… …Tak myślałem ;-)

Nasuwającą się od razu repliką w obronie autorów jest sugestia, że w sumie to przecież całkiem dobrze, iż uczą oni nowicjuszy tego, co sami musieli wcześniej wypracować przez lata. Nie wątpię wręcz, że taka intencja – oszczędzenia początkującym trudności – rzeczywiście autorom przyświeca. Dodają oni do pisanych przez siebie tekstów, kursów, tutoriali, itp. liczne wskazówki, te wszystkie dos and don’ts właśnie dlatego, iż wierzą w to, że w ten sposób pomogą swoim czytelnikom od razu, już na starcie kodować w sposób przejrzysty, efektywny, elastyczny, elegancki, “łatwo konserwowalny”, i tak dalej.


“Pamiętaj synku, by się zabezpieczać:
dawaj const, gdzie tylko możesz.”
(Źródło obrazka)

Chwalebne intencje – nie da się ukryć. Ale nietrudno jest wskazać przynajmniej dwa błędy takiego rozumowania. Pierwszym jest założenie, że każdą wiedzę i każdą umiejętność da się przekazać innym – czyli że w ogóle istnieje “droga na skróty”, niejako omijająca zdobywanie doświadczenia praktycznego w danej dziedzinie (w tym przypadku w programowaniu). Żeby stwierdzić, że to jednak nieprawda, wystarczy zastanowić się, czy ukończenie podstawowego kursu szachów u arcymistrza da nam od razu ELO 2500. Niestety w rzeczywistości dojście do takiego poziomu wymaga dziesiątków tysięcy rozegranych partii – i nic się na to nie poradzi.
Drugim błędem jest przyjmowanie, że przekazywane mądrości są faktycznie odpowiednie dla odbiorców. Nie, nie chcę wcale sugerować, że ktoś może mieć traumę z powodu wczesnych doświadczeń z hermetyzacją czy wzorcem fabryki ;) Raczej sugeruję, że przywiązując zbyt wielką wagę do przekazywanych zaleceń, początkujący mogą stracić wiele czasu na dopasowywanie swoich pierwszych programów do ich wysoko zawieszonej poprzeczki – najczęściej zupełnie niepotrzebnie. Gra w zgadywanie liczby, kółko i krzyżyk czy tetris nie potrzebują zwykle skomplikowanej, wewnętrznej architektury z dokładnie zaprojektowanymi relacjami między klasami i starannym oddzieleniem interfejsu od implementacji. Prosi się to bowiem o przytoczenie znanego powiedzenia na temat artylerii i pewnego gatunku insekta :)

Tak oto okazuje się więc, że zalecenia i rady tudzież – szumnie nazywane – standardy kodowania są “dziedziczone” przez początkujących programistów od autorów książek, kursów i tutoriali, którzy je tam umieszczają pomiędzy objaśnieniami kolejnych konstrukcji językowych czy elementów API. A że ponadto, jak opisałem wyżej, standardy te zbyt wcześnie poznane mogą być nie tyle nawet zbędne, co wręcz szkodliwe, pozwalam sobie nazwać je ‘chorobami dziedzicznymi’. Nic tak bowiem nie trafia do wyobraźni jak obrazowe porównanie ;>
Zakończyć chcę jednak optymistycznym akcentem. Otóż choroby te są jak najbardziej uleczalne. Terapia jest też nieskomplikowana i – jak sądzę – skuteczna. Trzeba po prostu kodować, kodować i jeszcze raz kodować :)

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


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