Maturitní otázka č. 8: programovací jazyky

Jak dělíme programovací jazyky? Uveďte různé způsoby dělení programovacích jazyků a zástupce těchto jednotlivých skupin.

Programovacích jazyků je hrozná spousta. Způsobů, jak se obvykle dělí do skupin, je jen o trochu méně. Nemá smysl snažit se jmenovat všechny; pro obsáhlejší přehled se podívejte předchozí odkaz na Wikipedii.

Základní dělení je na jazyky imperativní a ostatní. Imperativní jazyky jsou ty normální a průmyslově nejčastěji používané: program se skládá z příkazů, které jeden po druhém zacházejí s nějakými daty. Typickým imperativním jazykem je Assembler, ale patří sem vážně skoro všechny: C, Java, Pascal, Python, Ruby a tak.

Skupina těch zbývajících jazyků je tedy nutně docela podivná. Převážně jsou to jazyky deklarativní: v nich program sestává spíše z pravidel, jak má vypadat výsledek, či z popisu různých vztahů mezi daty. Hezkým příkladem je dotazovací jazyk SQL, ve kterém jen stručně popisujeme, jaká data bychom z databáze chtěli, a vlastně vůbec netušíme, jakým způsobem je počítač vyloví. V mnoha ohledech podobný (ale podivnější) je Prolog: tomu nejdříve popíšeme, co by měla data splňovat a jaké vztahy mají platit, a pak jej jen požádáme, aby si vymyslel správný výsledek. Jiným užitečným příkladem je XSLT, systém šablon pro zpracovávání XML souborů.

Další skupina, jazyky funkcionální (často považované za podskupinu těch deklarativních), klade větší důraz na funkce než na samotná data. Jejich formální popis například požaduje, aby funkce volaná se stejnými parametry pokaždé navrátila týž výsledek a neměnila nic jiného. Ostatně, nemá co měnit - v poctivém funkcionálním jazyce nejsou žádné proměnné, které by vydržely delší dobu. Hezkou ukázkou je jazyk Lisp, ve kterém funkcím jen určujeme, jak se mají volat navzájem, potažmo jakým způsobem si to mají vymyslet. Za náznak funkcionálního programování se považuje, když nějakým způsobem vytváříme nové funkce za běhu programu: kdybychom to trochu přehnali, mohli bychom tím způsobem něco jako Lisp vytvořit.

Úplně odlišné dělení je podle typování proměnných. Jde o to, jestli má proměnná s určitým názvem trvale (staticky) určený svůj typ, anebo do ní můžeme podle potřeby dosadit i něco jiného a typ tím změnit. Staticky typované jazyky jsou mimo jiné C, C++, Pascal, Java (ale Javascript už ne) - pozná se to (většinou, ale ne nutně) tak, že při deklarování proměnné její typ výslovně píšeme. Například kód int i; jednoznačně definuje proměnnou i typu celé číslo.

Naproti tomu, dynamické typování většinou nevyžaduje, abychom typ přímo napsali, protože na něm tolik nezáleží. V takových jazycích můžeme do programu za sebe napsat třeba i=14; i=23.7; i="třicetpět"; a za typ proměnné i se bude postupně dosazovat celé číslo, desetinné číslo a řetězec znaků, podle příkazů. Extrémním přístupem je duck typing, kdy se typ proměnných nepokrytě snažíme ignorovat. Funkce například potom ani neřeší, jakého typu jsou parametry, které dostala - zkusí s nimi provést potřebnou operaci a teprve když se to opravdu nepodaří, vyhlásí chybu.

Za zmíňku stojí ještě jeden odlišný způsob dělení, který se hodně plete. Některé jazyky používají tzv. slabé typování - například PHP a Javascript - a snaží se před námi typ přímo skrývat. Do té míry, že ho podle potřeby změní, aby s ním šla provést určitá operace. PHP nemá problém, když chceme porovnávat číslo s řetězcem znaků, a když zkusíme dva řetězce sečíst, dokonce nám je převede na čísla. Když se tohle bez našeho vědomí neděje, hovoříme o silném typování.

