čtvrtek, května 26, 2005

Vrstvení

Začněme jednoduchým programem:

#include <iostream>
using namespace std;
int main()
{
 //Part I
 double a[10];
 a[0] = 0.1;
 for(int i = 1; i != 10; i++) {
  a[i] = a[i - 1] + 0.1;
  cout << a[i] << ", ";
 } 
 
 //Part II
 for(int i = 0; i < 100; i++)
   a[9] *= a[9];
   
 cout << a[9]; 
 return 0;
}
  Jak všichni správně předpokládají, výstupem části Part I je následující řada čísel:
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1
  Skvěle. Teď pokračujme ve vykonávání části Part II. Výsledkem bude:
0
  Ale copak? Že by někde nastala nějaká chyba? Raději spusťme program znovu, zastavme ho před Part II a nahlédněme do debuggeru:
a[0] 0.10000000000000001
a[1] 0.20000000000000001
a[2] 0.30000000000000004
a[3] 0.40000000000000002
a[4] 0.50000000000000000
a[5] 0.59999999999999998
a[6] 0.69999999999999996
a[7] 0.79999999999999993
a[8] 0.89999999999999991
a[9] 0.99999999999999989
  Aha! Tak tady je problém! Jak už od začátku vědí všichni, kteří se někdy zajímali o to, jak to vlastně je s čísly v počítačích, problém je ve skutečnosti, že počítače neukládají reálná čísla jako reálná čísla, ale jako koeficienty řady
a0 + a1×1/2 + a2×1/4 + a3×1/8 + ... + ai×1/2i
(kde ai je hodnota příslušného bitu), plus exponent.

Kam tahle okázalá demonstrace směřuje? K zamyšlení nad abstrakcemi a nad tím, že čím více jich na sebe navrstvíme, tím méně vidíme do toho, co se děje pod nimi.

Abstrakce vytváříme proto, abychom se odstínili od některých závislostí, které sebou vývoj zařízení a programů nese. Stvořili jsem abstrakci integrovaného obvodu, která nás nenutí zabývat se tím, jestli tranzistor bude otevřený nebo zavřený, nad ní jsme vybudovali abstrakci třeba lineárního regulátoru napětí, která nás zbavuje nutnosti starat se o stabilizaci napájení, nebo programovatelného logického pole, díky kterému se návrh logických obvodů zjednodušil do psaní rovnic v nějakém vývojovém prostředí.

V programování máme nad strojovým kódem assembler (který ovšem už zcela filtruje starosti o procesor a jeho skutečné vnitřní stavy), nad assemblerem programovací jazyky, které abstrahují třeba od používaného hardware v podobě mezivrstev definujících obecné výstupní zařízení nebo úložiště. V poslední době už si ve velkém užíváme i abstrakci garbage collectingu, který nás osvobodí od přemýšlení nad pamětí jako takovou a umožňuje vytvářet objekty jak se nám jen zamane.

Samozřejmě, že každá další vrstva znamená určitou ztrátu kontroly, takže při programování logických polí se musíme přizpůsobovat struktuře makrocellů, při psaní aplikace v .NET zase nemáme jistotu kdy se pamět obsazená objektem opravdu uvolní.

Ovšem výhoda abstrakcí je obrovská: protože se bez určité míry abstrakce a fragmentace libovolného složitějšího projektu neobejdeme, umožňují standardní abstrakce soustředit se na skutečný problém, a nezabývat se znovu a znovu oním legendárním vymýšlením kola, a zrychlují tak vývoj čehokoliv.

Na druhou stranu: každá další vrstva přidává model chování, kterému je potřeba porozumět. Ať naprogramuji logické pole sebelíp, nikdy mě to nezbaví nutnosti řešit problém, jak přenesu megahertzové signály na druhou stranu plošného spoje, nebo jak vyfiltruji rušení, které mi bude zemí prolézat do analogové části zařízení. A stejně tak nemůžu předpokládat, že když v nějakém programovacím jazyce mění proměnná svůj význam v závislosti na kontextu, zbavím se problému s reprezentací čísel v FPU procesoru.

Správně by tedy s každou další abstrakcí měly naše znalosti o problému nakynout. Měli bychom chápat nejen jak se obvod nebo funkce tváří navenek, ale také to, co se děje uvnitř.

