Nehoruji primo pro OOP, jen jsem chtel ukazat, ze v OOP jazyku clovek muze programovat, aniz by zpocatku musel znat tu furu netrivialnich konceptu (mimochodem, objekty = data + metody a posilani zprav, coz jsou skutecne asi jedine podstatne koncepty OOP, prijdou pomerne intuitivni). Vas "algoritmizujici" priklad na zrcadlovy obraz cisla by ve smalltalku doslovne vypadal takto:
| cislo obraz |
cislo := 12345.
obraz := 0.
[cislo > 0] whileTrue: [
obraz := obraz * 10 + (cislo \\ 10)).
cislo := cislo // 10.
].
obraz.
Mozna je to profesni slepota, ale prijde mi, ze je pomerne dobre citelny i pro cloveka co smalltalk vubec nezna.
Jinak samozrejme netvrdim, ze OOP je jedina spravna cesta. Ale myslim si, ze jazyk C, Pascal apod. NEJSOU dobre jazyky na vyuku algoritmizace.
Priklad: Jako zadani jakehosi algoritmizacniho prikladu je implementace stromu, vcetne metod (funkci, procedur, podle jazyka), ktere strom prochazeji preorder, inorder, postorder. Aby tyto byly zaroven uzitecne, a zaroven obecne, musi byt (jinak to podle me opravdu nejde) naimplementovany tak, aby braly jako parametr kus vykonavatelneho kodu, ktery se pro kazdy prochazeny element zavola s timto elementem jako parametrem. Nyni mame podle schopnosti jazyka nasledujici moznosti:
1)
Jazyky, ktere to podporuji (Smalltalk, Ruby, Lisp, Python, Scala, C#) maji syntaxi pro anonymni (lambda) funkce, muzeme tedy primo psat napr: aTree inorderDo: [:eachElement | eachElement echo]
2)
V OOP jazycich udelame tridu, ktera implementuje pozadovanym zpusobem napr. metodu EchoExecutor::executeOnElement(e: TreeElement)
{
echo(e)
}
, a volame
aTree.inOrderDo(new EchoExecutor)
3)
Konecne v jazyce C, kde nemame ani jedno, ani druhe, nam zbyva ukazatel na funkci, Ten ma ovsem dve nevyhody:
a) "Ukazatel na funkci, ktera bere jako parametr ukazatel na TreeElement" je podle me nejobtizneji uchopitelny koncept ze vsech tri
b) Predstavme si, ze by prvky stromu byla cisla, a my chceme jejich soucet. V tomto pripade je nutne nekde ukladat mezisoucet. V pripade lambda funkci to neni problem, protoze mohou referencovat promennou z blizkeho kontextu, napr.
| sum |
sum := 0.
aTree inOrderDo: [:eachElement | sum := sum + eachElement]
V pripade OOP jazyku mame situaci snad jeste jednodussi, protoze staci do EchoExecutora pridat instancni promennou. Ovsem v pripade jazyka C a ukazatelu na funkce mame jedinou moznost, pouzit statickou (globalni v souboru) promennou. Ze jsou tyto spatne, vi kazdy slusny programator. Kdyz nic jineho, nas kod nyni neni thread-safe. Samozrejme, ze jsou cesty, jak tento problem obejit, myslim ze asi vetsina C-Ckaru by vycouvala tak, ze by funkce, ktera se vykonava nad elementem stromu, brala jeste druhy parametr, nejake void *data (chceme preci byt obecni a neomezovat se jenom na scitacky), do ktereho by bylo mozne ukladat mezistavy (pozn: to je presne pripad napr. knihovni funkce strtok vs. strtok_r). V pripade souctu by tam byl int *sum, a v tele funkce by se pretypovavalo - cimz jsme se efektivne zbavili typove kontroly, kterou za nas prekladac delal, a zavedli koncepty, ktere spolehlive maloktery zacatecnik rozdycha. A proc? Abychom simulovali, to co za nas delaji OOP jazyky samy - svazali data a kod do jednoho zapouzdreneho objektu. Ovsem zatimco OOP jazyky to za nas delaji vsude, rady a efektivne, my to delame ad-hoc, kod je tezko srozumitelny, a ztracime typovou kontrolu.
Zaver
Tvrdim, ze na vyuku algoritmizace je potreba jazyk, ktery snadno podporuje kod-jako-data. Ja znam pouze blokove uzavery a OOP instance v objektech (v LISPu je ovsem veskery kod zaroven data, ale nevim, jestli se to da povazovat za blokovy uzaver). Toto je nezbytne k budovani algoritmickych stavebnich bloku. Jake jsou dusledky? U ktereho kodu rozhodnete rychleji, co dela?
int coAsiDelaTahleFunkce(const int pole[], const int velikostPole)
{
int i;
for (i = 0; i < velikostPole; i++)
if (odd(pole[i]))
return true;
return false
}
vs
coAsiDelaTahleFunkce: pole
^pole anySatisfy: [:element | element isOdd]
Osobne nepovazuji za podvod to, ze v C-ckove verzi je zaroven implementovan algoritmus anySatisfy, protoze v praxi ho kazdy C-ckovy programator proste naimplementuje znovu a znovu. Jako jedinou alternativu ma totiz udelat si funkci,
int isOdd(void *arg)
{
return odd(*((int *)arg))
} /* (doufam, ze jsem tam ty hvezdicky dal spravne, melo jit o pretypovani na int* a naslednou dereferenci) */
, a to proste nikdo delat nebude. Pritom smyslem algoritmizace neni vsechny, i ty nejtrivialnejsi konstrukty, prepisovat pomoci assembler-like primitiv jako jsou for-smycky, ale vybudouvat rozumne stavebni bloky prislusne domene algoritmu, a z tech pak srozumitelne algoritmus vybudovat. V zaplave smycek clovek nepozna, co je allSatisfy, co anySatisfy, co je map, co je reduce, a tak, misto aby se vzdelaval v tom, jake konstrukty se pro tvorbu algoritmu hodi, cvici se jen ve for/while/break/continue/ukazatel na funkci, co ma jako parametr ukazatel na funkci vracejici ukazatel na pole charu a jak na nej pretypovat z void* idiomech, ktere jsou stejne neprenositelne do jinych jazyku.
Takze jeste jednou. Netvrdim ze OOP je jedina moznost jak lepe programovat. Jazyky podporujici funkce vyssiho radu (lambda funkce) jsou dalsi. Nicmene, jazyky ktere neumoznuji snadno reprezentovat kod jako data a manipulovat s nim, jsou proste velice omezujici a na vyuku programovani se nehodi. Pokud si myslite, ze jsou obtizne, vyzaduji kupu novych a tezko pochopitelnych pojmu, vezte, ze zadny z nich neni ani zdaleka tak komplikovany jako pretypovani void* ukazatele na ukazatel na funkci, ktera zpracovava pole stringu. Naopak, jak blokove uzavery (closures/lambda funkce), tak objektove programovani (tedy zasilani zprav a zapouzdreni kodu a dat do objektu) jsou velice intuitivni, pokud nejsou ovsem zkomplikovany kupou dalsich, s OOP vubec nebo jen vzdalene souvisejicich pojmu, jako je tomu napr. v jazyce C++.
Doufam ze tento post byl konstruktivnejsi nez muj predchozi :-)