Některé jazyky se obvykle označují jako kompilované a některé jako interpretované. Kompilovaný jazyk se nechá po přeložení (kompilaci) rovnou spustit procesorem, zatímco interpretovaný jazyk musí vždycky kousek po kousku číst a vykonávat nějaký jiný program (interpret).

Je to dost nespolehlivé vymezení. Například C či Assembler považujeme za kompilované jazyky, ale přesto se nám občas hodí je spouštět příkaz po příkazu a sledovat, co přesně se v počítači děje. Naopak, spoustu moderních jazyků považujeme za interpretované - Java, Javascript, PHP, Python, Ruby a tak - ale nikdo nám nebrání do nich nějak mazaně zabudovat program, který je bude vykonávat a výsledek předhodit rovnou procesoru. To nám může často pomoci k výraznému zisku rychlosti, a doopravdy se to proto dělá.

Zajímavý přístup je kompilace just-in-time, kdy program převádíme na spustitelný binární kód až na poslední chvíli a třeba i po částech. Takovéhle fígle jsou dobré třeba pro Javascript ve webových prohlížečích.

Správa paměti v programovacích jazycích – popište různé přístupy programovacích jazyků při alokaci a dealokaci proměnných.

S jedním velmi jednoduchým způsobem správy paměti se setkáváme třeba v Pascalu (a v trochu volnější podobě v C). Když chceme v nějaké funkci použít nějakou proměnnou, napíšeme na začátek, že tu proměnnou určité velikosti budeme potřebovat. Takováhle proměnná zjevně nemá svoje místo v paměti napevno určené, protože funkce se může volat rekurzivně a pokaždé je potřeba přichystat další a další proměnnou.

Ve skutečnosti dostane program jakýsi kus paměti pevné velikosti (zásobník neboli stack), na který si proměnné podle potřeby skládá. Dokud to funguje, je to velmi pohodlný a jednoduchý způsob, jak s pamětí zacházet. Když potřebujeme ale proměnných moc a nevejdou se, zásobník přeteče (nastane stack overflow) a to je zásadní problém - program v tu chvíli raději sletí.

V těchto jazycích tedy můžeme ručně požádat o kus paměti někde úplně jinde - použijeme tzv. heap. Takový kus paměti potom může být skoro jakkoliv velký. Řekneme operačnímu systému, kolik přesně paměti potřebujeme, a on ji pro nás někde sežene a nakonec jen řekne, kde jej máme hledat. Potíž je v tom, že po sobě musíme také uklízet: když ten kousek paměti nepotřebujeme, musíme to operačnímu systému zase dát najevo. Jinak se může stát, že náš program zabere spoustu paměti, kterou už ani neví, jak použít, a uživatelé budou nadávat. Říká se tomu memory leak a například webovým prohlížečům se to někdy stává.

Lidem, kteří si nezakládají na pořádkumilovnosti, se nabízí jazyky, které se o tohle postarají samy. Totiž, jakmile potřebujeme nějakou proměnnou, získají nám pro ni vhodné místo v paměti, a jakmile se jí vzdáme, příslušný kus paměti uvolní pro někoho jiného. Složitější část je jako obvykle uklízení. Stará se o něj nějaký program (obvykle neměnná součást programovacího jazyka), který pečlivě kontroluje, na jaké proměnné si v kódu pamatujeme ukazatele. Když na nějakou proměnnou ukazatel nevede, už ji určitě nedokážeme nijak použít (museli bychom střílet do paměti náhodně, abychom se strefili; to nejde) a proto je bezpečné tu proměnnou uvolnit, smazat. Program nebo spíše systém, který se o tohle stará, se nazývá garbage collector.

Jednoduchý přístup je počítání referencí. Každá proměnná potom má speciální poznámku - číslo, které udává, kolik ukazatelů (nebo referencí, svévolně zaměňuji oba pojmy) na ni vede. Jednak je možné, že jednu proměnnou pod různými názvy používáme na více místech v kódu a druhak ji můžou prostě používat jiné proměnné. Je potřeba tenhle počet velmi pečlivě zvyšovat a snižovat, a potom jakmile klesne na nulu, je pro garbage collector bezpečné proměnnou smazat.