Je jasné, že jen málokdo je schopen pojmout tolik informací, aby v každém okamžiku rozuměl celému řetězci abstrakcí, které leží mezi úrovní na které pracuje, a tím úplně dole. Jenže tato neznalost se může leckdy stát krajně limitující, ne-li přímo osudnou.

Asi nikdo, kdo používá funkci strcat(), která nakonec jednoho řetězce v C přilepí řetězec jiný, se nezatěžuje tím, že by uvažoval o její výkonostní charakteristice. Ta se ovšem může brutálně projevit v případě, kdy k jednomu řetezci připojujeme mnoho jiných, protože řetězce v C jsou jen prostými poli, a ke zjištění jejich délky, je potřeba je projít celé, až k hodnotě 0, která je ukončuje.

Tyhle drobné předpoklady když něco dobře funguje v případě A, bude to jistě dobře fungovat i v případě B, jsou pak zdrojem různých nedomrlostí nebo problémů při uvádění aplikace do chodu.

Přemýšlím nad tím (v dozvuku na zamyšlení o bastlení), jestli existuje naděje, že někdy vznikne nějaká abstrakce, která nám ušetří práci, a bude přitom zcela průhledná. Jaksi přirozeně skepticky se domnívám že nikoliv, a že znalost souvislostí vždycky bude tím, co bude dobrého návrháře odlišovat od toho špatného.

Chci říct: Ať už se s abstrakcemi dostaneme jakkoliv daleko, na tom, co se děje tam dole, pod nimi, pořád zatraceně záleží.

2 komentáře:

Mormegil řekl(a)...

Amen. A těžko mi je nedoplnit odkaz na The Law of Leaking Abstractions.

BVer řekl(a)...

Abstrakce jsou v programování, jak bylo řečeno, užitečná věc, díky níž se obor vyvíjí dopředu. Dá se to s jistou dávkou zjednodušení srovnat s objektově orientovaným programováním: Zatímco to nám slibuje, že můžeme opakovaně využívat jednou napsané třídy (reusing code), abstrakce nám nabízejí k opakovanému použití dokonce celé vrstvy kódu, nazval bych to "reusing layers".

Abstrakci můžeme chápat jako jakési OOP na mnohem vyšší úrovni. A zde už můžeme cítit závan čehosi shnilého. Před několika lety velmi populární OOP je dnes občas kritizováno za to, že svůj slib o znovupoužitelnosti neplní doslova, a programátoři, kteří ještě nedávno nejraději dědili všechno od všeho a na stěnu nad monitor si pak zavěsili hierarchii tříd ve formátu A0, jsou dnes jaksi opatrnější. Objektový přístup se samozřejmě s výhodou používá prakticky všude, ale už nad ním nikdo nejásá -- je to běžná věc a má své konceptuální limity. Naopak se skrytě prosazují metodické síly jdoucí spíše "kolmo" ke směru, jímž se vydalo OOP: šablony, což jsou vlastně jen zobecněná makra (např. STL), nebo refactoring s použitím unit testů umožňující lehce modifikovat objektové třídy, aniž bychom je přitom rozbili (zobecněné asserty), případně nejrůznější rozšíření komunikace tříd (Java interfaces, Qt signal/slot).

Dále: Jak se praví v Mormegilem citovaném článku o "Leaking Abstractions": abstrakce šetří čas při práci, ale nešetří čas při učení.
Kdo dnes začíná programovat, má to dosti těžké: na jednu stranu by měl co nejrychleji ovládnout nějaký jazyk vyšší úrovně (nebo dokonce jen konkrétní nástroj RAD), aby mohl co nejdříve produkovat kód, na druhou stranu budou jeho výplody zákonitě pomalé a paměťově rozežrané. Teprve léty získaná zkušenost naučí programátora vidět skrz všechny vrstvy abstrakcí a psát -- bez ohledu na zvolený programovací jazyk -- na všech úrovních efektivně.

Takže se podobně jako vystřízlivění z OOP můžeme dočkat i nějaké revize pohledu na softwarové vrstvy a jejich nekonečné bujení: jednou ten domek z karet prostě může spadnout :)

Viz též
Leapfrogging Abstractions
.