Tenhle přístup funguje většinou dobře, ale selhává v případě, kdy několik proměnných odkazuje na sebe navzájem a nevede mezi ně žádný ukazatel zvenčí. Z kódu přístupné tedy už nejsou, ale přesto mají všechny počitadlo referencí nenulové. Abychom je odstranili, je potřeba spustit nějaký složitější (tedy pomalejší) algoritmus, který se na celou záležitost podívá zdálky a podobné pochybné konstrukce zahodí. To je potom cyklický garbage collector. Spouští se čas od času, anebo třeba až ve chvíli, kdy systému dochází paměť.

Uveďte moderní nástroje pro vývoj software a spolupráci vývojářského týmu.

Pro začátek bude asi dobré zmínit nástroje nemoderní, které běží někde v pozadí a udělají za nás hodně práce. Kompilované jazyky mají jakýsi program - kompilátor, který na požádání přeloží kus zdrojového kódu do procesorem spustitelné binárky. Dá se s ním pracovat přímo, ale je potřeba mu potom přesně psát jeden soubor za druhým, které chcete zpracovat. Pro zjednodušení téhle práce můžete použít nějaký další program, kterému stručněji popíšete, se kterými soubory potřebujete pracovat, a on se na požádání postará, aby se všechny přeložily. Nejznámější takový program je Make, ale je i mnoho jiných, většinou pohodlnějších.

Když kód přeložíte, spustíte a výsledný program spadne, často je těžké najít, jaká chyba to způsobuje. Pomůže s tím další program - debugger, který se pokusí v okamžiku chyby vám vypsat podrobný popis, co se stalo, a nechá vás si číst obsah proměnných, které jsou v kódu použité. Dá se používat pro hodně účelů, i když zrovna v kódu žádná chyba není. Nejznámější je GDB, tedy Gnu Debugger.

Spouštění těchhle programů pod Windows je dost nepohodlné a zřejmě především proto lidé používají nástroje, kterým říkáme IDE (integrated development environment, jednotné vývojové prostředí). Tam stačí kliknout na správné tlačítko. Kromě toho IDE zpravidla obsahuje chytrý textový editor, který vás upozorní na chybu někdy ještě před tím, než svůj program spustíte, a který vám různými dalšími způsoby šetří psaní a usnadňuje práci. Slavné, leč dosti těžkopádné, jsou programy NetBeans a Eclipse.

Nástroji k týmové spolupráci se míní verzovací systémy, nástroje značně připomínající vnitřní chod Wikipedie. Pomáhají psaní kódu ve vícečlenných týmech a po svých uživatelích vyžadují, aby po napsání každé změny ve zdrojovém kódu popsali několika lidskými větami, o jakou změnu šlo. Verzovací systém se potom postará o to, aby si příslušné změněné soubory mohli stáhnout i ostatní (bez toho, aby museli stahovat úplně celý projekt nanovo; to může být velký rozdíl v množství stahovaných dat). Navíc příslušnou změnu zaznamená tak, aby se na ni někdo jiný mohl třeba později podívat a pokud se mu nelíbí, z projektu ji zase smazat - vrátit příslušné soubory do původní podoby.

Verzovací systémy navíc umožňují řešit kolize, kdy více lidí upravuje tentýž soubor a nakonec chtějí všichni své změny sloučit dohromady. V jednoduchých případech - změna několika řádků, dopsání jedné funkce a tak - to dokáže počítač vyřešit sám, jindy požádá programátora, aby změny nějak sloučil ručně podle svojí úvahy. Přesto je to výrazný zisk pohodlí oproti jiným řešením.

Konkrétních takových programů je opět hodně. Kdysi se používalo CVS, na nějž navazuje Subversion (SVN); v poměrně nedávné době se naráz vyrojily tři verzovací systémy, které chtějí být mnohem lepší než Subversion. Jsou to Git, Mercurial (Hg) a Bazaar (bzr). Různé projekty se rozhodly používat různé z z těchto programů; když budete na nějakém projektu spolupracovat, asi si nebudete moct vybírat. Jinak jsou ale všechny téměř stejné a běží dobře i pod Windows.