Vysoká škola ekonomická v Praze Fakulta informatiky a statistiky Katedra informačních technologií
Studijní program: Aplikovaná informatika Obor: Informační systémy a technologie
Diplomant:
Bc. Tomáš Porazil
Vedoucí diplomové práce:
Ing. Rudolf Pecinovský, CSc.
Oponent diplomové práce:
Ing. Vladimír Oraný
Programovací jazyk Ruby a účelnost jeho zařazení do výuky
školní rok 2010/2011
Prohlášení Prohlašuji, ţe jsem diplomovou práci zpracoval samostatně a ţe jsem uvedl všechny pouţité prameny a literaturu, ze kterých jsem čerpal.
V Praze dne 27. 6. 2011 ………………………………. podpis
Abstrakt Práce představuje jazyk Ruby a zabývá se myšlenkou, zda by mělo smysl zařadit tento jazyk do výuky informatiky na Vysoké škole ekonomické v Praze (VŠE). Na začátku studia musejí všichni studenti informatiky povinně absolvovat dva předměty týkající se programování, kdy pro mnohé z nich je to vůbec jejich první seznámení s programováním. V současné době je jako primární programovací jazyk vyučována Java. Tato práce si klade za cíl představit čtenáři jazyk Ruby, jako alternativu k současné době vyučovanému jazyku Java nebo alespoň jako jazyk, kterým je moţné na studium Javy navázat. V úvodu práce je čtenář seznámen s historií jazyka, s filosofií, která stála u jeho zrodu a se základními koncepty, na kterých je postaven. Dále jsou představeny základní konstrukty a syntaxe jazyka nutné pro pochopení závěrečné části práce, která pojednává o různých programovacích technikách, které lze v Ruby pouţít. Závěrečná kapitola vyzdvihuje vlastnosti Ruby, pro které by jej bylo vhodné zařadit mezi vyučované jazyky na VŠE. Jedná se zejména o jasnou a stručnou syntaxi, moţnost vyuţít různých programovacích paradigmat a tvorbu interních DSL. Praktickým výstupem z celé práce je webová aplikace, nabízející interaktivní podobu práce samotné. Celou práci je tak moţné procházet na internetu a všechny uvedené příklady programového kódu ihned vyzkoušet v praxi. Aplikace nastiňuje moţný směr, kterým se můţe ubírat výuka programovacích jazyků.
Klíčová slova Ruby, Java, programování, výuka programování
Abstract The thesis presents the Ruby language and deals with the idea of including this language in the teaching of computer science at the University of Economics in Prague (UEP). In the beginning of their studies, all students have to complete two compulsory science subjects related to programming, which is
the first experience with programming for many of
them. Currently, the primary programming language being taught is Java. This work aims to present Ruby language, as an alternative to currently taught Java or at least as a language which the students could continue with after studying Java. In the introduction of the thesis the reader gets familiar with the history of language with its philosophy, which stood at the birth of Ruby and the basic concepts on which it is based. Next there are presented the basic constructs and syntax which are necessary to understand the final part of the paper, which discusses the different programming techniques that can be used in Ruby. The final chapter highlights the features of Ruby, for which it would be appropriate to include Ruby in the languages taught at UEP. These include especially clear and concise syntax, possibility to use different programming paradigms and creating internal DSL. The practical outcome of the whole work is a web application that offers an interactive form of the work itself. It is possible to browse the whole work in the internet and all mentioned examples of program code can be immediately tested in practice. The application outlines possible direction which can teaching programming languages take.
Keywords Ruby, Java, programming, teaching programming.
Poděkování Rád bych poděkoval Ing. Rudolfu Pecinovskému, CSc. za vedení této práce a za cenné rady a připomínky při jejím zpracování. Dále bych rád poděkoval všem svým blízkým, kteří mi byli po celou dobu oporou a motivací, jak při psaní této práce, tak při celém studiu.
Obsah Úvod ............................................................................................................................. 15 1
Interaktivní podoba diplomové práce ..................................................................... 21
2
Proč Ruby ............................................................................................................... 23 2.1
Kultura ............................................................................................................ 23
2.2
Jak vypadá kód ................................................................................................ 24
2.3
Svoboda .......................................................................................................... 24
2.4
Monkey patching ............................................................................................ 24
2.5
Vlastnosti jazyka ............................................................................................. 25
2.5.1 Skriptovací jazyky ..................................................................................... 25 2.5.2 Interpretované jazyky ................................................................................ 26 2.5.3 Imperativní programování ......................................................................... 27 2.5.4 Funkcionální programování ....................................................................... 27 3
Ruby vs Java........................................................................................................... 29 3.1
První aplikace ................................................................................................. 29
3.2
Kam se poděly závorky................................................................................... 30
3.3
Dynamické typování ....................................................................................... 31
3.4
Datové typy ..................................................................................................... 31
3.4.1 Čísla ........................................................................................................... 32 3.4.2 Řetězce ....................................................................................................... 34 3.4.3 Symboly ..................................................................................................... 35 3.4.4 Pole ............................................................................................................ 36 3.4.5 Asociativní pole ......................................................................................... 37 3.4.6 Intervaly, posloupnosti .............................................................................. 38 3.4.7 Regulární výrazy ........................................................................................ 41 3.5
Algoritmické konstrukce ................................................................................. 43
3.5.1 Příkazy pro větvení (if, unless, elsif, case) ................................................ 43
3.5.2 Cykly ......................................................................................................... 46 3.6
Iterátory a bloky ............................................................................................. 47
3.6.1 Bloky kódu ................................................................................................ 48 3.6.2 Iterátory ..................................................................................................... 49 3.7
Metody............................................................................................................ 53
3.7.1 Pojmenované argumenty ........................................................................... 55 3.7.2 Libovolný počet argumentů ...................................................................... 55 3.8
Třídy ............................................................................................................... 56
3.8.1 Definice třídy a konstruktor ...................................................................... 56 3.8.2 Tvorba instance a instanční metody (metody instance) ............................ 57 3.8.3 Přístupové metody ..................................................................................... 59 3.8.4 Dědičnost ................................................................................................... 61 3.8.5 Konstanty .................................................................................................. 62 3.8.6 Virtuální atributy ....................................................................................... 63 3.8.7 Proměnné třídy a metody třídy .................................................................. 64 3.8.8 Modifikátory přístupu ............................................................................... 65 3.9
Moduly ........................................................................................................... 67
3.9.1 Jmenné prostory ........................................................................................ 68 3.9.2 Mixiny ....................................................................................................... 69 4
Programovací techniky .......................................................................................... 73 4.1
Dynamické typování....................................................................................... 73
4.1.1 Duck typing ............................................................................................... 74 4.1.2 Třídy nejsou datové typy ........................................................................... 74 4.2
Callbacks (zpětná volání) ............................................................................... 76
4.2.1 Method missing ......................................................................................... 77 4.3
Funkcionální programování ........................................................................... 80
4.3.1 Referenční transparentnost ........................................................................ 81
4.3.2 Princip kompozicionality ........................................................................... 83 4.3.3 Jednoduché funkce v Ruby ........................................................................ 84 4.3.4 Funkce vyšších řádů .................................................................................. 88 4.4
Domain Specific Language ............................................................................. 94
4.4.1 Language Oriented Programming .............................................................. 95 4.4.2 Interní DSL v Ruby ................................................................................... 97 4.4.3 Metaprogramování ................................................................................... 101 Závěr ........................................................................................................................... 109 Pouţitá literatura ......................................................................................................... 113
Úvod Programovací jazyk Ruby vznikl v Japonsku. Jeho tvůrcem je Yukihiro Matsumoto, kterému se ve světě komunity kolem jazyka Ruby neřekne jinak neţ Matz. Matsumoto uţ jako student snil o tom, ţe vytvoří jazyk, který by byl jednoduchý, práce s ním byla zábava a přitom se hodil pro řešení většiny problémů. Jako fanoušek skriptovacích jazyků a objektově orientovaného přístupu objevil jazyky Python a Perl. Ani jeden z těchto jazyků však nenaplnil jeho očekávání. V předmluvě k nejoblíbenější učebnici jazyka Ruby1 píše: „Chtěl jsem jazyk, který by byl mocnější než Perl a více objektově orientovaný, než Python“ [Thomas2004]. Práce na svém novém programovacím jazyku započal v roce 1993 a 21. prosince roku 1995 předvedl odborné veřejnosti první verzi jazyka Ruby 0.95. Od té doby uběhlo více neţ 15 let. V době psaní této práce je nejaktuálnější verze jazyka 1.9.2 a tento programovací jazyk se těší stále větší oblibě po celém světě. Postupem času se z Ruby stal jazyk, jenţ se pouţívá jak pro psaní malých jednoúčelových skriptů, tak pro psaní velkých škálovatelných aplikací. Například v NASA Langley Research Center slouţí v programech pro simulaci chování. Nejvíce však vstoupil do povědomí díky webovému frameworku Ruby on Rails od společnosti 37signals 2. Tento framework spatřil světlo světa v roce 2004 a jak je vidět na Obrázek 1 - Vývoj indexu TIOBE jazyka Ruby [Tiobe2011], právě po roce 2004 začíná pozvolný zájem o Ruby. Největší vzestup zájmu zaţívá Ruby v roce 2006 a tento zájem dosahuje svého maxima na konci roku 2008. Od té doby se objevila celá řada jiných webových frameworků od těch větších jako je například Merb3 aţ po ty, které by se daly nazvat spíše micro-frameworky, jejichţ typickým představitelem je Sinatra4. Ruby se oproti ostatním jazykům vymyká v jedné základní věci a tou je filosofie, s níţ byl tento jazyk vytvořen. Yukihiro Matsumoto tvrdí, ţe pro člověka je přirozené, aby tvořil aby maloval obrazy, dělal sochy, navrhoval budovy a v jeho případě psal software. Zároveň však musí být šťastný v tom, co dělá, a proto vzniklo Ruby. Jazyk, který má programátorům pomoci dosahovat cílů a přitom jim nebude stát v cestě a práce pro ně bude zábavou. V předmluvě v knize Programming Ruby Matsumoto o Ruby píše: „Věřím, že účelem života, alespoň částečně, je být šťastný. Na základě tohoto přesvědčení je Ruby navrženo tak, aby 1
Jedná se o knihu Programming Ruby, které se v anglicky mluvících zemích přezdívá "pickaxe", coţ v překladu znamená krumpáč, který je ztvárněný na obalu této knihy. 2 http://http://37signals.com/ 3 http://www.merbivore.com/ 4 http://www.sinatrarb.com/ 15
programování v něm nebylo pouze snadné, ale také zábavné. Umožňuje se soustředit na kreativní stránku programování a snižuje stres“ [Thomas2004].
Obrázek 1 - Vývoj indexu TIOBE jazyka Ruby *Tiobe2011]
Existují dva přístupy, jak lze k programovacím jazykům přistupovat. Jedním je dívat se na jazyk z hlediska toho, co všechno je moţné jím zapsat. Druhý přístup zdůrazňuje to, jak se s jazykem pracuje lidem, kteří ho vyuţívají při své práci. Je velice sloţité, najít rovnováhu mezi těmito dvěma přístupy. Tvůrce Ruby proto sám říká, ţe nevytvořil dokonalý jazyk. Nebylo to ani jeho cílem. Cílem bylo vytvořit jazyk, při jehoţ pouţívání se uţivatelé budou cítit dobře [Venners2003].
Jakýkoliv
jazyk,
který
vyhovuje
Turingově
terorii
kompletního
programovacího jazyka, můţe být nahrazen jiným jazykem, který splňuje tuto podmínku. Jinak řečeno, program, který lze napsat v jazyku Python, lze se stejným výsledkem zapsat v Assembleru nebo například v jazyku Brainfuck5. Počítač rozdíl nepozná. Patrný je ale pro člověka, který s daným jazykem pracuje. Projevuje v náročnosti práce a tím pádem se i odráţí v pocitech, které z práce s ním pramení. Nemá smysl tvořit dokonalý jazyk, protoţe kaţdý člověk je jiný, a proto se kaţdému bude pracovat lépe s jiným jazykem. 5
Experimentální programovací jazyk, jehoţ tvůrcem je Urban Müller. Záměrem bylo vytvořit jazyk z co nejjednodušším a nejmenším překladačem. Jazyk samotný má pouze osm příkazů. [Raiter] 16
Důraz na lidský faktor a jeho rozmanitost je prvkem, na který by se nemělo zapomínat. Ţijeme v době, kdy se stává běţné, ţe většina interakcí s druhými lidmi se odehrává prostřednictvím počítačů. A právě slovo prostřednictvím hraje zásadní roli. Počítače jsou pouze nástrojem, který komunikaci umoţňuje, nejsou však jejím příjemcem. Příjemcem sdělení je vţdy člověk. Kdyţ píšeme email, nekomunikujeme s počítačem, ale s příjemcem dané zprávy. Pokud bychom komunikovali s počítačem, mohli bychom znaky zadávat v ASCII kódu, psát příkazy v Assembleru nebo je zadávat rovnou pomocí nul a jedniček. To je jazyk, kterému rozumí počítač. Stejně tak je to se psaním softwaru. V emailu představuje sdělení text, který je v něm obsaţen. V případě softwaru je sdělení software sám. Počítačové programy nejsou psány pro počítače, jsou psány pro lidi, protoţe jen ti jsou jejich adresáty. Emaily se píší ve formě, která je pro lidi nejpřirozenějí. Stejným pravidlem bychom se měli řídit při psaní počítačových programů. Programátorům by tedy měl být poskytnut jazyk, kterým dokáţí co nejefektivněji vyjádřit své myšlenky. Yukihiro Matsumoto při psaní Ruby vycházel z myšlenky, ţe kaţdý uţivatel je jiný a jiným způsobem vyjadřuje své myšlenky. Tento předpoklad je tedy zabudován i v samotném jazyce. Jedna věc lze vyjádřit (zapsat) několika moţnými způsoby. Je to přesně opačný přístup, neţ který je pouţit například v Pythonu, kde lze jedna věc udělat většinou jen jedním způsobem. Tvůrce Ruby se tak inspiroval v jazyku Perl, který právě poskytuje více způsobů, jak dojít ke stejnému výsledku. Výhoda Pythonu spočívá v tom, ţe pokud existuje pouze jeden moţný způsob, jak daný problém zapsat, je velice pravděpodobné, ţe ho takto zapíše většina programátorů. Kód se tak stává méně variabilní a tím pádem i čitelnější. Výhoda Ruby naopak spočívá právě ve variabilitě. Jazyk programátora neomezuje a poskytuje mu širokou paletu příkazů (moţností), ze kterých si můţe vybrat tu, která mu nejvíce vyhovuje. Mohlo by se tak zdát, ţe program, který v Ruby napíše jeden programátor bude pro jiného špatně čitelný. Jak se ale zdá, tak právě čitelnost je jedna z jeho hlavních zbraní. Skoro v kaţdé učebnici Ruby, návodu nebo článku na internetu, je v úvodní kapitole malá ukázka kódu a výzva, ať si tento kód čtenář přečte nahlas. S trochou fantazie je vyslovený kus kódu k nerozeznání od anglické věty. S jistou nadsázkou lze říci, ţe to je důvod, proč existuje více moţností, jak vyjádřit jednu a tu samou věc různě napsaným kusem kódu, coţ nemusí být vţdy na škodu, spíše naopak. V běţné řeči lidé také nekomunikují všichni stejně. Poslední vlastností, která je pro tento jazyk charakteristická je princip nejmenšího překvapení6 [Matsumoto2001]. Tento princip je mimo jiné moţné najít v [James1987] a také
6
Z anglického Rule of Least Surprise nebo také Principle of Least Surprise 17
v [Raymond2003]. Netýká se pouze programování, ale také třeba navrhování uţivatelských rozhraní. Základním východiskem je situace, kdy uţivatel přichází jiţ s nějakými znalostmi a zkušenostmi a právě na těch by se mělo stavět a ne se snaţit jít proti nim. Matsumoto původně navrhoval Ruby tak, aby co nejméně přakvapovalo jeho samého, aby co nejvíce stavělo na programátorských principech, které on měl zaţité a které se mu líbily. Jak se však později ukázalo, tento aspekt přijala za svůj celá komunita kolem tohoto jazyka. Samozřejmě to neznamená, ţe by jazyk byl natolik intuitivní, ţe by nepřekvapil nikoho. Vţdy záleţí na znalostech předchozích programovacích jazyků daného jedince. Tímto principem se také rozumí, ţe programátora, jenţ se s Ruby dostatečně seznámí, by neměl jazyk uţ ničím překvapit. Ruby se snaţí být konzistentní a dodrţovat pravidla a principy, které si programátor osvojí jiţ při učení jeho základů.
Ruby na kolejích Největší rozšíření jazyka Ruby nastalo po uvolnění frameworku Ruby on Rails (dále uţ jen zkráceně jako RoR) jako opensource projektu. Tvůrcem RoR je dánský programátor David Heinemeier Hansson, který je spoluzakladatelem a společníkem ve společnosti 37signals. O celosvětovém úspěchu RoR svědčí i to, ţe Hansson v roce 2005 převzal cenu Hacker of the Year, kterou uděluje společnost Google a O'Reilly. O rok později pak převzal cenu Jolt, která je udělována za signifikantní dopad na softwarový průmysl [LoudThinking]. Hansson přiznává, ţe původním záměrem nebylo vytvořit webový framework a uţ vůbec ne takový, který by později mohl publikovat jako opensource. RoR vzniklo přirozenou cestou při programování aplikace Basecamp, která měla původně slouţit pouze pro interní potřeby ve společnosti 37signals. Po dokončení prací si Hansson uvědomil, kolik nástrojů pro svou vlastní potřebu vytvořil a tak z aplikace Basecamp separoval samotný framework. Toto se odehrálo v roce 2004, ale trvalo ještě celý jeden rok, neţ mohl celý projekt publikovat veřejně. Zásadní otázkou zůstává, proč si Hansson vybral pro svou práci právě Ruby. Proč teď existuje framework Ruby on Rails a ne třeba PHP on Rails nebo Python on Rails? Ruby s Rails pojí jedna důleţitá věc a to je filosofie, kterou vyznávají. Pokud člověk čte základní informace o Rails můţe mu připadat, ţe si čte o Ruby. Objevují se tu stejné věci, které měl v hlavě Yukihiro Matsumotu, kdyţ tvořil Ruby. Vztah Ruby a RoR lze popsat následující paralelou. Ruby je jazyk a v jazyku jako takovém, lze popsat cokoliv. RoR představují pouze úrčitý výsek, dalo by se říci ţánr, jako je například ve filmovém odvětví thriller. To znamená,
18
ţe pokud chceme natočit thriller, je RoR ideálním nástrojem, který perfektně naplní naše potřeby. Jestliţe bychom však chtěli dělat romantický film, RoR představuje nevhodný nástroj a existují lepší [Hopkins2010]. RoR je nástrojem pro tvorbu webových aplikací. Pokud bychom chtěli vyvíjet například desktopové aplikace, existují k tomuto účelu jiné nástroje. To ale neznamená, ţe se nedají vyvíjet desktopové aplikace v Ruby. Na otázku, zda by Hansson znovu sáhl po neznámém jazyku z Japonska, kdyby se mohl vrátit zpátky do minulosti, odpovídá jednoznačně - ano [Hopkins2010]. Podle jeho názoru hrají velkou roli emoce. Kdyţ psal dříve v Javě, C# nebo v PHP, nezaţíval nikdy stejné pocity jako při psaní v Ruby a dodává: „Ruby je mnohem blíže mým myšlenkovým pochodům. Zdá se mi, jako kdybych vyjadřoval přesně to, co chci říci a ani to nemusím překládat do kódu“ [Hopkins2010]. Existuje zde velká podobnost s běţnou lidkou řečí a různými jazyky. Kaţdý jazyk je jiný a myšlenky se v něm vyjadřují trošku odlišným způsobem. Člověk sám k sobě mluví svým vlastním vnitřním jazykem. Tento jazyk není podobný ţádnému známému jazyku a nejblíţe má zřejmě k predikátové logice. Při vyjadřování v běţném jazyce dochází tedy vţdy k překladu mezi naším vnitřním jazykem a jazykem, ve kterém chceme vyjádřit své myšlenky, tak aby nám rozuměli ostatní. Není tedy překvapivé, ţe lidé, kteří umí více jazyků, se v jednom dokáţí vyjadřovat lépe neţ v druhém. To samé platí i v programovacích jazycích. Hansson k tomuto tématu říká, ţe přirozené jazyky se od sebe liší v rytmu a zvukomalebnosti. Kdyţ například porovnáme němčinu a francouzštinu, nemusíme ani mluvit francouzsky, abychom zjistili, ţe francouzština je libozvučnější jazyk. Stejné je to s programovacími jazyky [Hopkins2010]. Většina lidí si dříve myslela, ţe programování je převáţně o tom, jak aplikace udělat co nejrychlejší, nebo tak aby zabíraly co nejméně paměti. V současné době se ale i v softwarovém odvětví akcentuje přístup, který je zaměřen na lidi, tedy na programátory. Stejně jako v kaţdém jiném odvětví, tak i zde platí, ţe lidé odvádějí mnohem kvalitnější práci, pokud je jejich práce baví a naplňuje. Napsat program, který bude přesně plnit dané zadání, není nic sloţitého a dosaţení cíle lze dojít bespočtem cest. Důleţitější neţ výsledek je proto cesta samotná a pocit, který člověk má, kdyţ po ní kráčí. V tom tkví zásadní rozdíl, rozdíl mezi prací a uţíváním si práce. Je známo, jaké produkty vznikají, pokud si lidé svou práci opravdu uţívají. O potřebě seberealizace psal jiţ v 80. letech minulého století americký psycholog Abraham Maslow. Maslow je autorem známé teorie lidských potřeb, jíţ se také říká Maslowova pyramida lidských potřeb. Pyramida znázorňuje hierarchicky uspořádané lidské potřeby od těch nejniţších, které jsou biologického rázu aţ po potřeby seberealizační, které
19
jsou na jejím vrcholu. Člověk nejdříve uspokojuje nejniţší potřeby a aţ pokud jsou naplněny na určitou úroveň, orientuje své jednání k naplnění vyších potřeb. Hansson ve svém rozhovoru s Peterem Hopkinsem [Hopkins2010] poznamenává ještě jeden důleţitý fakt. A to, ţe Ruby přilákalo k programování i lidi, kteří se o ně do té doby zajímali jen okrajově nebo vůbec. Existují různé názory na to, zda to vidět jako pozitivní nebo negativní jev. Jistá je však jedna věc - právě lidé, kteří nejsou ze své podstaty programátory, přicházejí díky svému rozhledu často s těmi nejlepšími nápady.
20
1 Interaktivní podoba diplomové práce Problém při studiu programovacích jazyků, ať uţ z papírových učebnic, akademických prací nebo z článků na internetu, je absence jakékoli interaktivity. Je dokázáno, ţe pokud si člověk můţe ihned vyzkoušet nově nabyté znalosti, osvojí si je v kratším čase a zapamatuje si je na delší dobu. Strmá učící křivka je kýţený stav, kterého chceme dosáhnout. Pojednává-li práce o programovacích jazycích, byla by škoda nevyuţít jejich síly a neumoţnit čtenáři, aby si ihned osvojil tématiku, o které se právě dočetl. Jedním z hlavních výstupů této práce je interaktivní webová aplikace, která představuje elektronickou verzi této práce. Hlavní předností aplikace není pouze převod diplomové práce do podoby webových stránek, ale jiţ zmíněná interaktivita. V kapitolách, které pojednávají o programování, se nachází velké mnoţství ukázek kódu a tyto ukázky je moţné si v aplikaci přímo zkusit. Čtenář tak má moţnost, vyzkoušet si, zda popisovanou problematiku pochopil správně a zda jím napsaný kód dělá přesně to, co by od něj očekával. Aplikace tak představuje cestu, jak dosáhnout okamţité zpětné vazby. Aplikace je umístěna na adrese:
http://porazil.net/diplomova-prace Nebývá zvykem ihned v úvodu akademické práce prezentovat výstupy, ke kterým se při zpracování došlo. Tento výstup je však natolik zásadní, ţe můţe ovlivnit vnímání celé práce. Webová aplikace přináší oproti klasicky psané práci (ať uţ vytištěné či v elektronické podobě) nový rozměr a snaţí se ukázat, jakým směrem je moţné se vydat při výuce programovacích jazyků. Aplikace je napsána v jazyku Ruby, s vyuţitím jiţ v úvodu zmíněného microframeworku Sinatra. Inspirací pro napsání aplikace byla aplikace Try Ruby7, která se snaţí Ruby atraktivní formou přiblíţit začátečníkům. Zdrojový kód aplikace je součástí této práce jako přílohy a současně je umístěn na serveru GitHub na adrese http://github.com/pory/thesis.
7
http://tryruby.org 21
22
2 Proč Ruby Otázkou stále zůstává, co je na Ruby tak úţasného, ţe má cenu se jím zabývat. To, ţe Ruby činí programátory šťastnějšími, není ta správná odpověď. Je důleţité klást si otázky, jeţ dávají odpovědi, které nejsou postaveny pouze na emocích, ale na reálných faktech. Pouze pokud známe praktické důvody toho, proč se nám nějaká technologie líbí, můţeme ji doporučit ostatním nebo naopak objevit technologii novou. Nejdříve potřebujeme znát argumenty, které pak mezi sebou můţeme porovnávat a hodnotit. V následujících odstavcích budou představeny základní koncepty a důvody toho, proč komunita kolem Ruby nedá na svůj jazyk dopustit. Většina z těchto tezí pak bude v práci dále rozvedena a představena na konkrétních příkladech.
2.1 Kultura Důleţitým prvkem kultury kolem Ruby je testování. Ne snad, ţe by Ruby jako první přišlo s touto myšlenkou, to vůbec ne. Jednotkové testování tu bylo dlouho před Ruby a příslušné knihovny, které k tomuto účelu slouţí, obsahuje snad kaţdý jazyk. Zajímavé ale je, jak testování přijala komunita kolem jazyka Ruby. Průměrný vývojář v Ruby napíše více kódu, který testuje kód aplikace, neţ samotného kódu aplikace. Poměr se pohybuje v průměru asi 1:1,28 [Confreaks2010]. Tento údaj můţe být v mnoha ohledech zavádějící, přesto však mnohé vypovídá o vyuţití principů testování. Není tedy určitě náhoda, ţe například framework Ruby on Rails v sobě přímo zahrnuje modul, který umoţňuje vyvíjet dle principů TDD9. Testování ale není ani tak o nástrojích, jako o filosofii. Testování má ještě jeden důleţitý dopad. Komunita kolem Ruby spolu velice úzce spolupracuje a ve velké míře se v aplikacích pouţívají kusy kódu (patche, nebo celé knihovny), které vymyslel někdo jiný. O tom, jak velká je tato míra spolupráce, svědčí například úspěch serveru GitHub, který je v Ruby komunitě velice populární10. Díky důkladnému pokrytí testy, pak uţivatel nemusí mít strach, ţe pokud převezme hotový kus kódu od někoho jiného, přestane mu fungovat jeho aplikace. Kdyby skutečně fungovat přestala, tak to pomocí testů hned odhalí. 8
Přesné označení tohoto ukazatele je Code to test ratio Test Driven Development – programování řízené testy 10 GitHub je server, který kromě verzovacího nástroje Git poskytuje celou řadu nástrojů pro tzv. kolaborativní (sociální) vývoj aplikací. V době psaní této práce je v něm zaregistrováno 596 278 lidí a nachází se v něm 1 728 729 repozitářů [GitHub2010] 9
23
2.2 Jak vypadá kód „Kód, kterému rozumí počítač, umí napsat každý trouba. Dobří programátoři píší kód, kterému rozumí lidé“, toto je myšlenka Martina Fowlera [Fowler1999], se kterou se nedá jinak neţ souhlasit. Navíc přesně vystihuje to, v čem se Ruby programátoři liší od ostatních. V celé komunitě je totiţ vidět snaha o to, aby kód, který programátor napíše, byl na jedné straně co nejkratší a na straně druhé co nejsrozumitelnější. To samozřejmě není pouze otázka programátorů, ale také jazyka, který k podobnému stylu vyjadřování poskytuje vhodné nástroje.
2.3 Svoboda Termín svoboda je velice široký, coţ je jedině správně. Ruby je svobodomyslný jazyk a svoboda k němu tedy neoddělitelně patří. Vše vychází uţ z licence, pod kterou je Ruby šířeno. Ruby je šířeno pod licencí MIT, jeţ byla původně vytvořena na Massachusetts Institute of Technology (odtud také pochází její zkratka) pro distribuci X Windows System. MIT je svobodná licence a produkty pod toutu licencí je moţné pouţít dokonce v proprietárním softwaru s podmínkou, ţe text licence MIT musí být dodán spolu se softwarem. MIT licence je velice krátká a srozumitelná a její celé znění lze nalézt například v [Opensource2010].
2.4 Monkey patching Tímto tajuplným názvem se označuje technika, pomocí které se mění nebo rozšiřuje kód aplikace za běhu programu [Avdi2010]. Jinými slovy to znamená, ţe je moţné kdykoliv za běhu aplikace vstoupit do jakékoliv třídy a modifikovat ji. Existují různá pojetí toho, co se pod touto technikou skrývá a toto je to nejširší. V Ruby komunitě se tato technika stala velice populární a právě zde je chápána ve svém nejširším významu. Původně se této technice říkaloGuerrilla patching, coţ bylo určitě výstiţnější. Slovo guerrilla se velice podobá slovu gorilla. Z toho vznikl Monkey patching. Velice zajímavý vývoj označení, ale na druhou stranu přesně odráţí to, jaká je komunita, která ho vymyslela. Monkey patching je v Ruby umoţněn díky dvěma základním věcem. Zaprvé se jedná o dynamicky interpretovaný jazyk. To znamená, ţe kaţdý kus kódu je interpretován aţ při svém zpracování. Zadruhé jsou třídy v Ruby navrţeny jako otevřené. V praxi to má za následek, ţe je moţné vzít například třídu String a přidat do ní nové metody nebo přepsat metody stávající. 24
Tento postup vyvolává celou řadu emocí a neshod. Jedním z největších argumentů proti pouţívání podobných postupů je, ţe se tím zásadním způsobem porušují obecně platné principy objektově orientovaného programování, konkrétně zapouzdření. Důsledkem jsou poté pluginy, které současné mění chování standardních tříd a v praxi to funguje tak, ţe plugin, který mění třídu jako poslední, vyhrává. Výsledkem je nefunkční aplikace, kde je skoro nemoţné dopátrat se příčiny chyby. Velikým odpůrcem této techniky je Jeff Atwood, který na svém blogu píše: „Pokud vás myšlenka kolem Monkey patching trochu děsí, pak je to v pořádku. Představte si, že ladíte kód, kde se třída String chová podstatně jinak, než jak jste se učili“ [Atwood2008]. Na druhou stranu Monkey patching je přesně v souladu s tím, co je uvedeno výše a co říká samotný tvůrce jazyka Ruby. Jeho smyslem nebylo vytvořit dokonalý jazyk, protoţe jazyk, který by byl dokonalý pro kaţdého, neexistuje. Monkey patching představuje způsob, jak si kaţdý můţe jazyk, alespoň trochu uzpůsobit k obrazu svému. Ruby ponechává vše na programátorech, nesvazuje jim ruce. David Heinemeier Hansson se sám sebe ptá: „Dáte-li mi lano, oběsím se s ním? Nejspíše ne a raději ho použiji na něco užitečnějšího.“ [Confreaks2010]. Díky pouţití této techniky je pak moţné v RoR zavolat na řetězec například metodu camelize, která ho převede do velbloudí notace. Podobný způsob vyjadřování je pak přesně tím důvodem, proč si Ruby získalo takovou oblibu.
2.5 Vlastnosti jazyka Zatím bylo o Ruby jako o jazyku pojednáváno hlavně v obecné rovině. Pouze v náznacích byly zmíněny některé základní vlastnosti jazyka. Tato část by tedy měla dát v krátkosti odpověď na to, co je Ruby za jazyk. Kam ho můţeme zařadit v souvislosti s ostatními programovacími jazyky. Ruby je interpretovaný skriptovací jazyk, který se zaměřuje na objektově orientované programování. Je inspirován a vychází z programovacích jazyků Perl, Smalltalk, Eiffel, Ada a Lisp. Kombinuje v sobě přístupy funkcionálního a imperativního programování. Dvě předchozí věty v krátkosti vystihují podstatu programovacího jazyka Ruby. O jejich srozumitelnosti by se však dalo diskutovat. Postupně si zde tedy rozebereme čtyři hlavní termíny, které Ruby popisují. 2.5.1 Skriptovací jazyky Popsat skriptopvací jazyky můţe být těţší, neţ se zdá. Neexistuje totiţ ţádná ucelená definice toho, co je to skriptovací jazyk. Tvůrci skriptovacích jazyků se o tuto definici moc nezajímají 25
a raději řeší pratktické problémy. Kdyţ byl dotázán tvůrce Perlu Larry Wall, ať popíše skriptovací jazyky, odpověděl v nadsázce: „… Scénář je něco, co dáte hercům. Program je to, co vám přitáhne diváky.“11 [Prechelt2002] Skriptovací jazyky jsou tak spíš posuzovány podle svých vlastností, neţ aby se daly zaškatulkovat podle přesné definice. Skriptovacími jazyky jsou kromě Ruby například PHP, Perl, Python nebo JavaScript. Typické vlastnosti skriptovacích jazyků jsou pak [Prechelt2002]: Obvykle je není nutné překládat a ve většině případů obsahují interaktivní interpret jazyka. Obsahují pouze takové mnoţství příkazů, které jsou bezpodmínečně nutné. Snaţí se programátora odstínit od všech nepotřebných věcí, tak aby se co nejvíce maximalizovala efektivita. Zejména podporují automatickou správu paměti. Funkcionalita usnadňující práci je zabudována přímo v jazyku a nenachází se v externích knihovnách. Neobsahují silnou typovou kontrolu.
2.5.2 Interpretované jazyky Programovací jazyky se dělí na dvě základní kategorie z hlediska typického zpracování počítačem12. Na jednom konci spektra se nachází jazyky interpretované, na druhém konci spektra stojí překládané jazyky. Nic ale není černobílé a tak existují i způsoby zpracování, které jsou někde na půli cesty. Interprety některých jazyků tak pouţívají i techniku překladu. Pokud bychom chtěli pro přehlednost způsoby zpracování roztřídit do kategorií, mohly by dané kategorie vypadat následovně: 1) Zdrojový kód aplikace je zpracován přímo
Příkladem těchto interpetů je unixový shell nebo COMMAND.COM. Výhodou je jednoduchost, nevýhodou naopak rychlost zpracování. 2) Zdrojový kód je přeložen do efektivnějšího mezikódu a ten je následně zpracován
Tuto techniku vyuţívají například jazyky Ruby, Python nebo Perl. 3) Zpracován je už předem přeložený kód
Předem přeloţenému kódu se většinou říká bytecode. Tento postup je typický například pro Javu. Výhodou je, ţe bytecode není ještě strojový kód a tak je nezávislý na platformě. 11
Anglické slovo script v češtině mimo jiné odpovídá výrazu scénář, proto je moţná tato slovní hříčka, kterou Larry Wall popsal skriptovací jazyky. 12 Jedná se pouze o typickou implementaci, ne o pravidlo. Teoreticky je moţné, kterýkoliv jazyk zpracovat libovolným způsobem. To znamená, ţe například typické zpracování PHP je pomocí interpretu, ale můţe být i nejdříve přeloţen. 26
Jeden a ten samý kód, tak můţe být spuštěn na jakékoliv platformě, pro kterou existuje JVM (Java Virtual Machine). JVM je tedy v podstatě interpret bytecodu do strojového kódu dané platformy. JVM můţeme jako interpret označit o to víc, ţe v poslední době se vyuţívá tzv. princip just-in-time compilation, který překládá bytecode průběţně aţ za běhu aplikace. 4) Zdrojový kód je přeložen přímo do strojového kódu
To je případ například typické implementace jazyka C. Pomocí překladače (kompilátoru), je zdrojový kód převeden rovnou do strojového kódu, který je zavislý na konkrétní architektuře. 2.5.3
Imperativní programování
Imperativní programování je jedno ze základních programovacích paradigmat. Protipólem k imperativnímu programování je programování deklarativní. Imperativ znamená rozkaz nebo rozkazovací způsob a je tedy zřejmé, ţe imperativní programování se skládá z řady příkazů. Tento způsob komunikace je člověku velice blízký. Můţeme ho nalézt v různých příručkách, jako jsou kuchařky nebo jiné doporučené postupy. Velice často tak také probíhá komunikace mezi rodičem a dítětem nebo zaměstnavatelem a zaměstnancem. Imperativní programovcí jazyky jsou tedy charakteristické tím, ţe program má tvar posloupnosti příkazů, které ve výsledku tvoří algoritmus řešení. Charakteristická pro toto paradigma je změna stavu. Stav se v průběhu programu udrţuje v proměných a jejich obsah je měněn operací přiřazení. Dalšími typickými příkazy jsou pak kromě přiřazení ještě cykly a příkazy pro větvení. Prvními imperativními programovacími jazyky byly strojové jazyky jednotlivých počítačů. V současné době mezi představitele můţeme zahrnout všechny jazyky, které vyznávají objektově orientovaný přístup, jako například C++, Java, Python, Perl a Ruby. 2.5.4
Funkcionální programování
Funkcionální programování je podmnoţinou deklarativního programování. Typické pro deklarativní programování je, ţe narozdíl od imperativního programování, neříká, jak se co má udělat, ale naopak říká, co se má udělat. Programátor nepopisuje algoritmus řešení, ale popisuje to, jak má vypadat výsledek. Programování v těchto jazycích se poté tedy neskládá z instrukcí, které se mají postupně provádět, ale z výčtu vlastností, které má výsledek splňovat. Zpravidla ani nezáleţí na pořadí podmínek, protoţe kód nebývá zpracován lineárně (postupně), jako jsme na to zvyklí z imperativních programovacích jazyků. Deklarativní programovací jazyky se dělí na funkcionální, logické, na jazyky s omezujícími podmínkami a na jazyky, které jsou specializované pouze na určitou doménu, jako například jazyk SQL. 27
Základem funkcionálních programovacích jazyků je teorie funkcí, lambda-kalkul. K programu je přistupováno jako k matematickému výrazu, kde pomocí funkcí popisujeme, co chceme vypočíst, a to, jak se výpočet bude provádět, není podstatné. „Funkciolnální programovací jazyky jsou třídou jazyků, které odrážejí matematické smýšlení lidí a technologii, která je za vším skryta, nechávají v pozadí“ [Goldberg1996]. Teorie kolem funkcionálního programování vznikla ještě před masovým rozšířením počítačů a má ji na svědomí profesor matemaiky Alonzo Church, který působil na Prinston University. K jeho nejznámějším vědeckým přínosům patří Church-Turingova teze o tom, ţe algoritmus je ekvivaltentní pojmu funkce. Není určitě bez zajímavosti, ţe Alan Turing, jedno z nejvýznamnějších jmen v historii informatiky, působil u Churche jako doktorand. Funkcionálnímu programování se také někdy říká programování bez přiřazení. V čistě funkcionálních jazycích operace přiřazení jednoduše neexistuje. Mnoho algoritmů má v těchto jazycích elegantnější zápis a dovoluje se dívat na problém z úplně jiné perspektivy. Funkcionální programování se například vyuţívá při programování umělé inteligence. Mezi typické představitele této rodiny jazyků patří Haskell, Scheme a Lisp.
28
3 Ruby vs Java Cílem této kapitoly je představit základní syntaktické a sémantické rozdíly mezi jazyky Ruby a Java. Ruby je dáváno do kontrastu s Javou, protoţe Java je z dlouhodobého hlediska nejpouţívanějším (nejrozšířenějším) programovacím jazykem a stala se v podstatě standardem mezi programovacími jazyky. Z tohoto důvodu a pro lepší pochopení Ruby jsou v celé práci příklady demonstrovány právě v kontrastu s jazykem Java. Aby mohly být vysvětleny a ukázány principy, proč má smysl se jazykem Ruby zabývat, je nejdříve nutné pochopit alespoň základní syntaxi a konstrukty jazyka. Právě k tomuto účelu slouţí tato kapitola. V kapitole jsou popsány základy jazyka, které jsou nutné pro pochopení sloţitějších případů vyuţití Ruby. Rozsah práce ani neumoţňuje podrobný výklad a pro pochopení hlubších souvislostí proto odkazuji na [Thomas2004], kde se nachází podrobná referenční příručka Ruby. Tato část práce bude obsahovat ukázky kódu a pro pochopení příkladů je důleţité rozpoznat, která část textu je programový kód, která výstup a která komentář. Všechny příklady budou vypisovány do růţového rámečku a pro snadnější optické odlišení bude pouţit jiný font. Typická ukázka kódu pak bude vypadat takto: # Takto jsou formátovány komentáře Toto je příkaz #=> Takto vypadá výstup
Komentáře se v Ruby zapisují znakem # a vše, co je napravo od něj, je ignorováno. Pro lepší grafické odlišení jsou navíc komentáře označeny zelenou barvou. Výstup aplikace je vţdy šedivou barvou a je uvozen značkou #=>, za kterou uţ následuje samotný výstup.
3.1 První aplikace Nejznámnější poučkou o Ruby je, ţe všechno v Ruby je objekt. Tato věta zaznívá o Ruby ze všech stran. Zdálo by se tedy logiké začít od objektů, kdyţ právě ty jsou pro Ruby natolik typické. Naše objevování Ruby ale začne něčím jednodušším a daleko klasičtějším - aplikací, která dokáţe na obrazovku vypsat text: Hello world. Napsat poprvé aplikaci v Ruby je snadnější, neţ se na první pohled můţe zdát. Aplikace nemusí být tvořena z ţádných tříd, ani nemusí obsahovat speciálně pojmenované metody. S trochou shovívavosti, lze aplikací nazvat jakýkoliv příkaz v Ruby, který je uloţen do souboru a interpretovaný Ruby interpretem. Přesnější označení takového souboru by bylo
29
spíše skript neţ aplikace. Ruby kód, který vypíše na obrazovku text Hello world, pak vypadá jednoduše takto: puts "Hello world"
Pro porovnání, takto vypadá stejná aplikace zapsaná v jazyku Java: class HelloWorldApp { public static void main(String[] args) { System.out.println("Hello World"); } }
Rozdíl mezi oběma aplikacemi je evidentní. Ruby obsahuje pouze příkaz, který se má vykonat a ţádný kód navíc. Díky tomu, ţe jde o skriptovací interpretovaný jazyk, interpret jen vyhodnotí jednotlivé řádky kódu a prezentuje výsledek. V tomto případě interpretuje příkaz puts (výpis na standardní výstup), který jako argument dostal řetězec Hello world.
3.2 Kam se poděly závorky Jedním z největších syntaktických rozdílů Ruby oproti Javě je pouţívání závorek u metod. V Ruby jsou totiţ tyto závorky pouze dobrovolné a nemusí se tedy vţdy uvádět. Většinou se závorky uvádějí jen, pokud to má za důsledek zpřehlednění kódu. Oba níţe uvedené příklady jsou zaměnitelné: puts ("Hello world") #=> Hello world puts "Hello world" #=> Hello world
Dalším nepřehlédnutelným rozdílem oproti Javě je pouţívání středníků na konci řádků. V Ruby se středníky na konci řádků nepíší. Přesto však má v Ruby středník své vyuţití. Pouţívá se k oddělení více příkazů, které se nacházejí na jednom řádku. S vyuţitím středníků je tedy moţné napsat například definici metody na jeden řádek: def say_hello_world; puts "Hello world!"; end
Je na programátorovi, aby zváţil, který zápis je pro něj v danou chvíli přehlednější a který lépe vystihuje jeho myšlenku. Zápis uvedený výše je ekvivalentní tomuto zápisu na více řádek: def say_hello_world puts "Hello world!" end
30
Sloţené závorky jsou další významný syntaktický prvek, který v Ruby oproti Javě chybí. Sloţené závorky, které se v Javě pouţívají pro definování bloku kódu kolem tříd nebo metod, jsou v Ruby nahrazeny klíčovým slovem end. Respektive počáteční závorka se nepouţívá vůbec a koncová závorka je nahrazena slovem end. Vše si můţeme demonstrovat na definici jednoduché třídy a metody v obou jazycích. Nejdříve v jazyku Java: public class Movie { public void play() { // some code to play the movie } }
A takto vypadá definice třídy a metody v jazyku Ruby: class Movie def play # some code to play the movie end end
3.3 Dynamické typování Dynamické typování je protikladem ke statickému typování, které je pouţito v Javě. Při dynamickém typování datový typ mají pouze hodnoty, ale nikoliv proměnné. Proto je moţné, aby proměnná odkazovala na hodnotu jakéhokoliv typu. Proměnné z tohoto důvodu nemusejí mít definovány datový typ a nemusejí být ani inicializovány před prvním pouţitím v programu. V Ruby je pak moţné udělat například toto: my_variable = "this is string" my_variable = 21 my_variable = 3.14
Datové typy jsou v dynamicky typovaných jazycích vyhodnocovány aţ za běhu programu, ve staticky typovaných jazycích k tomu dochází jiţ při překladu. Výhodou statického typování je bezesporu to, ţe překladač můţe odhalit chybu, ještě neţ je program spuštěn. V Ruby se chyba v typování projeví aţ jako chyba za běhu programu.
3.4 Datové typy V přecházející kapitole jsme se krátce seznámili se základními rozdíly mezi Javou a Ruby. Byly vyzdviţeny nejpatrnější rozdíly v syntaxi a představen pricnip dynamického typování. I 31
kdyţ se v Ruby datové typy nedefinují staticky tak jako například v Javě, neznamená to, ţe by nebyly důleţité, nebo ţe by dokonce nebyly potřeba. Stejně jako Java i Ruby obsahuje své interní datové typy pro čísla, řetězce, pole, asociativní pole a další a právě o těchto datových typech pojednává tato kapitola. 3.4.1 Čísla Ruby jako většina programovacích jazyků podporuje dva základní druhy čísel. Celá čísla a desetinná čísla. Můţe pak být trošku matoucí, ţe pro čísla existují v Ruby tři datové typy. Dva datové typy pro celá čísla a jeden datový typ pro čísla desetinná. Celá čísla mohou být jakéhokoliv rozsahu, vše záleţí pouze na volné paměti. Celá čísla (obvykle) v rozsahu od -230 do 230 - 1 nebo -262 do 262 - 1 jsou interně uloţeny v binární podobě a jsou reprezentovány instancí třídy Fixnum13. Čísla, která jsou mimo tento interval, jsou pak reprezentována instancí třídy Bignum. Programátor se však nemusí starat o to, jaký konkrétní datový typ je pouţit, protoţe je od všeho odstíněn a převod mezi těmito dvěma typy probíhá automaticky bez jeho vědomí. Automatický převod ilustruje následující příklad: number = 99 5.times do puts "Number is #{number} and its class is #{number.class}" number *= number end
Výsledek: Number is 99 and its class is Fixnum Number is 9801 and its class is Fixnum Number is 96059601 and its class is Fixnum Number is 9227446944279201 and its class is Fixnum Number is 85145777109487563964501441198401 and its class is Bignum
Ruby neomezuje programátory pouze na práci s čísly v desítkové soustavě. Desítková soustava je jen výchozí. Pro zapsání čísla v jiné číselné soustavě stačí jen k číslu jako prefix uvést symbol příslušné číselné soustavy. Osmičková číselná soustava má prefix 0, binární má prefix 0b, hexadecimální 0x a desítková, která je výchozí pak 0d: 125 #=> 125 #desítková číselná soustava 0d125 #=> 125 #desítková číselná soustava 0b1111101 #=> 125 #binární číselná soustava 0175 #=> 125 #osmičková číselná soustava
13
To zda bude tento rozsah -230 do 230 - 1 nebo -262 do 262 - 1 je určeno architekturou počítače, která můţe být buď 32 bitová nebo 64 bitová. 32
0x7d #=> 125 #hexadecimální číselná soustava
Pro větší přehlednost je moţné celá čísla zapisovat s podtrţítky. Pokud je číslo moc dlouhé a je tím sníţena jeho čitelnost, je moţné vyuţít podtrţítek namísto mezer14, které se pouţívají v běţném textu. Podtrţítka jsou interpretem jednoduše ignorována a ten k nim přistupuje jako k běţným číslům. Je tak moţné celá čísla zapsat následujícím způsobem: 12_000_000 #=> 12000000 1_500 + 2_500 #=> 4000
Celá čísla obsahují navíc pár velice uţitečných iterátorů. Jeden z nich byl uţ pouţit u příkladu výše, kdyţ jsme chtěli demonstrovat automatický převod mezi datovými typy. Iterátor nedělá nic jiného, neţ ţe opakovaně na jednotlivé prvky spouští blok kódu, který mu byl poskytnut. Tyto iterátory tedy vţdy pracují s bloky kódu jako parametrem. Nejběţněji pouţívané iterátory pro celá čísla: 3.times { print "Ahoj! " } #=> Ahoj! Ahoj! Ahoj! 1.upto(5) { |i| print i, " " } #=> 1 2 3 4 5 5.downto(1) { |i| print i, " " } #=> 5 4 3 2 1 5.step(25, 5) { |i| print i, " " } #=> 5 10 15 20 25
Kromě celých čísel Ruby obsahuje samozřejmě i desetinná čísla. Pro desetinná čísla slouţí v Ruby třída Float. Zápis desetinných čísel je moţný dvěma různými způsoby. První zápis je pomocí desetinné tečky. Druhý zápis je pomocí specifikace desítkového exponentu, která se zapíše za znak e. Před znakem e se vţdy musí vyskytovat číslo, jinak by interpret tento zápis vyhodnotil jako volání metody15. Desetinná čísla se pak zapisují takto: 156.48 #=> 156.48 1.5648e2 #=> 156.48
Převod mezi jednotlivými číselnými datovými typy se odehrává automaticky, je tak moţné udělat například následující zápis: 1.5648e2 + 3.52 + 40 #=> 200.0
14
Mezery se pro lepší čitelnost čísel pouţívají v našem kulturním prostředí. Podtrţítkem můţeme nahradit samozřejmě i jakýkoliv jiný oddělovač. 15 Zápis by pak vypadal například takto: 2.e. Kdyţ si uvědomíme, ţe vše v Ruby je objekt, tak přesně takto by vypadalo zasílání zprávy e číslu 2. 33
3.4.2 Řetězce Datovým typem, který se vyskytuje jak v Javě, tak v Ruby, je řetězec. Stejně jako v Javě, tak i v Ruby, jsou řetězce instancemi třídy String. Na rozdíl od Java jsou v Ruby třídou String reprezentovány nejen řetězce, ale i znaky. Těţko bychom tedy v Ruby hledali datový typ char nebo jemu podobný, který existuje v Javě. Řetězce jsou většinou tvořeny tisknutelnými
znaky, ale nemusí tomu tak být vţdy. Mimo jiné mohou řetězce obsahovat i binární data, které představují netisknutelné znaky. Binární data by se však do řetězců špatně zadávala a tak se místo nich pouţívají tzv. escape sekvence, které jsou pak nahrazeny při překladu korespondujícími binárními kódy. Nejběţnějším způsobem, jak vytvořit v Ruby řetězec je pomocí literálu. Řetězec je moţné samozřejmě také vytvořit jako instanci třídy String, ale s tímto přístupem se téměř není moţné setkat. Oba dva následující řádky kódu jsou tak ekvivalentní: puts "This is my first string" #=> This is my first string String.new("This is my first string") #=> This is my first string
Jak je uvedeno výše, řetězec se v Ruby tvoří nejčastěji pomocí literálu. Literál je posloupnost znaků, která je z kaţdé strany ohraničena oddělovačem. V Ruby existuje celkem pět moţných způsobů, jak vytvořit řetězec, resp. existuje pět různých druhů oddělovačů. Pouţitý oddělovač určuje, jakým způsobem budou zpracovány speciální znaky uvnitř řetězce. Nejjednodušším oddělovačem je apostrof (nejrychlejší zpracování řetězce). Uvnitř řetězce ohraničeného apostrofy se dvě zpětná lomítka nahradí jedním a zpětné lomítko následované apostrofem se nahradí apostrofem: puts 'This is my single quoted string with backslash "\\"' #=> This is my single quoted string with backslash "\" puts 'That\'s my second single quoted string with apostrophe' #=> That's my second single quoted string with apostrophe
Řetězce tvořeny uvozovkami umoţňují navíc vkládání escape sekvencí. Nejznámněší z nich je \n, která představuje kód pro vloţení nového řádku. Seznam všech pouţitelných escape sekvencí je moţné nalézt napříkad v [Thomas2004, str. 306]. Kromě moţnosti vkládání netisknutelných znaků, je moţné do řetězce vloţit libovolný Ruby výraz mezi znaky #{výraz}. Pokud vkládáme proměnnou instance, třídy nebo globální proměnnou, pak
nemusíme pouţívat sloţené závorky a proměnou můţeme psát rovnou za znak #. Kód je vyhodnocen a připojen k řetězci, jak můţeme vidět na následujícím příkladu: puts "Current date and time is #{ Time.now }"
34
#=> Current date and time is Thu Feb 24 08:57:14 +0100 2011 # $0 je globální proměnná, která označuje právě probíhající program puts "Program which is running is #$0" #=> Program which is running is irb
Existují ještě tři další způsoby, jak v Ruby vytvořit řetězec. Vzhledem k potřebám této práce, zde ale nebudou představeny. Jedná se o řetězce, které začínají speciálními znaky %Q, %q a řetězec typu here documents. Více podrobností lze nalézt v [Thomas2004, str. 58]. Zde byly představeny pouze dva hlavní a nejobvyklejší způsoby tvorby řetězců. 3.4.3 Symboly Datový typ Symbol, který je reprezentován stejnojmenou třídou, se nedá přirovnat k ţádnému datovému typu, který se nachází v Javě. Jedná se o unikátnost Ruby. Podobný datový typ můţeme nalézt například v jazycích LISP nebo Elrang, ale mezi nejznámnějšími jazyky bychom hledali zbytečně. Nejjednodušeji řečeno, slovy programátora, který vystupuje pod přezdívkou Why16: „Symboly jsou odlehčené řetězce“ [Why2005]. Jedná se o konstrukci, která je hlavně mezi začátečníky v Ruby špatně chápána a proto bude lepší ji hned ukázat na příkladu: output = :html if output == :html # process it like html else # process it another way end
Příklad představuje situaci, ve které se v programu rozhoduje, jakým způsobem bude zpracován výstup z aplikace. To jakým způsobem bude výstup zpracován, určuje proměnná output, která obsahuje název výstupu. V tomto konkrétním příkladu proměnná output
obsahuje symbol html a výstup by byl tedy zpracován jako html. Jak můţeme vidět, symboly se vytvářejí jako literály, které se skládají z dvojtečky a názvu symbolu. Stejného výsledku by bylo moţné dosáhnout, kdyby se na místo symbolu pouţil řetězec: output = 'html' if output == 'html' # process it like html else 16
Celá přezdívka zní Why the lucky stiff a zkráceně se pouţívá právě jako Why nebo také _why. Jedná se o jednu z nejznámnějších osob Ruby komunity, ale jeho pravá identita nikdy nebyla odhalena. Why je mimo jiné autorem jedné z nejoriginálnější učebnic Ruby [Why2005]. 35
# process it another way end
Oba výše uvedené příklady by bezesporu fungovaly správně a z hlediska logiky programu v nich není ţádný rozdíl. Určité rozdíly tu ale přesto existují. Prvním rozdílem je rychlost zpracování. Pokaţdé, kdyţ je někde v programu pouţit symbol, tak se tento symbol uloţí do speciální tabulky k ostatním symbolům. Kdyţ je pak stejný symbol pouţit na jiném místě v programu, nevytváří se nový objekt, ale je pouţit jiţ dříve vytvořený symbol. Oproti tomu, kdyţ v aplikaci pouţijeme řetězec, tak ten je vţdy vytvářen znovu (vţdy se vytváří nová instance). Kaţdý objekt má v Ruby své jednoznačné identifikační číslo. To, ţe se u řetězců vytváří vţdy nová instance a u symbolů nikoliv, tak můţeme demonstrovat na následujícím příkladu: puts :this_is_symbol.object_id #=> 333224 puts :this_is_symbol.object_id #=> 333224 puts 'this_is_string'.object_id #=> 15760920 puts 'this_is_string'.object_id #=> 15009312
Kromě toho, ţe jsou symboly oproti řetězcům rychleji zpracovány, má jejich vyuţití také sémantický význam. Jejich vyuţití má smysl v případech, kdy bychom například v Javě pouţili řetězec, ale přitom bychom tento řetězec nechtěli nikam vypisovat. Typickým případem je příklad výše, ve kterém se rozhoduje, jakým způsobem bude zpracován výstup z hypotetické aplikace. Řetězec by zde slouţil pouze jako argument pro rozhodování, ale nikam by se nevypisoval. Toto je tedy vhodný případ pro pouţití symbolu. Ve velké míře se symboly vyuţívají jako klíče v asociativních polích, jak bude ukázáno v kapitole 3.4.5 Asociativní pole. 3.4.4 Pole Datový typ pole existuje i v jazyku Java, avšak Ruby tento datový typ implementuje v některých případech trochu odlišně. Pole, které je reprezentováno instancí třídy Array, v sobě uchovává reference na objekty, které jsou v poli obsaţené. Kaţdý prvek pole tedy představuje odkaz na nějaký objekt. Pořadí prvků v poli je pevně dané a k jednotlivým prvkům se přistupuje pomocí indexu. Počáteční prvek pole má index nula. my_array = [ 'string', 3.14, :symbol ] my_array[0] #=> 'string' my_array[1] #=> 3.14 my_array[2] #=> :symbol my_array[3] #=> nil
36
Na prvním řádku můţeme vidět, jak lze v Ruby vytvořit pole pomocí literálu a zároveň to, ţe do pole můţeme vkládat prvky, které jsou různého datového typu. Pole lze samozřejmě vytvořit i pomocí klasického kontruktoru, který nabízí třída Array. Zápis by pak vypadal takto my_array = Array.new(). Na dalších řádcích příkladu, pak jiţ můţeme vidět, jak lze
k jednotlivým prvkům pole přistupovat pomocí metody [], které jako argument poskytneme index příslušného prvku. Pokud pole neobsahuje prvek s daným indexem, je vrácena hodnota nil. Metoda [], která slouţí pro získání jednotlivých prvků z pole, má i svůj ekvivalent []=,
který slouţí naopak pro zápis: my_favorite_animals = [] my_favorite_animals[0] = 'dog' my_favorite_animals[1] = 'kangaroo' my_favorite_animals[3] = 'Donald Duck' my_favorite_animals #=> ["dog", "kangaroo", nil, "Donald Duck"]
Nejdříve jsme si vytvořili prázdné pole, do kterého jsme poté pomocí metody []= a indexů přidali jednotlivé poloţky. Jak je vidět na výpisu pole, který je na posledním řádku, indexy nemusí jít popořadě. Na místo prázdných poloţek pole se jednoduše dosadil objekt nil. Můţe to vypadat, ţe pro manipulaci s polem se neobejdeme bez indexů, ale není tomu tak. Indexy se pouţívají pouze tehdy, pokud známe index prvku, se kterým chceme manipulovat. Chceme-li například přidat další prvek na konec pole, index k tomu znát nepotřebujeme. Pro přidání prvků nakonec pole slouţí metoda <<, která je důkazem toho, jak se snaţí být Ruby názorné. Znaky menšítek tvoří pomyslnou šipku, která znázorňuje, co se kam bude přidávat. Předchozí příklad by tak šel zapsat takto: my_favorite_animals = [] my_favorite_animals << 'dog' my_favorite_animals << 'kangaroo' my_favorite_animals << nil my_favorite_animals << 'Donald Duck' my_favorite_animals #=> ["dog", "kangaroo", nil, "Donald Duck"]
3.4.5 Asociativní pole Asociativní pole můţeme v literatuře nebo odborných článcích nalézt i pod označením mapy nebo slovníky. V Ruby je asociativní pole implementováno pomocí třídy Hash. Pole a asociativní pole toho mají společného více, neţ by se mohlo na první pohled zdát. Pole je mnoţina uspořádaných odkazů na objekty, kde se k jednotlivým prvkům pole přistupuje pomocí indexů. Hlavní odlišnost asociativního pole od klasického pole je, ţe index, který se pouţívá pro přístup k jednotlivým prvkům, nemusí být číslo, ale můţe to být libovolný objekt. 37
Prvky asociativního pole jsou tak vţdy tvořeny samotnou hodnotou a pak tzv. klíčem, který představuje jednoznačný identifikátor daného prvku. Klíčem v asociativním poli můţe být libovolný objekt, který musí splňovat pouze jednu podmínku. Kaţdý klíč musí být v rámci jednoho asociativního pole unikátní, tak aby mohl tvořit jednoznačný identifikátor. V Javě se implementaci asociativního pole, tak jak je v Ruby, asi nejvíce přibliţuje třída HashMap. Důleţitou změnu oproti klasickým představám o asociativním poli přináši verze Ruby 1.9.1. Od této verze si prvky v asociativním poli drţí pevné pořadí. To znamená, ţe prvky jsou uchovávány ve stejném pořadí, ve kterém byly do asociativního pole přidány. Ve všech dřívejších verzích Ruby platilo, ţe prvky asociativního pole byly ukládány v náhodném pořadí. Asociativní pole se tak ještě více přibliţuje poli, které je implementováno pomocí třídy Array. Nejjednodušším způsobem, jak lze v Ruby vytvořit asociativní pole, je stejně jako u klasického pole, pomocí literálu: personal_info = { :name => 'Tomáš Porazil', :sex => 'male' }
Literál je tvořen sloţenými závorkami, ve kterých se nachází jednotlivé prvky asociativního pole. Prvky jsou vţdy tvořeny dvojicí klíč a hodnota a jsou odděleny symbolem =>. Jednotlivé prvky jsou pak od sebe odděleny čárkou. V příkladu uvedeném výše jsou jako klíče zvoleny symboly a hodnotami jsou řetězce. Stejně jako v případě obyčejného pole, tak i u asociativního pole lze pro zápis vyuţít metodu []= a pro čtení naopak metodu []. Jediným rozdílem je, ţe namísto indexů se jako identifikátor prvků pouţije jejich klíč. Pokud prvek s námi poskytnutým klíčem neexistuje, je vrácena hodnota nil. currencies = {} currencies[:czech_republic] = 'Czech Crown' currencies[:france] = 'Euro' currencies[:croatia] = 'Kuna' currencies[:denmark] = 'Danish Krone' puts currencies[:czech_republic] #=> Czech Crown
3.4.6 Intervaly, posloupnosti Intervaly nebo sekvence jsou objekty, které můţeme v realitě běţně pozorovat kolem sebe. Kalendářní rok se například skládá z řady po sobě jdoucích měsíců, abeceda z řady po sobě jdoucích znaků a asi nejpřirozenější jsou pro nás intervaly číselné. Pro lepší ztvárnění reality Ruby obsahuje datový typ Range, který nám právě tyto intervaly pomáhá zachytit. Datový typ
38
Range má v Ruby tři základní vyuţití. Pouţívá se k implementaci posloupností, jako přepínač
v podmínkách a jako interval. Interval jako posloupnost Posloupnosti jsou charakterisické tím, ţe mají definován počáteční a koncový bod. Kaţdý prvek v posloupnosti zná svého předchůdce a následovníka, pakliţe oba existují. Jinými slovy, je posloupnost mnoţina ordinálních prvků. Posloupnosti lze v Ruby zapsat dvěma různými způsoby, přičemţ oba mají trošku jiný význam: # pozn. metoda to_a znamená "to array" a převádí daný objekt na pole 1..10 # (1..10).to_a #=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 1...10 # (1...10).to_a #=> [1, 2, 3, 4, 5, 6, 7, 8, 9]
První způsob (pomocí dvou teček) vytvoří posloupnost, která v sobě zahrnuje všechny prvky včetně počátečního a koncového bodu. Druhý zápis (pomocí tří teček) vytvoří instanci třídy Range, která neobsahuje koncový bod. V definici rozsahu se nemusí nacházet pouze čísla, ale mohou se vytvořit i posloupnosti znaků, nebo dokonce řetězců. Problémem je, ţe Ruby tvoří sekvence znaků pouze na základě anglické abecedy: ('a'..'h').to_a #=> ["a", "b", "c", "d", "e", "f", "g", "h"] ('car'..'cat').to_a #=> ["car", "cas", "cat"]
Kromě tvoření sekvencí ze znaků, řetězců a čísel lze tvořit sekvence i z uţivatelsky vytvořených objektů. Pro vytvoření rozsahu z uţivatelsky definovaných objektů musí objekt splňovat pár následujících podmínek. Objekt musí implementovat dvě metody:
Metodu succ, která vrátí následující objekt.
Metodu <=>, která slouţí pro porovnání dvou objektů. Tato metoda vrací hodnoty -1, 0, 1 v závislosti na tom, zda je první objekt niţší, roven nebo vyšší.
Velice umělým příkladem ukázky definování vlastní třídy, kterou lze pouţít jako posloupnost, můţe být níţe uvedená třída Month, která umoţňuje vytváření posloupností měsíců kalendářního roku: class Month VERBAL_MONTH = { 1 => "January", 2 => "February", 3 => "March", 4 => "April", 5 => "May", 6 => "June", 7 => "July", 8 => "August", 9 => "September", 10 => "October", 11 => "November", 12 => "December" } attr :month def initialize month @month = month
39
end def verbalise VERBAL_MONTH[@month] end def <=> other @month <=> other.month end def succ raise(IndexError, "Year has only 12 month") if @month >= 12 Month.new(@month.succ) end end
Pouţití této třídy by pak mohlo být například následujícím způsobem, kdy bychom chtěli definovat interval měsíců, ve kterých jsou prázdniny: holidays = Month.new(7)..Month.new(8) holidays.each { |month| puts month.verbalise } #=> Jully August
Interval jako podmínka Interval lze pouţít i jako podmínku. Nejedná se však o obyčejnou podmínku, ve které by se jednoduše vyhodnocovalo, zda je daný výraz pravdivý či ne. Interval má v podmínce úlohu přepínače. Podmínka se stane pravdivou, jakmile výraz vyhovuje počátečnímu bodu intervalu. Od této chvíle je výraz vţdy vyhodnocen jako pravdivý, aţ do doby, kdy výraz vyhovuje koncovému bodu intervalu a pomyslný přepínač se opět přepne do negativního stavu. Popsané chování demonstruje následující příklad: ["dog", "cow", "bird", "horse", "cat"].each do |animal| if (animal == "cow")..(animal == "horse") print(animal, " ") end end #=> cow bird horse
Postupně se prochází prvky pole a nic se nevypisuje, protoţe podmínka je vyhodnocena jako nesplněná aţ do doby, kdy se proměnná animal rovná řetězci cow. V tuto chvíli se sepne spínač a opět se vypne aţ v momentě, kdy se proměnná animal rovná řetězci horse. Interval
40
Poslední vyuţití instance třídy Range je v podobě intervalu. Pro zjištění zda se daný objekt nachází uvnitř intervalu, slouţí operátor ===, který vrací jednoduše true, pokud interval hodnotu obsahuje a false pokud nikoliv. Pouţití tohoto operátoru je velice jednoduché, jak ukazuje příklad, ve kterém se snaţíme zjistit, zda je daný prvek obsaţen v intervalu: (1..10) === 5 #=> true (1..10) === 11 #=> false ('a'..'d') === 'c' #=> true ('a'..'d') === 'e' #=> false
Velice časté vyuţití rozsahu jako intervalu je při větvení pomocí příkazu case, ve kterém interval slouţí jako návěstí. Zde je také vyuţit operátor === pro zjištění, zda se daný prvek v intervalu nachází, je ale pouţit skrytě. Jeho pouţití je při porovnávání hodnoty a intervalu, který je v návěstí: def verbalise_age age case age when 0..1: "baby" when 2..3: "toddler" when 4..12: "child" when 13..18: "adolescent" when 19..65: "adult" when 65..99: "old" when 99..130: "very old" else "death" end end puts "You are #{verbalise_age(5)}." #=> You are child. puts "You are #{verbalise_age(25)}." #=> You are adult. puts "You are #{verbalise_age(75)}." #=> You are old.
3.4.7 Regulární výrazy Regulární výrazy nejsou charakteristickým představitelem standardních datových typů, pokud se jedná o klasické (nejpouţívanější) programovací jazyky. Mezi základní datové typy patří řetězce (String), celá čísla (Integer), desetinná čísla (Float), pole (Array), nebo třeba asociativní pole (Hash) a ještě některé další. Regulární výrazy bychom mezi základními datovými typy marně hledali i v Javě. Vyjímku tvoří jazyk Ruby a pak také například Perl, nebo awk [Thomas2004]. Tyto jazyky si uvědomují (resp. jejich tvůrci), ţe i kdyţ jsou regulární výrazy pro většinu programátorů spíše kryptografické neţ samovysvětlující, jejich síla je natolik velká, ţe se je vyplatí zařadit bok po boku klasických datových typů.
41
Regulární výrazy se obecně pouţívají k prohledávání textů (řetězců). Na základě definovaného vzoru (regulárního výrazu) se prohledává daný text a hledá se shoda. Ve vzorech se nepouţívá pouze lineární text, ale vyuţívá se speciální syntaxe, pomocí které lze definovat například opakování znaků nebo celých slov, zda se má jednat o znaky nebo o čísla, nebo zda se má jednat například o netisknutelné znaky. Regulární výrazy tak představují svůj vlastní jazyk, který je definován v jiném jazyku. Výhodou je, ţe se syntaxe regulárních výrazů v jednotlivých programovacích jazycích příliš neliší a ve většině případů se respektují stejná syntaktická pravidla. Podrobná publikace, která do detailu popisuje regulární výrazy je například [Friedl2006] a detailní popis, který je přímo svázán s Ruby lze nalézt v [Thomas2004]. V této části věnované regulárním výrazům v Ruby si pouze krátce představíme jejich tvorbu, důleţité operátory, metody a třídy, které se k nim úzce váţí. Stejně jako řetězce i regulární výrazy můţeme v Ruby tvořit dvěma způsoby. První způsob je pomocí literálu, kdy příslušný výraz umístíme mezi lomítka a výraz má pak následující podobu /výraz/, nebo pomocí speciálního znaku %r a sloţených závorek a výraz pak vypadá takto %r{výraz}. Protoţe v Ruby je všechno objekt, tak regulární výraz můţeme vytvořit i klasickou cestou za pomoci konstruktoru. Regulární výrazy jsou instancemi třídy Regexp. Všechny tři následující způsoby tvorby regulárního výrazu jsou ekvivalentní: regexp = Regexp.new('ruby') regexp = /ruby/ regexp = %r{ruby}
Pro zjištění shody regulárního výrazu s řetězcem existuje na výběr několik moţností. Shodu lze zjistit buďto pomocí operátorů =~ (existuje shoda), !~ (neexistuje shoda) nebo pomocí metody instance Regexp#match, kterou poskytuje regulární výraz. Při rozhodování o tom, jakou metodu vyuţít, je rozhodující, s jakým výsledkem se nám bude lépe pracovat. Operátor =~, který hledá shodu, vrací jako výsledek pozici prvního znaku v řetězci, kde byla nalezena
shoda. V případě, ţe ţádná shoda nalezena nebyla, je vrácena hodnota false. Operátor !~ se pouţívá pro zjištění, zda řetězec naopak neobsahuje hledaný výraz. Výsledek příkazu je true, pokud výraz v řetězci obsaţen není a false, pokud ano. Pokud chceme komplexnější výsledek, můţeme vyuţít metodu match, která vrací instanci třídy MatchData, pokud byla nalezena shoda a false pokud nikoliv. Instance třídy MatchData poskytuje informace o celém zkoumaném řetězci. Výše popsané způsoby práce s regulárními výrazy jsou vidět na následujících příkladech: regexp = /Ruby/ regexp =~ 'Do you know Ruby the best programming language?' #=> 12
42
regexp !~ 'Do you know Ruby the best programming language?' #=> false regexp !~ 'Do you know Python programming language?' #=> true regexp.match('Do you know Ruby the best programming language?') #=> <#MatchData "Ruby">
3.5 Algoritmické konstrukce Imperativní programování se neobejde bez příkazů, pomocí kterých lze měnit průchod aplikací v závislosti na jejich stavu. Typickými programátorskými pomůckami jsou větvení a opakování. Stejně jako Java, tak i Ruby obsahuje celou řadu příkazů, které slouţí pro rozhodování, kterým směrem se má aplikace při svém běhu ubírat. V této části práce budou představeny ekvivalenty Javovských konstrukcí if-then, if-then-else, switch, for, while, do-while, tak jak se zapíší v jazyku Ruby. Některý příkazy byly pouţity jiţ
v příkladech v předchozích částech této práce, zde budou ale představeny obšírněji a probrány do větších detailů. 3.5.1 Příkazy pro větvení (if, unless, elsif, case) Stejně jako Java i Ruby pouţívá klasický příkaz if. Rozdíl v syntaxi je patrný hned na první pohled, protoţe v Ruby se nepouţívají k oddělení kontextu sloţené závorky, ale místo nich se k ukončení příkazu if pouţívá klíčové slovo end. sandwich = 'tommatos, cheese, salad, salami, egg' if sandwich.include? 'egg' puts 'I do not eat eggs!' else puts 'Yum! Yum!' end #=> I do not eat eggs!
Výše uvedený výraz sandwich.include? 'egg' je vyhodnocen jako pravdivý, pokud proměnná sandwich obsahuje řetězec 'egg'. Metoda include? vrací true, pokud řetězec obsahuje daný řetězec nebo znak. Nenechte se zmást otazníkem, který je součastí názvu metody. Otazník na konci metody je jmennou konvencí, kterou se označují metody, které vrací hodnoty true nebo false. Více se můţete o metodách dočíst v kapitole 3.7 Metody. Příkaz if lze v Ruby pouţít i bez klíčového slova end. V takovém případě se nejdříve uvádí výraz a aţ poté příkaz if s podmínkou, která se má vyhodnotit. Výhodou podobné syntaxe je, ţe mnohem více připomíná lidskou řeč (anglickou větu). V odborné literatuře se
43
této konstrukci, kdy je podmínka aţ za výrazem, říká modifikátor výrazu17. Tento zápis se vyuţívá hlavně pro svou kompaktní syntaxi, v které namísto třech řádků kódu, napíšeme jen jeden: puts 'It is really hot!' if temperature > 30
Kromě klasického příkazu if Ruby obsahuje ještě jeho velice blízkého příbuzného, příkaz unless. Tento příkaz není nic jiného, neţ negovaný příkaz if, ale je pozoruhodné, jakým
způsobem dokáţe tato jednoduchá konstrukce zpřehlednit kód. Pokud bychom to převedli do normální řeči, tak kdyţ někde v kódu uvedeme příkaz if, říkáme tím: Kdyţ je to pravda, proveď tento příkaz. Příkaz unless dělá přesný opak. Programu tím říkáme: Jestliţe to není pravda, proveď tento příkaz. Pouţití tohoto příkazu vede k čistšímu zápisu podmínky, ve které se nemusí vyuţívat operátor pro negaci: sentence = 'Stop or I will shoot!' unless sentence.include? '?' puts 'This sentence is not a question.' end #=> This sentence is not a question.
Příkaz unless, ale můţe v nesprávných rukou vést i k hůře čitelnému kódu, dalo by se říci aţ kryptografickému. Chris Whamond uvádí tři nejčastější chyby při pouţití tohoto příkazu [Whamond2010]:
Snaţit se vţdy pouţít pouze jednu podmínku (neřetězit podmínky pomocí operátoru &&).
Nepouţívat negace, unless je negací uţ sám o sobě.
Nikdy nepouţívat větev else, podmínka se tak stává skoro nečitelnou.
Stejného výsledku by šlo dosáhnout i pouţitím příkazu if. Musel by být ovšem pouţit operátor pro negaci, který by otočil logiku výrazu v podmínce a ta by se tak stala méně čitelnou: sentence = 'Stop or I will shoot!' if !sentence.include? '?' puts 'This sentence is not a question.' end #=> This sentence is not a question.
17
Přeloţeno z anglického Statement modifier
44
Velice často se stává, ţe program potřebujeme dělit na více neţ na dvě větve. Příkaz if nám umoţňuje směrovat chod programu pouze na základě jedné podmínky (podmínky mohou být samozřejmě řetězeny pomocí logických spojek AND a OR, ale ve výsledku výraz tvoří pouze jednu podmínku). Nic ve světě není pouze černobílé. Co tedy dělat napříkad v situaci, kdy nám nestačí zodpovědět pouze otázku, jestli je den nebo noc, ale chtěli bychom rozdělit den na specifičtější úseky. Ruby i Java nám v takovém případě poskytují stejné moţnosti. První moţností je vyuţití jiţ známého příkazu if, který rozšíříme o klíčové slovo elsif (v Javě se pouţívá konstrukce else-if). Toto klíčové slovo nám jednoduše umoţní do příkazu if přidat další podmínku. Výše zmíněný příklad s rozdělením dne, by pak v kódu mohl vypadat takto: if part_of_the_day == :morning # make a coffee elsif part_of_the_day == :forenoon # make a sandwich elsif part_of_the_day == :noon # cook a lunch elsif part_of_the_day == :afternoon # make a tea elsif part_of_the_day == :evening # cook a dinner else # do not bother me I am sleeping end
Příkaz elsif se většinou vyuţívá, pokud máme podmínky tři, maximálně čtyři. Při vyšším počtu podmínek se uţ zápis stává poněkud nepřehledným a je lepší vyuţít jinou konstrukci. Tato jiná konstrukce je v Javě známá pod jménem switch a v Ruby se jí říká case. Rozdíl je hlavně v názvu a syntaxi, ale pouţití obou příkazů je totoţné. I v Javě se v příkazu switch pouţívá klíčové slovo case, takţe tato konstrukce nemůţe být pro Java programátory aţ tak nepřehledná. Pomocí příkazu case by se náš příklad s rozdělením částí dne zapsal následujícícm způsobem: case part_of_the_day when :morning then # make a coffee when :forenoon then # make a sandwich when :noon then # cook a lunch when :afternoon then # make a tea when :evening then # cook a dinner else # do not bother me I am sleeping
45
end
Jak je vidět, pouţití příkazu case celý zápis velice zpřehlednilo. Zápis je také kratší o čtyři řádky. Pokud bychom se řídili filosofií, ţe čím méně řádků kódu, tím méně chyb, mohli bychom říci, ţe tento kód je méně náchylný k chybám. Za povšimnutí určitě stojí větev else, která odchytává případy, kdy podmínka nevyhovuje ani jedné z uvedených variant. V Javě je ekvivalentem větve else návěstí, které je označeno klíčovým slovem default. 3.5.2 Cykly Dobrá zpráva pro programátory, kteří k Ruby přecházejí z Javy je, ţe Ruby obsahuje pouze dva příkazy pro práci s cykly (příkazy while a until) a není zde tedy mnoho k učení. Jak jsme si mohli povšimnout, Ruby neobsahuje příkaz cyklu for jako Java. Respektive Ruby tento příkaz obsahuje, ale není to příkaz cyklu, ale iterátor - ty budou probrány aţ v následující kapitole. Další dobrou zprávou je, ţe tyto dva příkazy cyklu, které v Ruby existují, mají velice podobné pouţití jako příkaz while v jazyku Java. V této části práce budou představeny konstrukce, které jsou známy v Javě jako while a do-while. Výhodou je určitě i to, ţe syntaxe těchto příkazů je v Ruby velice podobná. Stačí si kolem podmínek odmyslet kulaté závorky a kolem těla cyklu si odmyslet sloţené závorky, respektive úvodní sloţenou závorku nahradit klíčovým slovem do a závěrečnou sloţenou závorku nahradit klíčovým slovem end. Stejně jako v Javě, cyklus while provádí opakování, dokud hodnota výrazu v podmínce nenabyde hodnoty false. Jinými slovy, pokud je hodnota true, cyklus probíhá pořád dál: i=0 while i < 10 print i i += 1 end #=> 0123456789
Příkaz if má svůj protějšek v příkazu unless a stejně tak má příkaz while svůj protějšek v příkazu until. Příkaz until v Javě neexistuje a jedná se o další z pokusů, jak se Ruby snaţí být co moţná nejexpresivnější. Cyklus s příkazem until se provádí, dokud hodnota výrazu v podmínce není true (přesný opak cyklu while). Takto by pak vypadal kód, kdyţ bychom opět chtěli vypsat čísla od 0 do 9, ale tentokrát pomocí příkazu until: i=0
46
until i == 10 print i i += 1 end #=> 0123456789
V předchozích příkladech, jsme si ukázali klasické cykly, které dokola provádějí příkazy, které v nich jsou vloţeny. Můţe ale nastat případ, kdy budeme chtít, aby se příslušné kusy kódu v cyklu provedli alespoň jednou a to nezávisle na vyhodnocené podmínce. V Javě se pro tento typ úlohy vyuţívá konstrukce do-while a ani Ruby nezůstává pozadu. Následující příklad demonstruje cyklus while, který se provede, i kdyţ je hodnota výrazu v podmínce false (cyklus se opakuje, pokud je hodnota výrazu v podmínce true): begin puts "Do-while statement transform from Java to Ruby" end while false #=> Do-while statement transform from Java to Ruby
Stejně jako lze příkaz if nebo unless pouţít aţ za výrazem jako tzv. modifikátor výrazu, stejně lze pouţít i příkaz while, jak můţeme vidět v předcházejícím příkladu. Kód, který je uzavřen do bloku begin-end, představuje výraz a while je pouţit jako modifikátor tohoto výrazu. Úplně stejně by šel pouţít i příkaz until.
3.6 Iterátory a bloky Příkazy pro cykly, které jsou definovány v Ruby, jsou skutečně základní, aţ primitivní. V Ruby dokoce neexistuje ani klasický příkaz cyklu for, který je znám z mnoha ostatních jazyků, jako je například Java, C nebo C++. Popravdě řečeno, Ruby ani nepotřebuje sofistikováné nástroje pro práci s cykly, protoţe ve většině případů, kdy je potřeba nějakým způsobem iterovat, se nepouţívají cykly, ale iterátory. Protoţe všechno v Ruby je objekt, tak dokonce i celá čísla mají své metody, které slouţí jako aerátory. To je důvodem, proč je v Ruby nepotřebný příkaz cyklu for. Příkaz cyklu for je jednoduše nahrazen iterátorem. Aby jsme byli korektní, klíčové slovo for se v Ruby vyskytuje, ale nejedná se o příkaz cyklu, ale o syntaktickou pomůcku, která vyuţívá iterátoru each. O tom ale aţ později. Pro pochopení iterátorů, je nedříve nutné nahlédnout alespoň částěčně na bloky kódu.
47
3.6.1 Bloky kódu Kdyţ Dave Thomas se svými kolegy píše v [Thomas2004] o blocích, zmiňují zde citaci jednoho z recenzentů knihy: „Toto je velice pěkná a zajímavá funkcionalita, pokud jste do teď nedávali pozor, je na čase abyste začali“. S tímto postřehem nelze neţ souhlasit. Bloky jsou jednou z nejzajímavějších struktur Ruby. V Ruby se pouţívají k implementaci zpětných volání18, k předávání částí kódu v aplikaci a k implementaci iterátorů. A jak se bloky kódu zapisují? Existují dva základní způsoby. Prvním způsobem je uzavřít kód do sloţených závorek a druhým je uzavřít kód mezi klíčová slova do a end: { puts 'This is a block' } do puts 'This is a block too' end
Oba uvedené zápisy jsou ekvivalentní. Pro rozhodnutí, kdy jaký zápis pouţít, se pouţívá jedno nepsané pravidlo - pro jednořádkový blok se pouţívají sloţené závorky a více řádkový blok se uzavírá mezi klíčová slova do a end. Bloky kódu samy o sobě nemají ţádný význam. Svůj význam dostanou, aţ kdyţ jsou volány jako parametr metody, která blok spustí. V metodě, která dostane blok jako parametr se blok spouští příkazem yield. def block_call_method puts 'Start of block_call_method' yield puts 'End of block_call_method' end block_call_method { puts 'This is my block' } #=>Start of block_call_method #=>This is my block #=>End of block_call_method
Metoda yield můţe obsahovat i parametry, které se dají následně pouţít v bloku kódu. Parametry metody yield se deklarují jako parametry jakékoliv jiné metody, zajímavější je pak vyuţití těchto parametrů uvnitř samotného bloku kódu. Parametry se v bloku kódu deklarují uvnitř samotného bloku a zapisují se mezi značky |. Na následujícím příkladu můţete vidět volání metody yield se dvěma parametry a jejich následné vyuţití ve víceřádkovém bloku kódu: def who_is_he
18
48
Přeloţeno z anglického Callback
yield("Tomas", 25) end who_is_he do |name, age| puts "His name is #{name} and he is #{age} years old." puts "After 25 years #{name} will be #{age + 25} years old." end #=> His name is Tomas and he is 25 years old. #=> After 25 years Tomas will be 50 years old.
Trochu sloţitějším příkladem vyuţití bloků v praxi je jejich vyuţití při počítání Fibonacciho posloupnosti. Příklad je s malými úpravami převzat z [Thomas2004, str. 47]: def fib_up_to(max) i1, i2 = 1, 1 # paralelní přiřazení (i1 = 1 a i2 = 1) while i1 <= max yield i1 i1, i2 = i2, i1+i2 end end fib_up_to(100) {|f| print f, " " } #=> 1 1 2 3 5 8 13 21 34 55 89
Příklad demonstruje sílu bloku kódu. Vyuţíváme metodu, která dokáţe spočítat Fibonacciho posloupnost, v které do bloku dostáváme jako parametry jednotlivá čísla posloupnosti a tato čísla můţeme dále libovolně zpracovat. Například je můţeme jednoduše vypsat tak, jak je dostaneme nebo s nimi nejdříve udělat výpočty a aţ po té je vypsat. Vše záleţí pouze na našem bloku kódu, který metodě poskytneme jako parametr. Bloky kódu tak představují velice flexibilní nástroj, který poskytuje téměř neomezené moţnosti. 3.6.2 Iterátory Předchozí krátká kapitola o blocích kódu představila základní koncept pouţití bloků a jejich syntaktický zápis. Alespoň částečná znalost a pochopení bloků kódu je totiţ nezbytná k pochopení iterátorů. Iterátory v Ruby z velké části nahrazují klasické cykly a jejich vyuţití je také mnohem častější neţ vyuţití cyklů. To zejména díky faktu, ţe se s nimi mnohem lépe pracuje. Iterátory nejsou ve své podstatě nic jiného neţ metody, které jako parametry přijímají bloky kódu. Aniţ bychom to zmiňovali, tak jsme se v této práci s jedním iterátorem jiţ setkali. Bylo to v kapitole 3.4.1 Čísla, ve které byl pouţit iterátor times, jenţ je definován pro
49
instance celých čísel (instance tříd Fixnum a Bignum). Pro připomenutí, zdrojový kód příkladu vypadal takto: number = 99 5.times do puts "Number is #{number} and its class is #{number.class}" number *= number end
Metoda times nedělá nic jiného, neţ ţe příslušný blok kódu spustí tolikrát, kolik je hodnota čísla, jehoţ metoda je volána. Blokem kódu, který je iterátoru times poslán jako parametr jsou v tomto případě dva řádky: puts "Number is #{number} and its class is #{number.class}" number *= number
Asi nejčastěji se lze v praxi s iterátory setkat při procházení kolekcí a to jak při procházení klasických polí (instance třídy Array), tak asociativních polí (instance třídy Hash). Mezi nejpouţívanější iterátor patří metoda each, která prochází jeden prvek kolekce po druhém a umoţňuje nám s ním pracovat. Například procházení polí pomocí klasických cyklů (například cyklu for), je zaloţeno na procházení prvků pole s vyuţitím indexů jednotlivých prvků pole. Nesporná výhoda iterátorů spočívá v tom, ţe se nepouţívají ţádné indexy a nemůţe se stát, ţe bychom indexovali prvek, který se v poli nenachází. Metoda each vţdy vrátí pouze prvky, které se v kolekci nacházejí. capitals = [ 'Rome', 'Paris', 'London' ] capitals.each do |capital| puts capital end #=> Rome #=> Paris #=> London
Příklad výše ukazuje jednoduché pouţití iterátoru each, v kterém jsou postupně procházeny jednotlivé prvky pole, jeţ jsou následně vypsány metodou puts. Do bloku je předáván parametr capital, představující odkaz na jednotlivé prvky v poli. Můţeme s ním pochopitelně dále pracovat. Jak jiţ bylo řečeno, v tomto případě neděláme nic jiného, neţ je výpis prvků na obrazovku. Na začátku kapitoly jsme slíbili, ţe se zde budeme zaobírat i klíčovým slovem for a jeho rolí. Teď je ta pravá chvíle se k němu vrátit. Výše uvedený příklad lze zapsat i pomocí 50
klíčového slova for, kdy zápis velice nápaadně připomíná tvorbu cyklů pomocí iterátoru v Javě. Zápis by pak vypadal takto: capitals = [ 'Rome', 'Paris', 'London' ] for capital in capitals do puts capital end
Tímto zápisem se ale nesmíme nechat zmýlit. Jedná se pouze o syntaktickou pomůcku. Ruby si pak tento zápis vnitřně upraví a pouţije iterátor each, jako my v prvním případě. Zápis pomocí příkazu for lze vyuţít vţdy, kdyţ má prvek, kolem kterého chceme iterovat definovánu metodu (iterátor), each. Je tak opět pouze na programátorovi, zda vyuţije syntaxi metody each nebo příkazu for. Velice podobně lze procházet i asociativní pole. K jeho procházení se také pouţívá metoda each, která ale do bloku kódu poskytuje tentokrát dva parametry. Prvním parametrem je identifikátor prvku v asociativním poli, tzv. klíč a druhým parametrem je hodnota. Příklad s městy tak můţeme rozšířit i o zemi, ve které se hlavní města nacházejí: capitals = { :Italy => 'Rome', :France => 'Paris', :England => 'London' } capitals.each do |country, capital| puts "Capital of #{country} is #{capital}." end #=> Capital of Italy is Rome. #=> Capital of France is Paris. #=> Capital of England is London.
Klíčová slova pro práci z iterátory a cykly Kromě samotných klíčových slov pouţívaných k definici cyklů nebo iterátorů, v Ruby existují ještě čtyři další klíčová slova, která se pouţívají ke změně průběhu cyklů, ale i iterátorů. Změnou průběhu cyklů rozumíme například přerušení cyklu nebo jeho opakování. Těmito čtyřmi klíčovými slovy jsou break, redo, next a retry: break: Příkaz se pouţívá k přerušení daného cyklu. Po pouţití tohoto příkazu
program zastaví procházení cyklu a začne zpracovávat příkazy, které se nacházejí za cyklem. redo:
Cyklus se vrátí na začátek s tím, ţe se jiţ znovu nevyhodnocuje podmínka cyklu (v případě iterátoru se nenačítá další prvek).
51
retry: Cyklus se vrátí na začátek stejně jako v případě příkazu redo, s tím rozdílem,
ţe se znovu vyhodnocuje podmínka cyklu. Cyklus probíhá celý znovu od začátku. next:
Po pouţití tohoto příkazu program skočí na konec těla cyklu a spustí další iteraci.
Zatímco příkazy break a next jsou na pochopení docela snadné, tak příkazy retry a redo vypadají velice podobně a rozdíl mezi nimi není tak lehce postřehnutelný.
V následujícím příkladu si tedy ukáţeme právě příkazy redo a retry a na vlice jednoduché ukázce budeme demonstrovat základní rozdíl: (0..3).each do |i| puts "value: #{i}" redo if i > 2 end #=> value: 0 #=> value: 1 #=> value: 2 #=> value: 3 #=> value: 3 #=> value: 3 # nekonečný cyklus
Jak je vidět, při příkazu redo se bez vyhodnocení podmínky (načtení dalšího prvku v případě iterátoru) znovu opakuje cyklus se stejnými hodnotami. V další ukázce je vidět rozdíl, který nastává při pouţití příkazu retry, při němţ se celý cyklus opakuje celý od začátku s původními (vychozími) hodnotami: (0..3).each do |i| puts "value: #{i}" retry if i > 2 end #=> value: 0 #=> value: 1 #=> value: 2 #=> value: 3 #=> value: 0 #=> value: 1 #=> value: 2 #=> value: 3
52
# nekonečný cyklus
3.7 Metody Metody patří k základním stavebním blokům kaţdého programu. Není proto divu, ţe jsme se s metodami setkali jiţ daleko dříve před toutu kapitolou. Různé metody byly pouţity v mnohých příkladech uvedených v této práci. Nyní se podíváme na metody trošku podrobněji, vysvětlíme si jejich syntaktický zápis a to jak se s nimi pracuje. Je nutné sjednotit terminologii a zmínit, ţe pro odlišení parametrů u metod je pouţita konvence převzatá z aglicky psané literatury. To znamená, ţe jsou formální parametry metod označovány jako parametry a skutečné parametry metod jsou označovány jako argumenty. Jako první se nabízí srovnání metod tak, jak je známe z Javy, s metodami v Ruby. Pustíme se tedy hned do příkladu. Představme si metodu, která nedělá nic jiného, neţ ţe dokáţe spočítat obsah plochy obdelníku. V Javě by zápis takové metody vypadal takto: public int computeRectangleArea(int width, int height) { return width * height; }
Metoda, která by měla stejnou funkcionalitu by se v Ruby zapsala takto: def compute_rectangle_area(width, height) return width * height end
Všimnout bychom si měli v první řadě faktu, ţe u metody v Ruby chybí modifikátor přístupu. Viditelnost metod uvnitř tříd se určuje pomocí speciálních bloků. Bez pouţití modifikátoru přístupu je metoda viditelná stejně, jako v Javě při pouţití klíčového slova public. Teď se toutu problematikou nebudeme více zabývat. Pojednává o ní samostatná kapitola 3.8.8 Modifikátory přístupu. Druhým klíčovým slovem v zápisu metody v Javě je slovo int, které označuje datový typ hodnoty, jeţ metoda vrací. I tuto část, bychom u metody v Ruby marně hledali. Můţe to znít zvláštně, ale v Ruby kaţdá metoda vrací hodnotu. Dokonce i v případě, ţe není pouţito klíčové slovo return, metoda vrací hodnotu. Touto hodnotou je hodnota posledního vyhodnoceného výrazu. Je uţ pouze na programátorovi, zda hodnotu, kterou metoda vrací, vyuţije nebo ne. Výše uvedená metoda v Ruby by tak měla stejný význam, i kdyby tělo metody neobsahovalo slovo return, ale pouze samotný výpočet. Z toho
53
samozřejmě vyplývá, ţe bychom v Ruby stejně marně hledali i ekvivalent klíčového slova void, které se pouţívá v Javě, protoţe jednoduše není třeba.
Po probrání odlišností mezi Javou a Ruby se jiţ můţeme zaměřit pouze na Ruby. Nejjednoduší zápis metody v Ruby je tvořen pouze klíčovým slovem def a názvem metody. Názvy metod začínají malými písmeny a je nepsaným pravidlem, ţe se u víceslovných názvů pouţívá podtrţítková notace. V názvech metod se můţe objevit i otazník nebo vykřičník. Metody, které končí otazníkem, obvykle vrací jako hodnotu true nebo false. Metody končící vykřičníkem obvykle označují nebezpečné metody nebo metody, které přímo modifikují svého příjmemce. Za návěstím metody následuje tělo metody a ta je pak celá uzavřena klíčovým slovem end. Pokud chceme definovat metodu, která bude přijímat parametry, uvádějí se názvy parametrů za název metody a jednotlivé parametry se oddělují čárkou. Parametry mohou být uzavřeny do kulatých závorek (tak jako je to u výše uvedeného příkladu), ale nemusí. Ekvivalentní zápis metody pro výpočet obsahu plochy obdelníku bez závorek kolem argumentů a bez klíčového slova return pak vypadá takto: def compute_rectangle_area width, height width * height end
V Ruby je moţné definovat implicitní hodnoty argumentů metody. Jinými slovko lze interpretovat tak, ţe metodu, která má definované výchozí hodnoty pro své argumenty, je moţné zavolat i bez těchto argumentů. Výchozí hodnoty pro argumenty se definují jednoduše přiřazením hodnoty do argumentu pomocí operátoru =. Podívejte se na následující příklad, po jehoţ shlédnutí bude snad vše trošku jasnější: def display_cool_people(person1= "Dave Thomas", person2 = "Chad Fowler", person3 = "Andy Hunt") "These people are cool: #{person1}, #{person2}, #{person3}!" end display_cool_people #=> These people are cool: Dave Thomas, Chad Fowler, Andy Hunt! display_cool_people("David Hansson") #=> These people are cool: David Hansson, Chad Fowler, Andy Hunt! display_cool_people("David Hansson", "Sam Ruby") #=> These people are cool: David Hansson, Sam Ruby, Andy Hunt! display_cool_people("David Hansson", "Sam Ruby", "Obie Fernandez") #=> These people are cool: David Hansson, Sam Ruby, Obie Fernandez!
54
3.7.1 Pojmenované argumenty Namísto obyčejných argumentů se v Ruby v hojné míře pouţívají tzv. pojmenované argumenty19. Ruby sice tento druh argumentů přímo nepodporuje20, ale komunita si pomohla pouţitím asociativních polí. Ve své podstatě jde o to, aby bylo moţné zadávat argumenty do metod v libovolném pořadí a mnoţství. Argumenty jsou pak uvnitř metody zpracovány na základě svého jména. Asociativní pole jsou k tomuto účelu přímo ideální, protoţe jako názvy argumentů se pouţívají symboly. Při volání metody ani nemusíme vytvářet instanci třídy Hash pomocí literálu, ale stačí pouze definovat klíče a hodnoty a Ruby z nich jiţ asociativní
pole vytvoří automaticky. Zdá se to jako drobnost, ale zápis je pak o dvě sloţené závorky chudší a tím pádem i čitelnejší. Následující příklad ukazuje vyuţití asociativních polí, jako pojmenovaných argumentů. Ve volání metody je pak v prvním případě asociativní pole vytvořeno pomocí literálu a v druhém případě pomocí úspornějšího zápisu. def display_country_info(country = {}) puts "Population of the #{country[:name]} is #{country[:population]}." end display_country_info({:name => "Czech Republic", :population => 10_500_000}) #=> Population of the Czech Republic is 10500000. display_country_info(:name => "Czech Republic", :population => 10_500_000) #=> Population of the Czech Republic is 10500000.
3.7.2 Libovolný počet argumentů Při tvorbě metody se můţeme dostat do situace, kdy nevíme, kolik argumentů by metoda měla být schopna přijmout. Ruby nám v takovémto případě poskytuje syntaktickou pomůcku. Před název argumentu stačí uvést hvězdičku (*) a s argumentem bude uvnitř metody automaticky nakládáno jako s polem. Při volání metody ovšem ţádné pole vytvářet nemusíme a můţeme psát jeden argument za druhým. Pouţití je vidět na následujícím příkladu: def store_programming_languages(*languages) puts "Languages class is #{languages.class}." puts "Stored languages are #{languages.join(", ")}." end store_programming_languages("Ruby", "Scala", "Python", "Java", "Lisp") #=> Languages class is Array.
19 20
Z anglického keyword arguments Ruby ve verzi 2.0 by pojmenované argumenty jiţ podporovat mělo 55
#=> Stored languages are Ruby, Scala, Python, Java, Lisp.
3.8 Třídy V Ruby je všechno objekt. Tato věta padla jiţ na začátku celé práce a nic na tomto výroku nebudeme měnit. V dosavadních kapitolách jsme se zabývali základními konstrukcemi jazyka a seznamovali jsme se s různými objekty, které jsou v Ruby definovány. Doposud jsme však nepracovali s vlastními objekty. V této části práce bude představen jeden ze základních principů objektově orientovaného programování a to je tvorba vlastních objektů, tvorba vlastních tříd. Koncept objektů, tříd a instancí zde nebude vysvětlován. Předpokládá se, ţe čtenář je s těmito termíny seznámen a má je dostatečně zaţité. Následující odstavce budou patřit tvorbě tříd v Ruby a všemu, co je se třídami v Ruby spojeno. To znamená, ţe se mimo jiné budeme zabývat syntaxí jejich zápisu, tvorbě instancí nebo například dědičností. Celá tato kapitola bude provázena jedním uceleným příkladem, na kterém bude vše názorně ukázáno a následně i dopodrobna vysvětleno. Příklad se bude týkat tvorby primitivního bankovního systému, resp. jen jeho části, která bude mít za úkol správu bankovních účtů. Vše je pro přehlednost kódu velice zjednodušeno, tak aby se zachovala názornost příkladů. V kapitole 2.4 Monkey patching jsme se zmiňovali o tom, ţe třídy v Ruby jsou definovány jako tzv. otevřené. To znamená, ţe třídu je moţné za běhu aplikace modifikovat a například do ní přidávat metody. Této vlastnosti budeme vyuţívat v následujících příkladech, v nichţ bude uvedena pouze část kódu třídy modifikující třídu stávající. Tím pádem nebudeme muset uvádět celý kód třídy znovu. V příkladech bude tedy uváděn pouze kód, který do třídy přidáváme a tím rozšiřujeme nebo měníme její funkcionalitu. 3.8.1 Definice třídy a konstruktor Aplikace má slouţit pro správu bankovních účtů. První objekt, který bude mít svou reprezentaci v systému, bude tedy bankovní účet, který bude zastoupen třídou Account. U kaţdého bankovního účtu budeme evidovat částku, která je na účtě uloţena, číslo účtu a jméno vlastníka účtu. Pro zjednodušení celého příkladu je vlastník účtu reprezentován pouze textovým řetězcem, v reálu by to byl spíše samostatný objekt s řadou dalších údajů. Takto popsaná třída Account vypadá v Ruby následovně: class Account def initialize(amount, account_number, owner) @amount = amount
56
@account_number = account_number @owner = owner end end
Třída se v Ruby definuje klíčovým slovem class, za nímţ následuje název třídy s velkým počátečním písmenem. Pokud je název třídy víceslovný, pouţívá se pro zápis velbloudí notace. Definice třídy končí klíčovým slovem end tak, jak je v Ruby zvykem uzavírat všechny bloky kódu. Jak je vidět, třída Account obsahuje pouze metodu initialize. Tato metoda má v Ruby speciální význam, jedná se totiţ o konstruktor třídy. V Ruby se konstruktor nejmenuje stejně jako název třídy jak tomu je v Javě, ale pokaţdé má stejný název initialize.
Poslední neznámou částí kódu jsou proměnné, které mají ve svém názvu jako první znak zavináč (@). Jedná se o proměnné instance. Jak jsme si jiţ mohli všimnout, Ruby hojně vyuţívá grafické výrazové prostředky ke zvýraznění určitých částí kódu. Tak jako například metody, které vrací hodnoty true nebo false, končí otazníkem, tak proměnné instance mají jako počáteční znak zavináč. Zatímco ale například otazník není ze syntaktického hlediska u názvu metod povinný, zavináč je u proměnných instance povinnou syntaktickou záleţitostí. Pro znalce Javy můţe být ještě trošku matoucí, ţe proměnné instance nejsou nikde definovány a rovnou se pouţívají. Toto je dáno dynamickými vlastnosmi jazyka. Jakmile jednou ve třídě pouţijeme proměnou se zavináčem (proměnou třídy), tato proměnná je automaticky dostupná v konextu celé třídy. 3.8.2 Tvorba instance a instanční metody (metody instance) Co nyní můţeme s třídou Account dělat? Jelikoţ jsme v této třídě definovali jedinou metodu a tou je kontruktor, nemůţeme s třídou dělat nic jiného, neţ vytvářet její instance21. S tvorbou instancí jsme se jiţ setkali a tak to nebude nic nového: account = Account.new(10_000, '123456789/0800', 'Porazil') #=> "#
"
Jak jsme jiţ viděli dříve, instance se v Ruby tvoří pomocí třídní metody new, která následně pomocí kontruktoru vytvoří instanci dané třídy. Metoda new má stejné parametry jako konstruktor. Po vytvoření třídy v konzoli, můţeme vidět, ţe nám Ruby interpret vrátil jako 21
Ve skutečnosti lze se třídou dělat mnohem více věcí neţ jen vytvářet instance, protoţe kaţdá třída v Ruby je potomkem třídy Object a tak od ní dědí značné mnoţství metod. 57
návratovou hodnotu textovou reprezentaci dané třídy. Textovou reprezentaci dané třídy poskytuje metoda inspect, kterou dědí kaţdá třída od třídy Object. Tuto metodu můţeme v dané třídě kdykoliv překrýt a upravit tak její výstup k obrazu svému. Nyní je na čase doplnit do třídy Account další funkcionalitu, aby dělala i něco uţitečnějšího. Na kaţdý bankovní účet by měly jít peníze vloţit a zároveň by z něj opět měly jít vybrat. Ve třídě account definujeme instanční metodu deposit, která bude slouţit k ukládání peněz (zvýšení stavu účtu) a metodu withdraw, která bude slouţit k výběru peněz (sníţení stavu účtu). class Account def withdraw amount if @amount - amount > 0 @amount -= amount else raise 'There is not enough money to withdraw on the account.' end end def deposit amount @amount += amount end end
Metoda withdraw slouţící k výběru peněz nejdříve zjišťuje, zda je na účtě dostatečné mnoţství peněz pro výběr. Pokud ano, příslušná částka se odečte z účtu. Pokud na účtě není dostatečné mnoţství peněz pro výběr dané částky, aplikace vyhodí vyjímku. Více o vyjímkách viz [Thomas2004, str. 129]. Metoda deposit, která slouţí pro vloţení peněz na účet přičte danou částku k mnoţství peněz, které jiţ na účtu je. Pouţití metod poté vypadá následujícím způsobem (instanci třídy Account jiţ máme vytvořenu z předchozích příkladů): account.deposit(2_000) #=> 120000 account.withdraw(8_000) #=> 4000 account.withdraw(5_000) #=> RuntimeError: On the account is not enough money to withdraw.
V kapitole 3.7 Metody jsme si říkali, ţe kaţdá metoda v Ruby vrací hodnotu i kdyţ neuvedeme klíčové slovo return a ţe touto hodnotou je výsledek vyhodnocení posledního řádku (výrazu) v metodě. Přesně tohoto principu můţeme vyuţít při testování funkčnosti třídy Account. Poslední řádky metod deposit a withdraw nám totiţ vrací vypočtené mnoţství
58
peněz na účtě (peněţní zůstatek). Po pouţití kaţdé z těchto metod tedy ihned vidíme, jaký měla vliv na mnoţství peněz na účtě. Poslední výběr peněz z účtu skončí vyjímkou, protoţe na účtě není dostatečné mnoţství peněz pro takto vysoký výběr. 3.8.3 Přístupové metody Nyní tedy máme třídu Account, jejíţ instance v sobě dokáţí uchovávat informace o stavu účtu, číslo účtu a jméno vlastníka účtu. Kromě toho jsme schopni na účet ukládat a vybírat z něj peníze. Zatím však nemáme moţnost, kdykoliv se zeptat jaké mnoţství peněz na účtě je. Tato informace je sice uloţena v proměnné instance @amount, avšak k proměnným instance nelze z vnějšku třídy přistupovat. Metoda, která by přistupovala k proměnné instance a vracela by její hodnotu by vypadala jednoduše takto: def amount @amount end
Protoţe je potřeba přístupu k proměnným instance z vnějšku třídy poměrně častá, existuje v Ruby zjednodušený zápis, který přístupové metody generuje automaticky. Kromě zůstatku na účtu budeme chtít mít z vnějšku třídy přístup i ke jménu majitele účtu a k číslu účtu. K definici přístupových metod pro čtení proměnných instance slouţí metoda attr_reader: class Account attr_reader :account_number, :owner, :amount end
Metoda attr_reader neudělá nic jiného, neţ ţe v pozadí vygeneruje stejné přístupové metody, které bychom definovali my. V pozadí se tedy vygeneruje kód, který je velice podobný tomutu: def account_number @account_number end def owner @owner end def amount @amount end
59
Výsledek naší změny můţeme ihned otestovat na naší instanci třídy Account. Měli bychom být nyní schopni přistupovat ke všem informacím, které jsme do instance vloţili, kdyţ jsme ji vytvářeli. Stav účtu se díky našim zásahům jiţ samozřejmě změnil: account.account_number #=> "123456789/0800" account.owner #=> "Porazil" account.amount #=> 4000
Nyní máme přístup ke všem proměnným instance a můţeme je číst. Můţeme je pouze číst a nemůţeme měnit jejich hodnoty. Tento stav je ţádoucí, protoţe nechceme, aby bylo moţné z venku třídy měnit částku, která je uloţena na účtě. Pro manipulaci s mnoţstvím peněz na účtě slouţí pouze metody deposit a withdraw. Můţeme chtít ale změnit majitele účtu a změnit tak obsah proměnné @owner. Nejdříve si ukáţeme, jak by vypadala naše vlastní metoda, která by tuto operaci umoţnila a hned v zápětí si ukáţeme konstrukt, který je pro tento případ definován v Ruby. Naše vlastní metoda by vypadala takto: def owner=(value) @owner = value end
Důleţité je všimnout si názvu metody, která v sobě zahrnuje znak přiřazení. Metoda se jmenuje owner=. My ale opět tuto metodu nemusíme definovat sami a můţeme vyuţít metody attr_writer, která za nás tuto metodu vygeneruje: class Account attr_writer :owner end
Metoda attr_writer vygenerovala metodu, pomocí níţ lze nyní změnit hodnotu proměnné @owner. Funkčnost této metody můţeme opět ihned ověřit. Nejdříve vypíšeme současnou
hodnotu proměnné @owner, poté změníme její hodnotu a nakonec opět její hodnotu vypíšeme, abychom zjistili, ţe se změna opravdu uskutečnila: account.owner #=> "Porazil" account.owner = 'Novák' account.owner #=> "Novák"
Kromě metod attr_reader a attr_writer, existuje v Ruby ještě jedna velice podobná metoda, kterou je attr_accessor. Jesliţe attr_reader vytváří metody pro čtení instančních proměnných a attr_writer metody pro změnu jejich hodnot, pak attr_accessor je
60
kombinací obojího. Jinými slovy attr_accesor vytváří jak metody pro čtení, tak metody pro zápis. 3.8.4 Dědičnost Koncept dědičnosti patří k základním myšlenkám objektově orientovaného programování a tak tu nebude vysvětlována jeho podstata ani důvody pro jeho zavedení. Ruby (stejně jako Java) podporuje pouze jednoduchou dědičnost. To znamená, ţe potomek můţe mít pouze jednoho rodiče. Principu podobnému vícenásobné dědičnosti je moţné dosáhnout pomocí modulů a tzv. mixinů, více v kapitole 3.9 Moduly. Budeme pokračovat v našem příkladu s bankovními účty. Máme zapracovat poţadavek, který říká, ţe kromě běţných účtů, je moţné mít i kreditový účet. Rozdíl mezi běţným a kreditovým účtem spočívá v tom, ţe na kreditovém účtu lze jít do záporných hodnot, za jejichţ nesplacení se následně platí úroky. Kreditový účet je v podstatě úvěrový účet. Obrazem kreditového účtu v naší aplikaci bude třída CreditAccount, která bude potomkem třídy Account: class CreditAccount < Account def initialize(account_number, owner, credit_amount) super(0, account_number, owner) @credit_amount = credit_amount end end
Na tomto příkladu jsou vidět dvě důleţité věci. První věcí, která stojí za povšimnutí, je způsob, jakým se v Ruby zapisuje dědičnost. Pro zápis se pouţívá znak menšítka (<), které znázorňuje šipku směřující od rodiče k potomkovi, CreditAccount < Account. Tento zápis říká, ţe třída CreditAccount je potomkem třídy Account. Druhou věcí, na kterou upozorníme je metoda super, která se nachází uvnitř metody initialize. Tato metoda zavolá metodu se stejným názvem v rámci rodiče dané třídy. V tomto případě se zavolá metoda initialize, která je definována ve třídě Account. Děje se tak mimo jiné proto, aby docházelo k co nejmenšímu opakování stejných částí kódu a byl tak dodrţen princip DRY22. Třída CreditAccount zavádí oproti své rodičovksé třídě navíc jednu proměnnou instance @credit_amount, která představuje maximální moţný limit, do kterého lze čerpat úvěr. Výchozí hodnota pro zůstatek na účtu je automaticky stanovena na nulu.
22
Zkratka z anglického spojení Don’t Repeat Yourself 61
Podívame-li se na metodu withdraw slouţící pro výběr z účtu, zjistíme, ţe tak jak je definována ve třídě Account, je pro potřeby ve třídě CreditAccount nepouţitelná. Metoda totiţ neumoţňuje uskutečnit výběr peněz, pokud by byl zůstatek na účtě po výběru menší neţ nula. Pricnip kreditového účtu je ale přesně opačný a je zaloţen právě na výběrech, které jdou do záporných hodnot. Metodu withdraw musíme tedy ve třídě CreditAccount překrýt a implementovat ji tak, aby umoţňovala záporný výběr z účtu aţ do stanovaného limitu: class CreditAccount < Account def withdraw amount if @credit_amount +@amount - amount > 0 @amount -= amount else raise 'You reached your credit account limit.' end end end
Vytvoříme instanci třídy CreditAccount a otestujeme, zda je metoda withdraw implementována podle našich předtav: credit_account = CreditAccount.new('987654321/0800', 'Porazil', 20_000) credit_account.withdraw(10_000) #=> -10000 credit_account.withdraw(15_000) #=> RuntimeError: You reached your credit account limit.
3.8.5 Konstanty Záporný zůstatek na kreditovém účtu nemůţe být napořád a po uplynutí určité doby, se platí jako penále úroky z dluţného částky. My se zde pro zjednodušení nebudeme zabývat sledováním časových období, ale chceme být schopni získat hodnotu úroků. Úroky se počítají jako procenta z dluţné částky. Jako první tedy zavedeme do naší třídy úrokovou míru: class CreditAccount < Account INTEREST_RATE = 1.5 end
Úrokovou míru jsme definovali jako konstantu s názvem INTEREST_RATE a přiřadili jsme jí hodnotu 1,5 jako 1,5%. Konstanty v Ruby začínají velkým počátečním písmenem (to je syntaktická nutnost, tak interpret rozezná konstanty od běţných proměnných), je však velice obvyklé, ţe je název konstant tvořen pouze velkými písmeny a u víceslovných názvů se mezery nahrazují podtrţítky. Konstanty jsou dostupné i mimo vlastní třídu a přistupuje se k nim následujícím způsobem: 62
CreditAccount::INTEREST_RATE #=> 1.5
Konstanty mají v Ruby jednu malou a velice nepříjemnou vlastnost. Hodnota konstanty lze za běhu programu změnit a program neskončí chybou. Je pravda, ţe při změně konstanty je vráceno varování, ale to je vše a nová hodnota se do konstanty bez větších problémů uloţí. Ukázka změny hodnoty konstanty: MY_CONSTANT = 'constant value' #=> 'constant value' MY_CONSTANT = 'more constant value' #=> warning: already initialized constant MY_CONSTANT #=> 'more constant value'
3.8.6 Virtuální atributy Definovanou konstantu INTEREST_RATE nyní můţeme vyuţít pro výpočet úroků z dluţné částky. Úroky budeme definovat jako tzv. virtuální atribut. Vysvětlení toho, co jsou to virtuální atributy, bude následovat aţ po uvedení příkladu, aby bylo vše na čem vysvětlovat. Úroky z dluţné částky získáme následujícím způsobem: class CreditAccount < Account def interest return 0 unless @amount < 0 return (@amount * INTEREST_RATE/100).round(2).abs end end
Pokud zůstatek na účtě není menší neţ nula, úroky jsou nulové. Jakmile je ale zůstatek na účtě větší neţ nula, jsou z této částky počítány úroky na základě definované úrokové míry. Částka je poté ještě zaukrouhlena na dvě desetinná místa a je z ní získáno absolutní číslo, aby výsledek nebyl se záporným znaménkem. Nyní můţeme zjistit, kolik činí úroky z dluţné částky: credit_account.amount #=> -10000 credit_account.interest #=> 150
Úroky z dluţné částky 10 000 Kč činí 150 Kč, coţ je při úrokové míře 1,5% správný výsledek. Pro získání úroku byl vyuţit virtuální atribut interest. Ve skutečosti je interest metoda a nyní si vysvětlíme, proč se v Ruby některým metodám říká atributy a některým virtuální atributy. Kdyţ vytváříme třídu, rozhodujeme se nad tím, jakým způsobem bude zachycen vnitřní stav třídy a jak bude tento stav reprezentován navenek (jak budou se třídou
63
spolupracovat ostatní třídy). Zatímco vnitřní stav třídy je uchováván v instančních proměnných, vnější stav třídy je reprezentován pomocí atributů. Metody třídy pak definují akce, které lze se třídou provádět. Je důleţité si uvědomit, ţe se třídami se v Ruby komunikuje pouze prostřednitvím metod, protoţe všechny proměnné instance jsou v Ruby viditelné pouze v kontextu dané instance a z vnějšku k nim lze přistupovat pouze prostřednictvím metod. Rozdělení metod na atributy, které představují stav třídy, a metody, které představují opravdové akce, tak přispívá k pochopení tříd a jakým způsobem s nimi lze pracovat. Nyní se konečně dostáváme i k virtuálním atributům. Virtuální atributy jsou podmnoţinou atributů. Klasické atributy představují pouze převod vnitřního stavu třídy (proměnné instance) do vnějšího prostředí. Virtuální atributy na rozdíl od těch klasických nemají svou vnitřní reprezentaci a často jsou získávány výpočtem nebo nějakým sloţitějším postupem. Jinými slovy virtuální atributy navenek představují stav třídy, uvnitř třídy však nemají svou reprezentaci a jsou odvozeny z ostatních vnitřních stavů. Stejně tak je tomu v našem případě, kdy virtuální atribut interest nemá ve třídě svou vnitřní reprezentaci a je odvozen na základě zůstatku na účtě (@amount) a úrokové míry (INTEREST_RATE). Ve svém důsledku se jedná pouze o terminologii, která je navíc závislá na subjektivním vnímání. Výše uvedené rozdělení vychází z [Thomas2004, str. 30] a autor této práce se s tímto rozdělením plně ztotoţňuje. 3.8.7 Proměnné třídy a metody třídy Prozatím jsme hovořili pouze o atributech a metodách, které se pojili přímo s konkrétními instancemi objektů. Můţe ale nastat situace, kdy budeme chtít, aby byla určitá vlastnost sdílena napříč všemi instancemi určitého objektu. To je chvíle, kdy přicházejí na řadu proměnné třídy a metody třídy. Jedná se o metody, které se nepojí s konkrétní instancí, ale jsou definovány přímo ve třídě, ze které se instance tvoří. Tím, ţe nejsou svázány s konkrétní instancí, jim umoţňuje, aby byly sdíleny skrz všechny instance objektu. Proměnné třídy i třídní metody se samozřejmě pouţívají i v Javě a nejedná se tedy o ţádný nový konstrukt, se kterým by přišlo aţ Ruby. class Person @@number_of_people = 0 def initialize @@number_of_people += 1 puts "New great person was created." end def Person.number_of_people @@number_of_people
64
end end
Jestliţe se proměnné instance tvoří pomocí jednoho zavináče, pak třídní proměnné se tvoří pomocí dvou zavináčů. @@number_of_people je tedy třídní proměnou třídy Person. Námi definovaná proměnná třídy slouţí k zachycení počtu vytvořených instancí třídy Person. Pokaţdé, kdyţ se vytvoří nová instance, tak je hodnota proměnné @@number_of_people zvýšena o jedničku. Třídní proměnné jsou viditelné pouze uvnitř třídy a jejích instancí. Pokud chceme zvnějšku zjistit hodnotu proměnné @@number_of_people, musíme vytvořit třídní metodu, která
nám
vrátí
její
hodnotu.
Z tohoto
důvodu
jsme
vytvořili
metodu
Person.number_of_people. Třídní metody se tvoří tak, ţe nejdříve uvedeme název třídy a
za tečkou poté název metody. Pouţití třídy Person vypadá následujícím způsobem: Person.number_of_people #=> 0 Person.new #=> New great person was created. Person.new #=> New great person was created. Person.new #=> New great person was created. Person.number_of_people #=> 3
Pro úplnost je ještě potřeba uvést, ţe pro vytváření třídních metod existuje více způsobů zápisu. Jedním moţným zápisem je uvádět před název metody název třídy. Tento způsob jsme pouţili v našem příkladu. Druhým a velice častým způsobem je zápis, v němţ se název třídy nahradí klíčovým slovem self. Výraz self odkazuje na objekt, v jehoţ kontextu byl pouţit. Definice metody třídy pomocí klíčového slova self by pak vypadala takto: class Person def self.number_of_people @@number_of_people end end
3.8.8 Modifikátory přístupu Kdyţ vytváříme třídu, vţdy dojdeme do momentu, ve kterém se rozhodujeme, které části třídy budeme chtít mít viditelné pro okolí a naopak, které budeme chtít, aby byly před okolím skryty. Vytváříme tak rozhraní třídy pomocí, kterého lze s danou třídou komunikovat. Provázanost tříd a jejich vzájemná závislost na implementačních detailech by měla být co nejmenší. Třídy by měly být závislé pouze na poskytnutém rozhraní a i to by mělo být
65
navrţeno tak, aby umoţňovalo změnu implementace třídy, aniţ by muselo dojít ke změně kódu tříd, které jsou na tomto rozhraní závislé. Jelikoţ se v Ruby nedá z vnějšku přistupovat k proměnným ve třídě, rozhraní třídy je definováno pouze prostřednitvím metod, které třída poskytuje svému okolí. Viditelnost metod lze nastavit pomocí tří příkazů: public:
Takto označené metody mohou být pouţívány kýmkoliv. Ţádné omezení přístupu není aplikováno. Všechny metody jsou ve výchozím stavu veřejné, neboli public. Jedinou vyjímku tvoří metoda initialize, která je automaticky označena modifikátorem přístupu private.
private:
Metody označené tímto modifikátorem jsou soukromé. Takovéto metody mohou být volány pouze v kontextu daného objektu. Ţádný jiný objekt ani třída nemá právo k těmto metodám přistupovat, dokonce ani objekt, který je odvozen od stejné třídy.
protected: K takto označeným metodám je přístup pouze v rámci daného objektu a
v rámci potomků tohoto objektu. Ruby je dynamickým jazykem ve všech svých aspektech a ne jinak je tomu i při vyhodnocování přístupu k metodám. K vyhodnocování dochází dynamicky aţ za běhu programu a ne při překladu, tak jak to bývá běţné ve statických programovacích jazycích. Nelze tedy například předem odhalit, zda je v programu volána metoda, ke které nelze přistupovat. Modifikátory přístupu je moţné pouţít dvěma různými způsoby. Prvním způsobem je jejich pouţití bez argumentů, kdy je kaţdá následující metoda podřízena daným přístupovým pravidlům. Pouţití si ukáţeme na následujícím příkladu: Class MyClass def method_public_by_default end private def private_method end protected def protected_method end public def another_public_method end end
66
Druhým moţným způsobem pouţití, je vypsání názvů metod, které se mají řídit danými přístupovými pravidly jako argumentů za přístupovou metodu. Jako argumenty se uvádějí názvy metod ve tvaru symbolů. Pokud vám to není jasné, podívejte se na následující příklad, který je ekvivalentem příkladu výše: Class MyClass private :private_method protected :protected_method def method_public_by_default end def private_method end def protected_method end def another_public_method end end
Metody, které chceme, aby zůstaly veřejné (modifikátor přístupu public), nemusíme ţádným způsobem označovat, protoţe jak jsme si říkali na začátku této subkapitoly, metody jsou veřejné samy od sebe (ve výchozím nastavení).
3.9 Moduly Moduly poskytují mechanismus, s jehoţ pomocí lze organizovat existující kód, který spolu určitým způsobem souvisí, do samostatných struktur. Existujícím kódem se myslí třídy, metody, konstanty a ve výjimečných případech atributy. Důvod, proč se v modulech nepouţívají atributy, bude objasněn později. Někoho by mohlo napadnout, ţe pro organizaci souvisejícího kódu jiţ slouţí třídy. Moduly však mají oproti třídám jednu zásadní vlastnost, nelze vytvářet jejich instance. Modul slouţí pouze jako obálka pro kód, který je v něm uveden. Moduly mají dvě základní vyuţití: 1. Pouţívají se jako jmenné prostory. 2. Vyuţívají se jako tzv. mixiny, kdy se přidávají do existujících tříd. Syntaxe pro zápis modulů je stejná jako syntaxe pro zápis tříd, pouze se klíčové slovo class nahradí klíčovým slovem module. Název modulu se píše s velkým počátečním
písmenem, a pokud je název víceslovný, pak se pouţívá velbloudí notace. Definice modulu končí klíčovým slovem end, stejně jako definice třídy. module ThisIsMyFirstModule
67
# code inside a module end
3.9.1 Jmenné prostory Prvním vyuţitím modulů je jejich vyuţití jako jmenných prostorů. V takovém případě je modul vyuţíván pouze jako schránka pro kód, který je v modulu obsaţen. Veškerý kód uvnitř modulu se z vnějšku volá s prefixem, který tvoří název modulu. Moţnost uschovat určitou funkcionalitu do jmenného prostoru je důleţitá z následujícího důvodu. Programování je z velké části o kombinování znovupouţitelných částí kódu a můţe se stát, ţe narazíme na případ, kdy chceme v našem programu vyuţít dva soubory, přičemţ kaţdý slouţí k jinému účelu, ale oba obsahují třídu se stejným názvem. Představme si situaci, kdy programujeme golfovou aplikaci a chceme v ní vyuţít třídy, které jsme jiţ kdysi napsali. Jedná se o třídy, které reprezentují golfový klub a golfovou hůl. Podívejme se, jak by vypadala situace, do které bychom se dostali, kdybychom nevyuţili jmenné prostory. Pro názornost je v ukázce veškerý kód uveden, jako kdyby se nacházel v jednom souboru. class Club # code for a club as a sport unit def initialize puts "Club as a sport unit." end end class Club # code for a club as a sport equipment def initialize puts "Club as a sport equipment." end end Club.new #=> Club as a sport equipment.
Golfový klub i golfová hůl se v angličtině označují stejným slovem club, a proto i obě dvě pouţité třídy nesou označení Club. Kdyţ v aplikaci chceme vytvořit novou instanci třídy Club, vyhrává ta třída, která je definována jako poslední. V našem případě je to třída Club,
která reprezentuje golfovou hůl. Řešení výše vzniklého problému je velice jednoduché. Stačí vyuţít moduly a pouţít je jako jmenné prostory. Kaţdou třídu obalíme modulem, který ji
68
uzavře do samostatného jmenného prostoru a tím umoţní její jednoznačnou identifikaci v rámci aplikace. module GolfUnit class Club # code for a club as a sport unit def initialize puts "Club as a sport unit." end end end module GolfEquipment class Club # code for a club as a sport equipment def initialize puts "Club as a sport equipment." end end end GolfUnit::Club.new #=> Club as a sport unit. GolfEquipment::Club.new #=> Club as a sport equipment.
Ke třídám uvnitř modulu se přistupuje pomocí dvojtečkové notace, kdy se nejdříve napíše název modulu, poté dvě dvojtečky a nakonec název třídy. Stejným způsobem by se přistupovalo i ke konstantám. Pouze pro přístup k metodám modulu lze vyuţít klasické jednotečkové notace. 3.9.2 Mixiny Na začátku této podkalitoly pojednávající o modulech jsme uvedli, ţe moduly nemohou vytvářet své instance. Toto tvrzení samozřejmě platí i nadále. Jestliţe ale moduly nemohou vytvářet instance, co se děje s instančními metodami definovanými uvnitř modulu? Moduly mají ještě jednu zásadní vlastnost, lze je namixovat (namíchat) do tříd. V tu chvíli přicházejí ke slovu instanční metody, které se rázem stávají instančními metodami třídy, do které je modul namixován. Pro namixování modulu do třídy se pouţívá metoda include. module Inspector def about_me puts "#{self.class.name}: #{self.to_s}" end end class OrdinaryClass
69
include Inspector end OrdinaryClass.new.about_me => #OrdinaryClass: #
Metoda include zajistí, ţe se instanční metody modulu stanou instančními metodami třídy. Je potřeba upozornit na to, ţe se do třídy nevkládá kód modulu, ale jen reference na modul. Změní-li se kód modulu, promítnou se tyto změny i do tříd, ve kterých je modul namixován. Vyuţití modulů tímto způsobem představuje velkou felxibilitu. Jestliţe jsme říkali, ţe Ruby nepodporuje vícenásobnou dědičnost, tak mixováním modulů do tříd, lze dosáhnout velice podobného efektu. Největší síla mixování modulů do tříd vyplouvá na povrch, pokud spolu kód třídy a modulu navzájem spolupracuje. Díky nejrůznějším modulům tak můţeme do třídy zadarmo získat funkcionalitu, jejíţ implementace by nám zabrala nemalé mnoţství času. Mixiny tak představují cestu, jak můţeme rozšiřovat funkcionalitu tříd velice transparentním způsobem. Příkladem modulu ze standardní knihovny, který lze namixovat do třídy a získat téměř bez práce mnoţství funkcionality je modul Comparable. Tento modul slouţí pro porovnávání objektů a poskytuje následující metody pro porovnávání: <, <=, ==, =>, > a between?. Po třídě, do které je namixován vyţaduje pouze metodu <=>, která slouţí pro porovnání dvou objektů. Porovnání dvou objektů a <=> b, můţe nabývat následujích hodnot: -1: objekt a je menší neţ objekt b 0: oba objekty jsou rovny 1: objekt a je větší neţ objekt b class Mountain include Comparable attr_reader :height def initialize(name, height) @name = name @height = height end def <=>(other) @height <=> other.height end end mount_everest = Mountain.new("Mount Everest", 8848) snezka = Mountain.new("Sněžka", 1602)
70
mount_everest < snezka #=> false mount_everest <= snezka #=> false mount_everest == snezka #=> false mount_everest > snezka #=> true mount_everest >= snezka #=> true
71
72
4 Programovací techniky Předcházející kapitola poslouţila jako kratičký (určitě ne vyčerpávající) úvod do jazyka Ruby a měla za úkol vybavit čtenáře znalostmi, které jsou nezbytné pro pochopení následujících částí práce. Minulá kapitola byla jen zahřívacím kolem oproti tomu, co bude následovat. Nyní budou představeny koncepty, které z Ruby dělají tak zajímavý programovací jazyk. Budeme se zaobírat principem dynamického typování, vyuţitím zpětných volání, funkcionálním programováním a v neposlední řadě tvorbou doménově specifického jazyka v Ruby. Kapitola má za cíl představit Ruby z co moţná nejvíce úhlů pohledu, aby byla vyzdvyţena jeho variabilita a různé moţnosti uplatnění.
4.1 Dynamické typování Přechod od striktně typových jazyků jako je Java nebo C# k jazykům, které vyuţívají dynamické typování, můţe být pro někoho matoucí aţ frustrující. Jedná se o přechod ze světa, kde má kaţdá proměnná určený datový typ do světa, ve kterém proměnná datový typ nemá, protoţe datový typ se určuje aţ podle hodnoty, na kterou proměnná odkazuje. Někdy se mylně tvrdí, ţe Ruby není silně typový jazyk, protoţe není třeba explicitně definovat datové typy. Tento výrok, ale mísí dohromady dva různé termíny. Na jedné straně je třeba od sebe odlišovat jazyky typované staticky a dynamicky. Na druhé straně je třeba odlišovat jazyky typované silně a slabě. Statické nebo dynamické typování se liší podle toho, kdy dochází k vyhodnocování informace o datovém typu. Při statickém typování je datový typ objektu znám jiţ při překladu. Při dynamickém typování dochází k vyhodnocení datového typu aţ za běhu programu. Důsledkem tohoto přístupu nemají datový typ proměnné, ale hodnoty. U dynamicky typovaných jazyků je tedy datový typ určen pouze hodnotou, která je v proměnné uloţena. To je důvod, proč můţe jedna a ta samá proměnná v různých částech programu představovat hodnoty různých datových typů. Ruby je dynamický, silně typovaný jazyk. Ruby není o nic méně typovaný jazyk neţ Java. V Ruby datové typy existují a jediný rozdíl spočívá v tom, ţe nemusejí být deklarovány. Navíc jsou tyto datové typy velice silně dodrţovány. Deklarace datových typů nemá se slabým či silným typováním nic společného. V programovacích jazycích, ve kterých jsou datové typy dodrţovány slabě lze například provést tento zápis, který neskončí chybou:
73
2 + '2' #=> '22'
Konkrétně tento zápis i s tímto výsledkem lze provést v Javascriptu. V Ruby podobný zápis chybou skončí. Tento příklad ukazuje základní rozdíl mezi slabě a silně typovými jazyky. V Ruby ani v Javascriptu není třeba definovat datové typy a přesto Ruby na jejich dodrţování trvá a Javascript nikoliv. Někdy bývá jako argument, který svědčí pro jazyky s deklarativním typováním, ţe jsou tyto jazyky méně chybové a hodí se tak pro větší a robustnější aplikace. Čas, ale sám ukázal, ţe tomu tak není a ţe Ruby není hračkou pouze pro skupinu nadšenců nebo pro projekty malé velikosti. Obavy z větší chybovosti se nenaplnily, a pokud se Ruby programátorů zeptáte, jak často mají problém s tím, ţe by program skončil chybou kvůli špatnému datovému typu, velice pravděpodobně odpoví, ţe snad někdy. Z velké části je to také dané kulturou psaní kódu, v níţ se preferují krátké metody, které jsou natolik průhledné, ţe v nich lze udělat chybu v typování jen velice obtíţně. Druhým aspektem je pak vyuţití testování, kdy se testování kódu nebere jako moţnost, ale jako povinnost. 4.1.1 Duck typing Duck typing je vlastnost jazyka, která vychází z toho, ţe není nutné deklarovat ţádné datové typy. V Ruby (a v ostatních jazycích, ve kterých není deklarace datových typů povinná) se k objektům nepřistupuje na základě jejich datového typu, ale na základě vlastností, nebo lépe řečeno, na základě metod, na jejichţ volání umí odpovědět. Duck typing se řídí heslem: pokud vypadá jako kachna a chodí jako kachna, pak to bude kachna. Objekt je tak posuzován pouze na základě svých vlastností. Staticky typované jazyky lze přirovnat k aristokracii. Pořád se totiţ pracuje s rodokmenem objektu a s tím, kdo byli předkové, prapředkové a tak dále. Aplikace se rozhoduje na základě původu objektu. Pokud staticky typované jazyky přirovnáme k aristokracii, pak dynamicky typované jazyky můţeme přirovnat k meritokracii23. Kaţdý objekt je posuzován pouze na základě svých schopností a nikoliv původu. Je důleţité, co objekt umí a ne z jakého rodokmenu pochází. 4.1.2 Třídy nejsou datové typy Vyuţití přístupu, kterému se postupem času začalo říkat duck typing vyţaduje jiný způsob přemýšlení a tím pádem i jiný způsob psaní programů. Na začátku je důleţité si uvědomit, co 23
Elitní skupina lidí, jejichţ postavení je určováno na základě schopností a talentu, ne na základě postavení a bohatství. 74
je to datový typ. Běţně se za datový typ pokládá třída. Ale i Java jakoţto staticky typovaný jazyk nám ukazuje, ţe datovým typem nemusí být vţdy třída. Vezměme následující příklad: Customer customer; customer = database.findCustomer("porazil");
Výše uvedený Java kód říká, ţe proměnná customer, je datového typu Customer. Znamená to ovšem, ţe proměnná customer je datového typu Customer a ten odpovídá třídě Customer? Nemusí tomu tak být, protoţe Customer můţe být rozhraní a pak datovým typem není třída, ale právě rozhraní. V Ruby nic takového jako rozhraní nenajdeme. Jak jiţ bylo řečeno, tak Ruby vyuţívá duck typing a tak jsou v něm objekty posuzovány pouze na základě svých vlastností. To je důvod proč rozhraní v Ruby ani není potřeba. Představme si situaci, kdy máme metodu, která akceptuje jeden parametr. Z důvodu absence statického typování nevíme při psaní programu, jaký má argument datový typ a s ohledem na duck typing na tom ani nezáleţí. Není totiţ důleţité, jestli argument odpovídá datovému typu, který jsme očekávali, ale zda má definovány metody, které voláme. Ke zjištění, zda objekt dokáţe reagovat na zaslání konkrétní zprávy, slouţí metoda respond_to?: class MyClass def greet puts 'Hello world' end end my_class = MyClass.new my_class.respond_to? :greet #=> true my_class.respond_to? :say_hello #=> false
Na následujícím příkladu si ukáţeme praktické vyuţítí duck typingu. Našim cílem bude implementovat triviální překladač z češtiny do libovolného jazyka. Do jakého jazyka bude překladač překládat, bude záleţet na poskytnutém překládacím stroji. class Translator def initialize(translator_engine) @translator_engine = translator_engine end def translate(word) @translator_engine.process(word) end end
75
class PirateEngine DICTIONARY = { 'ahoj' => 'ahoy' } def process(word) DICTIONARY[word] end end class EnglishEngine DICTIONARY = { 'ahoj' => 'hello' } def process(word) DICTIONARY[word] end end pirate_translator = Translator.new(PirateEngine.new) english_translator = Translator.new(EnglishEngine.new) pirate_translator.translate('ahoj') #=> ahoy english_translator.translate('ahoj') #=> hello
Instance třídy Translator má definovánu jedinou metodu translate. Tato metoda zavolá na poskytnutém překládacím stroji metodu process. U překládacího stroje nezáleţí na datovém typu, ale pouze na tom, zda dokáţe reagovat na metodu process.
4.2 Callbacks (zpětná volání) Ruby kromě klasického imperativního stylu umoţňuje programování s vyuţitím zpětných volání. Zpracování programu pak neprobíhá lineárně, ale dochází k nelineárním přeskokům, které na první pohled nemusí být plně postřehnutelné. Zpětná volání se obvykle spouštějí jako reakce na určité jasně definované události. K většímu přiblíţení, co můţe být těmito událostmi a jak můţe zpětné volání vypadat, si můţeme ukázat na frameworku Ruby on Rails. Zpětná volání se zde hojně vyuţívají při práci s databází, kdy můţeme definovat události a následné akce, které se mají provést. Konkrétně můţeme například definovat událost: before_save na kterou naváţeme vlastní metodu, která odpovídá akci, která se má provést.
Událost before_save se vyvolá před kaţdým uloţením daného modelu do databáze a my tak ještě před uloţením můţeme model upravit, aby například ukládané telefonní číslo bylo v mezinárodním formátu, pokud v něm jiţ není. Zpětná volání tak představují velice elegantní řešení, jak se vypořádat s problémy podobnéhu typu. Konkrétně zpětné volání v podobě before_save je specifické pro Ruby on Rails. Není to funkcionalita, která by byla zabudována přímo v samotném jazyku. Příklad slouţil hlavně 76
k přiblíţení toho, co se pod pojmem zpětná volání ukrývá. Nicméně i Ruby má definována svoje zpětná volání, která se hojně vyuţívají. Jedná se o metody, které reagují na události, které jsou spojeny s vytvářením nebo úpravou tříd a modulů. Zde je nekompletní přehled těch nejvyuţívanějších zpětných volání, která se v Ruby vyskytují [alHabache2008b]: method_missing const_missing included extended method_added singleton_method_added method_removed singleton_method_removed inherited
Názvy jednotlivých metod jsou se znalostí angličtiny samovysvětlující a nepotřebují dalších komentářů. Protoţe se tyto metody vyuţívají v kódu velice podobným způsobem, na příkladu si ukáţeme pouţití pouze jedné z nich. Bude to metoda method_missing a to hlavně z toho důvodu, ţe toto zpětné volání se stalo mezi Ruby programátory jedno z nejpopulárnějších a nejvíce vyuţívaným. 4.2.1 Method missing Zpětné volání method_missing se spouští vţdy, kdyţ voláme metodu, která neexistuje. Vţdy, kdyţ zasíláme objektu zprávu v podobě volání metody, interpret se podívá do definice třídy, zda třída tuto metodu obsahuje. Jestliţe třída neobsahuje implementaci dané metody, pak se spustí zpětné volání method_missing, coţ konkrétně znamená, ţe se interpret opět podívá do definice třídy, ale tentokrát hledá metodu s názvem method_missing. Pokud třída neobsahuje ani metodu method_missing program skončí chybou NoMethodError. Představme si třídu, která slouţí k míchání barev. Tato třída obsahuje jednu jedinou metodu mix, která z poskytnutých barev dokáţe namixovat výslednou barvu. Kód třídy ColorMixer, která by poskytovala danou funkcionalitu, by mohl vypadat například takto: class ColorMixer def mix(*colors) colors = colors.flatten if colors.empty? raise ArgumentError.new("You have to provide at least one color.") elsif colors.size == 1
77
return puts "The resulting color is #{colors.first}." end return puts "The resulting color will consist from: #{colors.join(' and ')}." end end
Z kapitoly 3.7.2 Libovolný počet argumentů víme, ţe hvězdička (*) před názvem parametru znamená, ţe se s parametrem bude pracovat jako s polem. To znamené, ţe i pokud metodě mix neposkytneme pole, ale pouze jednotlivé barvy oddělené čárkou, pole z nich bude
vytvořeno automaticky. Problém ale můţe nastat, pokud bychom jako parametr pouţili pole. Potom by parametr colors obsahoval dvě pole v sobě a právě k vyřešení tohoto problému slouţí metoda flatten. Flatten dokáţe z pole odstranit všechna zanořená pole: my_array = ["a", "b", ["c", "d"]] my_array.flatten #=> ["a", "b", "c", "d"]
Zbylé řádky metody mix zajišťují jiţ pouze kontrolu parametrů a výpis pomyslně namíchaných barev na obrazovku. Pouţití třídy ColorMixer je pak moţné následujícím způsobem: color_mixer = ColorMixer.new color_mixer.mix("blue", "red") #=>The resulting color will consist from: blue and red.
Pouţití metody mix tímto způsobem je klasické a standardní. Nebylo by to ale Ruby, kdyby nenabízelo ještě jinou cestu, kterou bychom se při míchání barev mohli vydat. Představte si situaci, kdy by bylo moţné, zadat barvy, které chceme smíchat rovnou do názvu metody. Metoda mix by pak mohla mít následující podobu: color_mixer.mix_blue_and_red
Pokud nyní zkusíme tuto metodu zavolat, program skončí chybou NoMethodError. Nyní jiţ přichází čas pro vyuţití zpětného volání method_missing. Jak jsme si jiţ říkali, pokud interpret nenalezne ve třídě danou metodu, v našem případě metodu mix_blue_and_red, pak se pokusí nalézt metodu method_missing. Kdyţ, ale interpret nenalezne ani metodu method_missing, program skončí chybou. Nyní je tedy potřeba implementovat metodu method_missing ve třídě ColorMixer: class ColorMixer def method_missing(id, *args, &block) method = id.to_s
78
return mix(method.sub('mix_', '').split('_and_')) if method =~ /^mix_/ raise NoMethodError end end
Metoda má tři parametry. Prvním parametrem, který je v tomto případě označen jako id, je název metody, který je předáván jako symbol. Následuje pole parametrů (args), které představuje parametry, se kterými byla volána metoda, jejíţ název se ve třídě nepodařilo nalézt. Posledním parametrem je block kódu (block). Naše nově vymyšlená metoda nemá ţádné parametry a proto při implementaci metody method_missing je pro nás významný pouze první parametr, který odpovídá názvu metody. Implementace metody method_missing má obecně následující logiku. Pomocí regulárního výrazu se porovná název metody s daným vzorem. Pokud se nalezne shoda s některým se vzorů (v našem případě je jen jeden), pak dochází k dalšímu zpracování. V opačném případě metoda vyhodí výjímku NoMethodError, stejně tak, jako kdyby ţádná naše implementace metody method_missing neexistovala. Konkrétně v našem případě se název metody porovnává s regulárním výrazem /^mix/, který říká, ţe by daný řetězec měl začínat řetězcem mix_. Pokud tomu tak je, pak se
z názvu metody odstraní řetězec mix_ a zbylá část se rozdělí na pole dle oddělovače, který tvoří řetězec _and_. Tímto způsobem jsou z názvu metody separovány jednotlivé barvy a následně je zavolána původní metoda mix. Nyní je jiţ tedy moţné pouţít následující zápis: color_mixer.mix_blue_and_red #=> The resulting color will consist from: blue and red.
Při pouţití zpětného volání v podobě method_missing, je potřeba nezapomenout na jednu zásadní věc. Je zapotřebí ovědomit si, ţe pouţitím této techniky do třídy v podstatě přidáváme nové metody. Instance by měla proto vracet hodnotu true jako odpověď na metodu respond_to?, ve které je jako paramater název metody, kterou řešíme vyuţitím method_missing. Jinými slovy, aby byl výše uvedený příklad kompletní je zapotřebí
předefinovat metodu respond_to?, tak aby výsledek této metody odpovídal realitě. V současné podobě totiţ tomu tak není, jak se můţeme přesvědčit: color_mixer = ColorMixer.new color_mixer.mix_blue_and_red #=> The resulting color will consist from: blue and red. color_mixer.respond_to?(:mix_blue_and_red) #=> false
79
Instance třídy ColorMixer dokáţe reagovat na metodu mix_blue_and_red, ale metoda respond_to? říká něco jiného. Tuto nekonzistenci můţeme vyřešit překrytím metody respond_to? ve třídě ColorMixer: class ColorMixer def respond_to? method return true if method.to_s =~ /^mix_/ return super(method) end end
Nyní je jiţ vše v naprostém pořádku, o čemţ se můţeme i sami přesvědčit: color_mixer = ColorMixer.new color_mixer.mix_blue_and_red #=> The resulting color will consist from: blue and red. color_mixer.respond_to?(:mix_blue_and_red) #=> true
4.3 Funkcionální programování Ruby patří mezi imperativní jazyky a můţe se tak zdát zvláštní, ţe budeme mluvit o funkcionálním programování právě ve spojitosti s Ruby. Funkcionální programovací jazyky patří do rodiny deklarativních jazyků. Cílem této kapitoly není přesvědčit čtenáře, ţe Ruby je funkcionální jazyk. Cílem této kapitoly je ukázat, ţe je moţné v Ruby vyuţít postupy a principy, které jsou pouţívány ve funkcionálním paradigmatu. Kdyţ jsme na začátku této práce popisovali základní vlastnosti Ruby, tvrdili jsme, ţe Ruby v sobě kombinuje přístupy imperativního i funkcionálního programovaní. Prozatím jsme se zaměřovali převáţně na imperativní stránku Ruby a nyní je na čase dokázat, ţe Ruby obsahuje i přístupy známé z funkcionálního programování. Hlavní rysy funkcionálního programování jsou zmíněny v kapitole 2.5.4 Funkcionální programování. V této kapitole se ale podíváme na funkcionální programování mnohem podrobněji a kromě představení základních principů si přímo ukáţeme, jak jsou tyto principy implementovány v Ruby. Jak jsme si jiţ řekli dříve, Ruby není funkcionální jazyk, pouze přebírá a vyuţívá některých myšlenek z funkcionálního způsobu myšlení a cílem této kapitoly je představit zde právě tyto myšlenky. Podle Tonyho Morrise [Morris2010] dochází při debatě o funkcionálních jazycích často k jedné zásadní chybě. Většinou se totiţ tato debata koncentruje jiţ na konkrétní znaky
80
funkcionálních jazyků, jako jsou například funkce vyšších řádů 24 nebo currying (oba termíny budou podrobně vysvětleny později v této kapitole) a zapomíná se na samotnou podstatu. Funkcionální jazyky jsou postaveny na dvou velice jednoduchých vlastnostech, přičemţ první vlastnost dokonce implikuje druhou. První vlastností je referenční transparentnost a druhou vlastností je princip kompozicionality. Tyto termíny budou dále podrobněji rozebrány. Je však ještě zapotřebí říci, ţe tyto termíny platí striktně pro čistě funkcionální jazyky, takţe i v jazycích, kterou jsou za funkcionální povaţovány, jsou tyto principy dodrţovány ve větší či menší míře. 4.3.1 Referenční transparentnost Funkcionální programování má svoje základy v matematice, konkrétně v teorii funkcí a v lamda-kalkulu. Referenční transparentnost je princip, který je vlastní matematickým funkcím. Princip referenční transparentnosti říká, ţe výsledek funkce je závislý pouze na vstupních proměnných a není závislý na ţádném externím stavu. Vezměme si jednoduchou funkci: f(x) = x + 1
Hodnota této funkce je závisla pouze na proměnné x. Jinými slovy můţeme říci, ţe tato funkce nemá ţádný externí stav, na kterém by byl závislý její výsledek. Toto neplatí pouze pro tuto triviální funkci, ale platí to pro veškeré matematické funkce. Matematické funkce jsou referenčně transparentní, nemají ţádnou závislost na svém okolí. Na referenční transparentnost je moţné nahlíţet ještě z jiného úhlu pohledu. Můţeme říci, ţe funkce je referenčně transparentní, jestliţe při neměnných vstupních hodnotách, je výsledek funkce pořád stejný. To má mimo jiné za následek, ţe namísto funkce můţeme vloţit do programu rovnou její výsledek a nijak se tím neovlivní správné fungování programu. Toto je umoţněno díky tomu, ţe výsledek funkce není závislý na ţádném externím stavu. Pokud jsme jiţ jednou výsledek funkce s danými parametry získaly, můţeme jím v programu nahradit samotnou funkci. Výše popsanou matematickou funkci je moţné samozřejmě přepsat v Ruby. Dokonce je i velice jednoduché dosáhnout referenční trasparentnosti, jakou má tato matematická funkce: def plus_one(x) x+1 end 24
High Order Functions 81
plus_one(5) #=> 6 plus_one(5) #=> 6
Výsledek metody je opravdu závislý pouze na vstupním parametru a volání metody se stejnými parametry vrací pořád stejnou hodnotu. Metoda je referenčně transparentní. Další vlastností referenční trasparentnosti a matematických funkcí je, ţe matematická funkce vţdy vrací výsledek. Ve funkcionálních programech funkce vţdy vrací výsledek, protoţe jediným moţným výsledkem funkce je právě její výsledek a nikoliv změna stavu, tak jak tomu můţe být v imperativních jazycích. Tím, ţe funkce nemění ţádný stav, říkáme, ţe nemá ţádné vedlejší efekty. Toto je další klíčová vlastnost funkcionálního přístupu, která vyplývá z referenční transparentnosti. Pro úplnost ještě uveďmě, ţe kromě referenční transparentnosti existuje tzv. referenční neprůhlednost, která je přesným opakem referenční transparentnosti. To znamená, ţe výsledek funkce (pohybujeme-li se v Ruby tak metody) není závislý pouze na proměnných, které do funkce vstupují, ale také na externím stavu. Výsledek funkce tak nemusí a velice pravděpodobně ani nebude pořád stejný. Podívejme se na následující příklad: @one = 1 def plus_one(x) x + @one end plus_one(5) #=> 6 @one = 2 plus_one(5) #=> 7
Metoda plus_one je závislá na externím stavu @one, o kterém předpokládá, ţe se nebude měnit. Tento stav se však kdykoliv v průběhu programu změnit můţe, také se tomu tak stane a tím pádem metoda i při stejných vstupních parametrech nevrací stejnou hodnotu. Metoda není referenčně transparentní. V souvislosti s objektově orientovaným programováním je důleţité si uvědomit jednu základní věc, která by mohla zaniknout. Objektově orientované programování pouţívá jako základní stavební kameny třídy, pomocí nichţ se tvoří objekty. Objekty ve většině případů neslouţí k ničemu jinému, neţ k uchování stavu, v němţ
se nachází výsek reality
reprezentovaný daným objektem. Jiţ samotnou existencí stavů je tedy samozřejmě nemoţné dosáhnout referenční transparentnosti. Důsledkem je, ţe všechny metody, které pouţívají instanční proměnné, nemohou být a nejsou referenčně transparentní. 82
Navzdory předchozímu odstavci, lze i programu, který se řídí objektově orientovaným paradigmatem, vyuţít principu referenční transparentnosti. Vyuţitím tohoto prinicpu dochází k zpřehlednění kódu, přičmţ jsou příkazy v metodě závislé pouze na vstupních parametrech a nemusíme pátrat po tom, jakých hodnot nabývají externí hodnoty. Zároveň lze takovýto kód rychleji procházet, krokovat a hledat případné chyby. Do popředí zájmu se zejména v poslední době dostává koncept škálování aplikací, kdy je výpočetní výkon rozloţen na více počítačů, které jsou spojeny prostřednictvím počítačové sítě, nejčastěji internetu. Při distribuovaném zpracování je klíčové, aby prováděné operace nebyly závislé na externím stavu. Tím, ţe je program zpracováván paralelně by tak mohlo docházet k tomu, ţe by se stav během provádění operace změnil, díky operaci, která je zpracovávána na jiném počítači. Docházelo by tak k nekonzistenci a najít chybu v takovém programu by bylo takřka nemoţné. Poslední a neméně důleţitou výhodou psaní kódu, který je co nejméně zavislý na externích stavech, je znovupouţitelnost takového kódu. Tím, ţe je metoda závislá pouze na svých parametrech, můţe být kdykoliv z kódu vyjmuta a pouţita v jiném programu. 4.3.2 Princip kompozicionality Princip kompozicionality (neboli princip skládatelnosti) bývá označován jako Fregeho princip [Morris2010]. Gottlob Frege byl německý matematik a filosof, který ţil na přelomu 19. a 20. století a bývá povaţován za zakladatele analytické filosofie. Jeho práce se mimo jiné zabývaly filosofií jazyka a matematikou. Fregeho princip říká, ţe význam komplexního výrazu je určen významem jeho jednotlivých částí a pravidel, které tyto části spojují [Linnebo2008]. Význam celku je určován na základě významu jednotlivých částí. Tuto teorii můţeme vztáhnout na počítačový program. Předpokládejme, ţe se program skládá z částí A, B, C a D a chceme vytvořit nový program, který se bude skládat z částí A, B, C a E. Program bude úplně stejný, aţ na to, ţe v novém programu bude část D nahrazena částí E. Na základě principu kompozicionality pak můţeme tvrdit, ţe úsilí, které bude potřeba k vyvinutí programu, se bude rovnat úsilí, které je potřeba k vyvinutí části E. Čím více se tato poslední věta blíţí pravdě, tím více program splňuje princip kompozicionality a tím více se přibliţuje k funkcionálnímu paradigmatu. Princip kompozicionality vychází z principu referenční transparentnosti. Čím více je dodrţován princip referenční transparentnosti, tím více jsou jednotlivé části kódu nezávislé. Části aplikace tak mohou být vyjmuty ze stávajícho programu a bez většího úsilý pouţity v jiném programu. Programy, které se skládají z jasně identifikovatelných a nezávislých součastí mají své nezastupitelné výhody. Na základě chování jednotlivých částí, můţeme
83
jasně říci, jak se program bude chovat jako celek. Tím, ţe jsou jednotlivé části nezávislé je moţné je i samostatně testovat a snáze nalézat chyby, neţ kdyby bylo zapotřebí zkoumat celou aplikaci. Nezanedbatelnou výhodou takovýchto aplikací je i jejich moţnost snadného škálování. 4.3.3 Jednoduché funkce v Ruby Jak jiţ bylo v této práci mnohokrát řečeno, Ruby není funkcionální jazyk a ani nemá ambici jím být. Ruby je imperativní jazyk, který současně implementuje některé prvky z funkcionálního
programování.
V následujících
částech
budou
představeny
prvky
funkcionálního programování v Ruby od jednoduchých aţ moţná triviálních případů aţ po komplexní uţití v podobě funkcí vyšších řádů. Funkce ve funkcionálních programovacích jazycích splňují dvě základní podmínky (nebo se k jejich splnění alespoň přibliţují). Jedná se o jiţ představené koncepty referenční transparentnosti a kompozicionality. Pro naše potřeby si tyto principy můţeme shrnout do následujících dvou předpokladů [alHabache2009]: 1.
Funkce by při stejném vstupu měla vracet stále stejný výstup
2.
Funkce má dělat pouze jednu jedinou věc (plnit jasně definovaný cíl) a nemá mít ţádné vedlejší efekty jako je např. změna proměnných nebo výpis na obrazovku.
Ruby je natolik flexibilní jazyk, ţe uţivatele nenutí psát kód, který by odpovídal psaní funkcí, na druhou stranu mu toto psaní do jisté míry umoţňuje. Pouţití tohoto stylu psaní má v Ruby své opodstatnění, které se týká zejména čitelnosti kódu. Pokud jsou metody psané tak, ţe jsou pouze jednoúčelové, slouţí názvy metod de facto jako dokumentace. V případě, ţe programátor cítí potřebu okomentovat metodu, kterou právě vytvořil, aby v budoucnosti rozuměl kódu, který napsal, s největší pravděpodobností metoda neplní pouze jeden účel a je příliš sloţitá. Jednoúčelové metody, které nejsou zavislé na vnějším kontextu lze také mnohem snáze pouţít při programování pomocí vláken. V neposlední řadě se tyto metody mnohem lépe testují a zapadají tak do konceptu TDD, který je mezi Ruby programátory natolik oblíben. First-class funkce Programování ve funkcionálních jazycích je postaveno na programování pomocí funkcí, coţ klade na funkce jisté poţadavky. Tyto poţadavky bývají shrnovány termínem funkce první třídy25, který mimo jiné vyjadřuje, ţe funkce jsou také objekty. Za tímto výrokem se dále
25
Volně přeloţeno z anglického functions as first-class citizens nebo také first class functions.
84
ukrývá moţnost předávat funkce jako argumenty do ostatních funkcí, vracet funkce jako návratové hodnoty z funkcí a ukládat funkce do datových struktur. Ruby toto všechno umoţnuje ať uţ ve větší či menší míře elegance. Téměř veškeré prvky funkcionálního programování, se kterými se můţeme v Ruby setkat, jsou umoţněny díky tzv. closures. Closures jsou v Ruby implementovány lambda a Proc funkcemi a bloky kódu. Lambda i Proc funkce jsou velice podobné blokům kódu, o kterých bylo hovořeno v kapitole 3.6.1 Bloky kódu, avšak existují mezi nimi jisté rozdíly. Vysvětlení těchto dvou konceptů je však za hranicemi rozsahu této práce a podrobný článek zabývající se touto tématikou napsal například Robert Sosinski [Sosinski2008], ve kterém vzájemně porovnává rozdíly mezi lambda a Proc funkcemi a bloky kódu. Ruby umoţňuje uloţit funkce do proměnných a ty pak předávat jako argumenty do metod. Uloţení funkce do proměnné si můţeme představit, jako kdybychom do proměnné dokázali uloţit metodu a pak ji ve vhodnou chvíli zavolat. V Ruby je moţné do proměnné uloţit lambda a Proc funkce a následně je zavolat metodou call, které jako argumenty předáme argumenty funkce, pokud nějaké má. V následujícím příkladu vytvoříme lamba funkci pro součet dvou čísel: sum_two_numbers = lambda { |a, b| a + b } sum_two_numbers.call(1, 2) #=> 3
V tomto případě jsme funkci rovnou zavolali pomocí metody call, ale stejně tak bychom ji mohli předat jako argument metodě. O tomto ale jiţ více v kapitole 4.3.4 Funkce vyšších řádů. Symbol vykřičníku a jeho užití Prozatím jsme o symbolu vykřičníku u názvu metod hovořili pouze z hlediska jeho expresivnosti, kdy nás tento symbol na něco upozorňuje. Ve většině případů nás upozorňuje, ţe jeho pouţitím dojde ke změně objektu, který je příjemcem zprávy. Před znalostí principů funkcionálního programování bychom se s tímto vysvětlením mohli spokojit, avšak teď jiţ víme, ţe uţití vykřičníku pochází právě z funkcionálního přístupu. Princip referenční transparentnosti říká, ţe funkce by neměla mít ţádné vedlejší efekty a ţádným způsobem by neměla měnit externí stav. Jelikoţ Ruby je imperativní jazyk, je velice jednoduché napsat metodu, která bude měnit stav objektu a tím pádem bude mít vedlejší efekt. Připomeňme si jen, ţe funkce by podle pricnipu referenční transparentnosti měla dělat pouze jednu věc (měla by mít jen jedno jasné poslání) a výsledek funkce by měl být v její návratové hodnotě.
85
Znak vykřičníku (!) v názvu metod neříká pouze: „Pouţívejte tuto metodu rozváţně“, ale také říká, ţe metoda má vedlejší efekt, mění stav objektu a tím pádem se nejedná o jednoduchou (čistou) funkci26. Jinými slovy, znak vykřičníku nás upozorňuje, ţe se metoda neřídí principem referenční transparentnosti. Následující příklady ukazují standardní metody třídy String, kdy můţeme vidět, jaký je rozdíl v pouţívání metod s vykřičníkem a bez něj: greeting = "Hello everybody!" puts greeting.upcase #=> "HELLO EVERYBODY!" puts greeting #=> "Hello everybody!" puts greeting.upcase! #=> "HELLO EVERYBODY!" puts greeting #=> "HELLO EVERYBODY!"
Metoda upcase je instanční metoda třídy String a převádí daný řetězec (příjemce zprávy) na velká písmena. Kdyţ po uţití metody upcase, vypíšeme na obrazovku samotný řetězec pomocí příkazu puts, vidíme, ţe se řetězec nezměnil. Metoda upcase vzala řetězec, převedla ho na velká písmena a takto převedený řetězec vrátila jako návratovou hodnotu, přičemţ původní řetězec zůstal beze změny. Toto však jiţ neplatí při pouţití metody upcase!, která kromě toho, ţe vrací převedený řetězec na velká písmena, tak změnila i původní řetězec. Jinými slovy, změnila stav objektu a právě proto se v jejím názvu vyskytuje znak vykřičníku. Uţití vykřičníku u názvů metod, které nějakým způsobem mění stav, je pravidlo, které se dodrţuje napříč všemi standardními knihovnami v Ruby. Je velice vhodné toto pravidlo dodrţovat i u vlastních metod. Jiţ tento jediný znak, který se objeví navíc v názvu metody, toho o metodě mnohé vypovídá a můţe tak zabránit špatnému uţití. Všechno v Ruby je výraz „Všechno v Ruby je výraz“ je hodně silné tvrzení. Všechno v Ruby není výraz, ale skoro všechno. Kaţdý kus kódu, u nějţ by mělo smysl, aby vracel nějakou hodnotu, tak skutečně činí. Tento fakt odlišuje Ruby například od Javy nebo od jazyka C, ve kterých třeba příkazy if nebo case jsou jen příkazy, kdeţto Ruby k nim přistupuje jako k výrazům. Podívejte se na
následující pouţití příkazů if a case v Ruby: value = if true "this will be the value" else "this will not be the value" end puts value #=> this will be the value
26
Z naglického pure function.
86
year = 1985 what_happend = case year when 1985 then "This is the year when I was born" else "Nohing interesting did not happen" end puts what_happend #=> This is the year when I was born
Skutečnost, ţe lze k příkazům if a case přistupovat jako k výrazům, umoţňuje vyuţít hodnotu, kterou vrací výraz jako celek. V případě rozhodovacích příkazů je výsledkem výrazu poslední řádek větve, do které se program svým průběhem dostane. Toto neobvyklé vyuţití příkazů if a case má své opodstatnění hlavně v případech, kdy se tímto nestandardním pouţitím zvyšuje čitelnost a přehlednost kódu. Fakt, ţe všechno v Ruby je výraz se hojně vyuţívá ještě k jiné věci a to k řetězení příkazů. Protoţe kaţdý výraz vrací hodnotu, je moţné tuto hodnotu ihned vyuţít v následujícím příkazu nebo metodě. Je tak například moţné přiřadit jednu hodnotu více proměnným najednou nebo vyuţít zřetězeného volání metod: puts ["c", "a", "d", "b"].sort.reverse #=> ["d", "c", "b", "a"] a = b = c = "this is common value" puts a #=> this is common value puts b #=> this is common value puts c #=> this is common value a = (b = 1 + 2) + 3 puts a #=> 6 puts b #=> 3
Návratové hodnoty u metod Funkce ve funkcionálním programování nemění stav a jsou tzv. bez vedlejších efektů. Jediným moţným způsobem, jak dostat výsledek funkce, je prostřednictvím návratové hodnoty. Kaţdá funkce má tedy svoji návratovou hodnotu, a protoţe kaţdá funkce vrací nějakou hodnotu, je zbytečné, aby se vţdy nutně pouţívalo klíčové slovo, které by říkalo toto je návratová hodnota. Moţnost neuvádět klíčové slovo return u metod v Ruby je tedy další vlastností, kterou si Ruby vypůjčilo od funkcionálního přístupu k programování. Fakt, ţe není nutné pouţívat vţdy klíčové slovo return, byl zmíněn jiţ v kapitole 3.7 Metody, nyní se však dostáváme k tomu, jaký se za tím skrývá smysl. Pokud by metody v Ruby měli sledovat princip psaní
87
tzv. čistých funkcí, měla by být vyuţívána právě návratová hodnota metody a metoda by neměla mít ţádné vedlejší efekty (měnit stav objektu). Kaţdá metody v Ruby tedy vrací hodnotu a to dokonce i tehdy, kdy mění stav objektu a ţádnou hodnotu by vracet nemusela. Je pouze na programátorovi, zda tuto hodnotu vyuţije nebo ne. Jednoduchou ukázku toho, ţe metoda v Ruby vrací hodnotu i bez klíčového slova return poskytuje následující příklad: def say_hello(name) "Hello #{name}. How are you?" end say_hello("David") #=> Hello David. How are you?
4.3.4 Funkce vyšších řádů Funkce vyšších řádů jsou takové funkce, které přebírají funkce jako parametr a/nebo vracejí funkci jako výsledek. Toto je velice jednoduchá a srozumitelná defince, která vychází z [Malý2011]. V Ruby je tato funkcionalita umoţněna díky closures, ať uţ jsou pouţity v jakékoliv ze tří svých podob (lamba funkce, Proc funkce, bloky kódu). Rovnou si ukáţeme příklad, na němţ bude vidět, jak je moţné vytvořit metodu, která bude mít jako jeden ze svých parametrů blok kódu. Zároveň bude ukázka slouţit jako představení toho, jak je moţné pomocí konceptu funkcí vyšších řádů definovat vlastní syntaktická pravidla. Konkrétně si definujeme vlastní příkaz cyklu: def loop(items, &code) for item in items do code.call(item) end end loop(1..3) do |number| puts number * 10 end #=> 10 #=> 20 #=> 30
Metoda loop je náš vlastní příkaz cyklu, který jsme si definovali. Jak můţeme vidět, tak metoda přijímá dva parametry, kde prvním parametrem je kolekce kolem které chceme iterovat a druhým parametrem je kód, který se má spustit nad kaţdou poloţkou kolekce. Při pouţití metody loop uvedeme první parametr klasicky do kulatých závorek (v našem případě
88
je tímto parametrem interval od jedné do tří) a druhým parametrem je blok kódu, který je uvozen klíčovými slovy do a end. Poslední neznámou částí kódu, o které je třeba se zmínit, je ampersand u druhého parametru v definici metody loop, &code. Znak empersandu (&) zajišťuje převod parametru na datový typ Proc. Při volání metody, tak jak jsme ji uvedli v našem případě, dochází k převodu bloku kódu na datový typ Proc. V metodě loop bychom mohli rovnou pouţít blok kódu i bez jeho převodu na datový typ Proc. Pouţití bloku kódu uvnitř metody má ovšem tu nevýhodu, ţe z názvu metody není poznat, ţe metoda pro své správné fungování potřebuje jako parametr blok kódu. Tato skutečnost je poznat pouze z těla metody, ve které se pro vloţení bloku kódu pouţívá klíčové slovo yield. Pro názornost zde uvádíme, jak by vypadala metoda loop, kdyby pracovala přímo s blokem kódu: def loop(items) for item in items do yield(item) end end
Znak ampersandu by také nebylo potřeba u parametru code uvádět v případě, ţe bychom při pouţití metody loop, jako druhý parametr uvedli lambda funkci, jejíţ interní reprezentací je automaticky datový typ Proc. Metoda loop a její pouţití by poté vypadalo následujícím způsobem: def loop(items, code) for item in items do code.call(item) end end loop(1..3, lambda { |number| puts number * 10 })
Volání metody loop s lambda funkcí jako druhým argumentem není příliš čitelné a proto se preferuje namísto lambda funkcí vyuţití bloku kódu. Nicméně i tento příklad je plně funkční. Nejčastěji se tak při vyuţití funkcí vyšších řádů můţeme setkat s paramtrem, který je uvozen ampersandem. Tento zápis má hned dvě hlavní výhody. První výhodou je, ţe hned z návěstí metody je patrné, ţe metoda přijímá jako parametr blok kódu (lambda funkci). Druhou výhodou je pak poskytnutí jisté variability, kdy můţeme danou metodu pouţít jak s blokem kódu, tak s lambda funkcí. Metody na steroidech 89
Kaţdý programátor by se měl snaţit, aby kód, který napíše, byl čitelný, dobře udrţovatelný a v neposlední řadě snadno rozšiřitelný. Nyní bude následovat příklad, jehoţ snahou je ukázat hlavní výhody vyuţití prvků fukncionálního programování v Ruby a jak tyto principy přispívají k zajištění výše uvedených vlastností dobrého kódu. Tento příklad je volně inspirován článkem Can Your Programming Language Do This, jehoţ autorem je Joel Spolski [Spolski2006]. Příklad se skládá ze dvou části, přičemţ v první části je vyřešen problém jedním způsobem a v druhé části je ukázano, jak by se problém dal řešit elegantnější cestou s totoţným výsledkem. Naším úkolem je napsat kód, který dokáţe uvařit dvě jídla. Prvním jídlem jsou špagety a druhým jídlem je guacamole. Pro zjednodušení předpokládejme, ţe při vaření špaget stačí všechny ingredience dát do hrnce a uvařit. Při přípravě guacamole pro zjednodušení předpokládejme, ţe všechny ingredience dáme do mixéru, kde je rozmixujeme. Kód, který by virtuálně připravil tato dvě jídla, by pak mohl vypadat takto: def cook_spaghetti puts "Get spaghetti and tomatos." get_in_pot("spaghetti") get_in_pot("tomatos") puts "Spaghetti is finished." end def cook_guacamole puts "Get avocado, onion and tomatos." put_in_blender("avocado") put_in_blender("onion") put_in_blender("tomatos") puts "Guacamole is finished." end def get_in_pot(ingredient) puts "#{ingredient.capitalize} is (are) in the pot." end def put_in_blender(ingredient) puts "#{ingredient.capitalize} is (are) in the blender." end
cook_spaghetti #=> Get spaghetti and tomatos. #=> Spaghetti is (are) in the pot. #=> Tomatos is (are) in the pot. #=> Spaghetti is (are) finished.
90
cook_guacamole #=> Get avocado, onion and tomatos. #=> Avocado is (are) in the blender. #=> Onion is in (are) the blender. #=> Tomatos is (are) in the blender. #=> Guacamole is finished.
Pro zjednodušení jsou veškeré činnosti nahrazeny textovým výpisem do konzole. Jako ilustrace je to, ale naprosto dostačující a po spuštění metod v konzoli, tak alespoň ihned můţeme vidět výsledek. Kód, který jsme napsali, dělá přesně to, co jsme po něm poţadovali. Pokud se však na něj podíváme pozorněji, můţeme si všimnout, ţe se v něm vyskytují části kódu, které se opakují. Jak metoda cook_spaghetti, tak metoda cook_guacamole, mají tři velice podobné části. V první části se připravují ingredience, které jsou k přípravě pokrmu potřeba. V druhé části se všechny ingredience postupně zpracovávají danou metodou přípravy. Ve třetí části nás jen program informuje o tom, ţe je pokrm hotový. Na první pohled to není příliš patrné, ale tak, jak je aplikace napsána teď, neodpovídá principu DRY. Jinými slovy v aplikaci existují části kódu, které se opakují. Nabízí se tedy moţnost zobecnit části kódu, které se opakují a vytvořit jednu obecnou metodu cook. Největší problém představuje, jakým způsobem do této nové metody předáme postup, kterým se ingredience mají zpracovat. Se znalostí funkcí vyšších řádů však víme, ţe tento postup můţeme do metody předat pomocí bloku kódu. Obecná metoda cook a její následné pouţití vypadá takto: def cook(meal, *ingredients, &cooking_method) puts "Get #{ingredients.join(' and ')}." ingredients.each { |ingredient| cooking_method.call(ingredient) } puts "#{meal.capitalize} is (are) finished." end cook("spaghetti", "spaghetti", "tomatos") do |ingredient| puts "#{ingredient.capitalize} is (are) in the pot." end #=>Get spaghetti and tomatos. #=>Spaghetti is (are) in the pot. #=>Tomatos is (are) in the pot. #=>Spaghetti is (are) finished. cook("guacamole", "avocado", "tomatos", "onion") do |ingredient| puts "#{ingredient.capitalize} is (are) in the blender." end #=> Get avocado and tomatos and onion.
91
#=> Avocado is (are) in the blender. #=> Tomatos is (are) in the blender. #=> Onion is (are) in the blender. #=> Guacamole is (are) finished.
Výrazným způsobem se zkrátil kód celé aplikace. Veškré fungování nyní stojí na metodě cook, která se skládá pouze ze tří řádků kódu. Metoda cook přijímá tři parametry, kterými
jsou zaprvé název pokrmu, dále jednotlivé ingredience, ze kterých se automaticky vytvoří pole díky znaku hvězdičky (*) a posledním parametrem je blok kódu, který představuje způsob
zpracování
jednotlivých
ingrediencí.
Generalizací
metody
cook
z metod
cook_spaghetti a cook_guacamole, jsme dosáhli ještě jedné nezanedbatelné věci.
Kdybychom ještě před provedeným refactoringem chtěli přidat přípravu nového pokrmu, museli bychom v kódu definovat novou metodu. Tato nová metoda by slouţila k přípravě právě jednoho pokrmu. Nyní pro přípravu nového pokrmu nemusíme vytvářet novou metodu, ale pouze zavoláme metodu cook, které předáme odpovídající parametry. Výše popsanou generalizací se nám tedy podařilo upravit kód tak, aby byl do budoucna mnohem snáze rozšiřitelný. Na výše uvedeném příkladu je vidět, jakou moc a variabilitu získávají metody, kterým můţeme předávat bloky kódu. Bloky kódu si můţeme představit jako rozhraní metody, se kterým můţeme libovolně pracovat. Pokud vytvoříme metodu, která jako parametr akceptuje blok kódu, je to jako bychom k metodě vytvořili API27 a tím mnohonásobně zvýšili její flexibilitu. Otvíráme tím tak cestu i ostatním programátorům, kteří naši metodou mohou vyuţít způsobem, který by nás třeba ani nenapadl. Currying Dle [Haksell2008] currying je proces transformace funkce, která přijímá více parametrů, na funkci, která přijímá pouze jeden parametr a pokud funkci ještě nějaké parametry chybí, je výsledkem této transformace opět funkce. Currying je vlastnost, která je v Ruby implementována aţ od verze 1.9. Podívejme se nejdříve na to, co tento pojem znamená, protoţe z výše uvedené definice to nemusí být na první pohled patrné. Výše uvedená definice by se dala parafrázovat tak, ţe currying je schopnost převést funkci o n parametrech na n funkcí, kde kaţdá akceptuje pouze jeden parametr. Představme si matematickou funkci, která akceptuje tři parametry x, y a z [alHabache2009b]: f(x,y,z) = 4x + 3y + 2z
27
Zkratka z anglického Application Programming Interface.
92
Tuto funkci můţeme rozepsat jako kompozici tří funkcí (pro kaţdý parametr jednu): f(x),(y),(z) = 2z + (3y + (4x))
Currying je proces v němţ můţeme sloţenou funkci, která akceptuje více parametrů, převést na kompozici funkcí, u níţ kaţdá akceptuje jen jeden parametr. Výsledkem této transformace potom jsou tzv. parciální funkce, které vznikají z původních funkcí tak, ţe mají jiţ některé parametry předvyplněny. Pokud zůstaneme u výše zmíněného matematického příkladu, pak parciální funkcí by byla funkce, kde by byla za parametr x dosazena jiţ konkrétní hodnota. Nyní si vše ukáţeme na konkrétním příkladu. Chceme vytvořit dvě funkce, kterým jako argument předáme interval a první funkce vypočte součet všech čísel v daném intervalu a druhá funkce vypočte součet násobků všech čísel v daném intervalu: sum = lambda do |range| range.reduce { |sum, n| sum + n } end multiply = lambda do |range| range.reduce { |sum, n| sum * n } end sum.call(1..5) #=> 15 # 1 + 2 + 3 + 4 + 5 = 15 multiply.call(1..5) #=> 120 # 1 * 2 * 3 * 4 * 5 = 120
Kdyţ se pozorněji podíváme na funkce, které jsme definovali, zjistíme, ţe jsou téměř totoţné. Jediné v čem se odlišují, je druh výpočtu, který se v nich provádí. Konkrétně se jedná o části sum + n a sum * n. Jinak je zápis naprosto totoţný. Obě funkce tedy můţeme zobecnit do jedné funkce, které navíc kromě rozsahu předáme jako argument i výpočet, který se má provést: calculation = lambda do |f, range| range.reduce { |sum, n| f.call(sum, n) } end
Takto zobecněnou funkci poté pouţijeme následujícím způsobem, abychom dostali stejné výsledky, jako v předchozím příkladu: calculation.call(lambda {|sum, n| sum + n }, 1..5) #=> 15 calculation.call(lambda {|sum, n| sum * n }, 1..5) #=> 120
Volání funkce calculation není příliš elegantní a přes mnoţství argumentů můţeme i přehlédnout k čemu uvedená funkce vlastně slouţí. Nyní přichází ke slovo currying, s jehoţ 93
pomocí můţeme tuto obecnou funkci transformovat na funkci konkrétní. Currying nám umoţní vytvořit parciální funkci, ve které bude předvyplněn parametr, který se týká výpočtu. Ruby pro tuto transformaci definuje metodu Proc#curry. Můţeme tak vytvořit funkce sum a multiply, které ale tentokrát nebudou implementovány samostatně a nebude tak docházet
k duplicitám v kódu. Obě funkce budou nyní vycházet z funkce calculation. Vytvoření obou funkcí a jejich pouţití je vidět na následujícíh řádcích: sum = calculation.curry.call(lambda {|sum, n| sum + n }) multiplication = calculation.curry.call(lambda {|sum, n| sum * n }) sum.call(1..5) #=> 15 multiply.call(1..5) #=> 120
4.4 Domain Specific Language Tento termín lze doslovně přeloţit jako doménově specifický jazyk, avšak běţně se tento termín nepřekládá a vyuţívá se jeho zkratka DSL. V následujícím textu se budeme na tento termín odkazovat také převáţně touto zkratkou. Martin Fowler DSL definuje jako „počítačový jazyk, který se zaměřuje na řešení úzce vymezených problémů, v kontrastu s obecnými programovacími jazyky, které dokáží řešit veškeré problémy s vývojem softwaru“ [Fowler2010]. Mezi nejznámnější představitele DSL patří kaskádové styly (CSS), regulární výrazy, SQL a mezi ty méně známé ale určitě ne méně významné například rake, make, Haml a svým způsobem i třeba webový framework Ruby on Rails. Z příkladů konkrétních DSL si jiţ můţeme udělat lepší představu o tom, co se za tímto termínem ukrývá. Jedná se o jazyky, které byly vytvořeny, aby co moţná nejvíce přímočaře dokázaly poslouţit k úkolu, pro který byly vytvořeny. Preferuje se snadnost pouţití a elegantní syntaxe. V mnohých případech (můţeme říci, ţe ve většině) se ani nejedná o kompletní programovací jazyk dle Alana Turinga. Není to ani cílem těchto jazyků. Jejich cílem je, aby se co nejvíce hodily k řešení problémů, které souvisí s velice úzkou oblastí. V případě CSS je to adresování prvků a definice pravidel pro vzhled, v případě SQL je to kladení dotazů do databáze. Díky tomu, ţe jsou DSL zaměřeny pouze na úzkou oblast (nepopisuje celý systém), je moţné zaměřit se pouze na konkrétní problém a veškerá nepotřebná data nechat stranou. Tato vlastnost umoţnuje, aby byly DSL stručné a syntakticky přehledné. DSL mohou být definovány přímo v jazyku, ve kterém jsou pouţívány. DSL se pak stávají nadstavbou daného programovacího jazyka. Můţeme říci, ţe takto definované DSL
94
tvoří jazyky v jazyku. Do programovacího jazyka jsou přidávána nová syntaktická pravidla, která tvoří DSL. Takto definovaným DSL se říká interní. Naproti tomu existují externí DSL, které jsou nezávislé na programovacím jazyku. Externí DSL mohou existovat samostatně napříč různými systémy a různými programovacími jazyky. Typické vlastnosti externích a interních DSL jsou shrnuty v následujících bodech: Externí DSL Představuje svůj vlastní svět se svými syntaktickými i sémantickými pravidly. Nezávislý na programovacím jazyku a díky tomu je moţné definovat syntaxi, která se přesně hodí k řešení problémů v dané oblasti. Protoţe DSL nevychází z ţádného programovacího jazyku, ale je postaven na svých vlastních základech je nutné pro jeho pouţití napsat vlastní parser (pokud jiţ není v daném jazyku napsaný). Problematická podpora v integrovaných vývojových prostředích (IDE). Interní DSL Omezení pouze na syntaxi jazyka, ve kterém je definován (omezení daná moţnostmi jazyka). DSL je definováno v daném programovacím jazyce, je v něm i zpracováno a proto není nutné psát nový parser. Omezené vyuţití v rozšířených programovacích jazycích jako je Java nebo C#, díky jejich syntaktickým pravidlům28. Představuje syntaktické rozšíření jazyka, které v extrémním případě umoţňuje vývoj aplikace pouze prostřednitvím DSL bloků. DSL v tomto případě slouţí jako vrstva s ještě vyšší mírou abstrakce, neţ jakou poskytuje programovací jazyk. 4.4.1 Language Oriented Programming Pojem, který se velice úzce pojí s interními DSL je Language Oriented Programming. Termín můţeme volně interpretovat jako programování za pomoci jazyka. Tento přístup vyuţívá a navazuje na principy, na kterých stojí DSL. Celá myšlenka je velice prostá a vychází ze základního předpokladu, ţe při vývoji systému se v první fázi vyvine DSL, který systém popisuje, a v druhé fázi se systém staví pomocí jiţ vytvořeného DSL. Výhodou takto navrţeného systému je snadné prototypování nových částí, rychlejší vývoj a flexibilnější reakce na změny. Toto vše je dosaţeno především díky velice úspornému kódu, kterého je 28
Java ani C# nepatří k jazykům, ve kterých by byla tvorba interních DSL snadnou záleţitostí. Oba dva jazyky ale běţí nad platformou, na níţ je definován dostatečně mocný jazyk pro tvorbu DSL – v Javě Groovy, v.NET Boo. 95
dosaţeno vyuţitím DSL. Nezanedbatelnou výhodou je i čitelnost takovýchto systémů, kdy je za předpokladu dobře navrţeného DSL, kódu schopen porozumět i neprogramátor. Toto není důleţité z hlediska toho, ţe bychom chtěli psát kód, kterému by rozumněli i lidé, kteří neprogramují, ale z důvodů, které dobře vystihuje Martin Fowler [Fowler1999]: „Kód, kterému rozumí počítač, umí napsat každý trouba. Dobří programátoři píší kód, kterému rozumí lidé“. Tato myšlenka byla v této práci uţ jednou zmíněna, avšak k tomuto tématu se hodí tak dobře, ţe by byla škoda ji opomenout. Pokud vás více zajímá téma Language Oriented Programming lze nalézt více informací v [Ward1994], kde byl tento termín zmíněn vůbec poprvé. Webový framework Ruby on Rails, který je postaven na programovacím jazyku Ruby představuje typický systém, ve kterém je vyuţit princip Language Oriented Programming a tím pádem i DSL. Ne nadarmo se o Rails říká, ţe se nejedná o nic jiného neţ o DSL, které je postaveno na Ruby. Nám teď právě tento framework poslouţí jako figurant, na kterém si konečně ukáţeme co to vlastně DSL je. Podívejte se na následující příklad: class Person < ActiveRecord::Base validates_presence_of :name end
Person je klasická Ruby třída, která je potomkem ActiveRecord::Base29. Na prvním řádku
tohoto příkladu není nic zajímavého, zajímavý je ale uţ hned druhý řádek. Metoda validates_presence_of, která slouţí ke kontrole, zda je objekt platný (validní). Třída Person tuto metodu podědila od svého předka ActiveRecord::Base. Metoda nám říká, ţe
aby
byl
objekt
platný,
musí
mít
vyplněné
jméno
(atribut
name).
Metoda
validates_presence_of je typickou ukázkou vyuţití DSL v praxi. Nejedná se totiţ o
metodu, která by byla definována standardně v jazyku, ale je definována aţ pomocí daného jazyka. Jiţ na tomto jednoduchém příkladě si lze povšimnout, ţe to jak lze interní DSL implementovat je velice závislé na pouţitém programovacím jazyku. Na první pohled totiţ není patrné, ţe metoda validates_presence_of je skutečně metoda a uţ vůbec ne, ţe se jedná o metodu třídy. Výše uvedenou definici třídy bychom mohli přepsat takto: class Person < ActiveRecord::Base self.validates_presence_of(:name) end
29
ActiveRecord je modul, který v Ruby on Rails zajišťuje objektově relační mapování, to znamená mapování objektů na příslušné tabulky v databázi. 96
Tento zápis je uţ samozřejmě mnohem méně elegantní a také mnohem méně čitelný. Dokazuje to ovšem jednu zásadní věc a tou je fakt, ţe interní DSL se nejlépe tvoří v jazycích, které mají flexibilní syntaxi. Důleţité vlastnosti programovacích jazyků, které jsou vhodné pro tvorbu interních DSL lze shrnout do následujích třech bodů (poslední dva body budou představeny a obhájeny v následujících podkapitolách): Flexibilní syntaxe, která dovoluje opomenout závorky kolem argumentů metod a vůbec vyuţívá závorek co nejméně. Implementace closures. Implementace technik podporujících metaprogramování. 4.4.2 Interní DSL v Ruby DSL vyšlo v předchozích kapitoléch poměrně do detailu probráno, avšak doposud jsme se pohybovali pouze v oblasti teorie. Toto by se nyní mělo změnit. Implementaci interního DSL v Ruby si ukáţeme rovnou na příkladu. Ne nadarmo se říká, ţe příklad mnohdy vystačí za tisíc slov a v tomto případě s tímto tvrzením nelze neţ souhlasit. Představme si, ţe programujeme aplikaci, ve které se ve velkém mnoţství pracuje s budovami, neboli s objekty, které reprezentují budovy. Budovy se skládají z pater, patra obsahují místnosti a u místností evidujeme jejich rozlohu. Z tohoto krátkého popisu je patrné, ţe se v aplikaci budou vyskytovat tři základní objekty, které reprezentují budovu, patro a místnost. V aplikaci se tedy budou vyskytovat třídy Building, Floor a Room. Naším cílem je navrhnout interní DSL, které by usnadňovalo tvorbu budov, tak abychom vţdy nemuseli vytvářet instance jednotlivých objektů a ty poté kombinovat dohromady do výsledné budovy. Po zralé úvaze jsme navrhli DSL, které vyhovuje našim poţadavkům a v něm zapsaný program má následující podobu: family_house = BuildingCreator.build 'Family house' do floor do room 'kitchen', :area => 30 room 'dining room', :area => 20 room 'living room', :area => 40 end floor do room 'bed room', :area => 40 room 'bed room', :area => 30 room 'bathroom', :area => 20 end end
97
Ke stavbě našeho pomyslného domu vyuţíváme ve velké míře bloky kódu, které tvoří přehlednou a kompaktní syntaxi. I bez sebemenšího vysvětlení je patrné, co daným kódem chceme říci. Máme v úmyslu vytvořit dům, který se bude jmenovat Family house a který bude mít dvě patra. V prvním patře budou tři místnosti a ve druhém patře budou jen dvě místnosti. U kaţdé z místností definujeme její název a také rozlohu. Jaká je výhoda v pouţití DSL? Vytvořit budovu, která by splňovala předchozí poţadavky lze samozřejmě i s pouţitím klasického konstruktoru a pomocí běţných metod. Pro srovnání si ukáţeme, jak by takový zápis mohl vypadat: family_house = Building.new('Family house') first_floor = Floor.new(1) first_floor.rooms << Room.new('kitchen', :area => 30) first_floor.rooms << Room.new('dining room', :area => 20) first_floor.rooms << Room.new('living room', :area => 40) family_house.floors << first_floor second_floor = Floor.new(2) second_floor.rooms << Room.new('bed room', :area => 40) second_floor.rooms << Room.new('bed room', :area => 30) second_floor.rooms << Room.new('bathroom', :area => 20) family_house.floors << second_floor
Srovnáme-li dva předchozí příklady, které v zásadě dělají stejnou věc, je na první pohled jasné, proč se uţití DSL stalo natolik populární. Jen na takto jednoduchém příkladu lze vidět, o kolik je zápis jednodušší a kratší. Kdyţ posléze vezmeme v úvahu, ţe v kratším kódu je s vysokou pravděpodobností i méně chyb, můţeme tvrdit, ţe zápis pomocí DSL je i méně chybový. Tento druhý příklad nám ukazuje ještě jinou věc. Velice podobně by vypadal kód třeba v Javě (pokud si přimyslíme ještě středníky na konci řádků). Tento zápis totiţ nevyuţívá vlastností Ruby, pro které je tak oblíbené a dobře čitelné, jako je moţnost psát metody bez závorek nebo vyuţití bloků kódu.
Implementace Ještě neţ se pustíme do implementace našeho DSL, musíme si definovat jednotlivé třídy. Třídy Building, Floor a Room, by měli být na DSL absolutně nezávislé, tak aby je bylo moţné pouţít i běţným způsobem. To znamená vytvořit budovu a její části klasicky pomocí kontruktorů. To, jestli byla budova správně vytvořena a odpovídá našim poţadavkům, 98
budeme kontrolovat pomocí textové reprezentace třídy Building. Po zavolání metody puts na instanci třídy Building, se automaticky vypisuje její textová reprezentace, která je daná metodou to_s. My budeme chtít předefinovat metodu to_s tak, abychom dostali textový výstup odpovídající textovému popisu domu, pater a místností v něm obsaţených. Textový výpis ukázkové budovy by vypadal takto: puts family_house #=> Family house has 2 floor(s). #=> Floor 1 has 3 room(s): kitchen (30), dining room (20), living room (40). #=> Floor 2 has 3 room(s): bed room (40), bed room (30), bathroom (20).
Samotný text je samovysvětlující a za vysvětlení stojí pouze čísla, která jsou uvedená v závorkách za jednotlivými názvy místností. Čísla v závorkách udávají rozlohu dané místnosti. Implementace tříd, jejichţ instance budou tvořit budovu, nebude příliš komplexní, ba právě naopak - bude spíše triviální. Kaţdá třída bude mít pouze dvě metody, initialize (kontruktor) a metodu to_s (textová reprezentace třídy). Kaţdá třída bude mít také definovány přístupové metody ke svým atributům tak, aby k nim mohlo být jednak přistupováno zvnějšku třídy a také, aby bylo moţné změnit jejich hodnotu. Implementace tříd Building, Floor a Room vypadá takto: class Building attr_accessor :name, :floors def initialize(name) @name = name @floors = [] end def to_s result = "#{@name} has #{@floors.size} floor(s).\n" @floors.each do |floor| result << floor.to_s end return result end end class Floor attr_accessor :number, :rooms def initialize(number) @number = number
99
@rooms = [] end def to_s result = "Floor #{@number} has #{@rooms.size} room(s): " result << @rooms.map { |room| room.to_s }.join(', ') + ".\n" end end class Room attr_accessor :type, :area def initialize(type, options = {}) @type = type @area = options[:area] end def to_s "#{type} (#{area})" end end
Daleko zajímavější neţ implementace tříd Building, Floor nebo Room, je implementace modulu BuildingCreator. Tento modul je zodpovědný za interpretaci námi vymyšleného DSL. Pokud se podíváme zpět na DSL, zjistíme, ţe se v něm objevují pouze tři metody. Jedná se o metody build, floor a room. U metody build by jistě kaţdý poznal, ţe se jedná o metodu, ale u metod floor a room, to jiţ stoprocentně tvrdit nelze. Jak uţ jsme jednou uváděli, je tomu tak proto, ţe pokud jsou metody uvedeny bez závorek, budí dojem spíše klíčových slov neţ metod. Tato vizuální vlastnost, nebo můţeme říci klamání zevnějškem, je přesně ta vlastnost, kterou u metod chceme vyuţít. Chceme, aby naše metody vypadaly jako klíčová slova. Jak jiţ bylo řečeno BuildingCreator je modul. Nejedná se tedy o třídu, jak bychom moţná očekávali. Vyuţití modulu se v tomto případě přímo nabízí, protoţe nepotřebujeme, aby mohl BuildingCreator vytvářet své vlastní instance, ale potřebujeme pouze, aby dokázal inicializovat a pracovat s ostatními třídami. V podstatě se jedná pouze o obálku, která sdruţuje metody (kód) do jednoho celku. Nejdříve se podíváme na implementaci modulu a poté si vysvětlíme jeho jednotlivé části: module BuildingCreator def self.build(name, &block)
100
building = Building.new(name) building.instance_eval(&block) return building end Building.class_eval do define_method :floor do |&block| floor = Floor.new(@floors.size + 1) floor.instance_eval(&block) @floors << floor end end Floor.class_eval do define_method :room do |type, options| @rooms << Room.new(type, options) end end end
BuildingCreator obsahuje pouze jednu jedinou metodu a to je metoda build. Ta tedy
zároveň tvoří rozhraní, pomocí něhoţ lze s modulem pracovat. Protoţe modul nemůţe vytvářet své instance, je metoda build definována jako metoda třídy (nebo v tomto případě modulu). Metoda build akceptuje dva parametry, u nichţ prvním je název budovy, kterou chceme vytvořit (name), a druhým parametrem je blok kódu (&block). S bloky kódu jako parametry metod jsme se uţ setkali a tak to pro nás není ničím novým. Novinkou ovšem je, jak je tento blok kódu dále zpracován. Blok kódu je zpracován metodou instance_eval, která nás přivádí do světa metaprogramování. Kromě metody instance_eval, se v modulu objevily ještě nám neznámé metody class_eval a define_method, které také souvisí s metaprogramováním. Metaprogramování představuje v Ruby poměrně rozsáhlou oblast, kterou určitě není moţné pokrýt v rozsahu této práce. Avšak protoţe je tento přístup pro Ruby natolik typický, je třeba ho alespoň částečně zmínit. V následující kapitole tedy budou mimo jiné vysvětleny metaprogramovací postupy, které se objevily v modulu BuildingCreator a také představeny některé další. 4.4.3 Metaprogramování Dříve neţ budeme pokračovat ve vysvětlování našeho příkadu, je důleţité objasnit, co to vlastně metaprogramování znamená. Pokud jste o něm nikdy dřív neslyšeli, pak se nenechte zmýlit, ţe se jedná o termín, který se pojí jen s Ruby. Metaprogramování je technika, která je 101
známá uţ velice dlouhou dobu a v různých jazycích je více či méně pouţitelná. To, ţe se o metaprogramování mluví často ve spojitosti s Ruby, není náhoda. Ruby se pro metaprogramování hodí více neţ dobře, ale to neznamená, ţe je to jediný jazyk, ve kterém lze tuto techniku uplatnit. A co to tedy metaprogramování je, nebo co tento termín znamená? Velice krátkou a srozumitelnou definici můţeme nalézt v [Why2005], kde se autor o metaprogramování vyjadřuje jako o „psaní kódu, který píše kód“. Tato definice je velice široká a spadají do ní například i generátory kódu. Kaţdý kód, který dokáţe vytvářet další kód, bychom tak mohli povaţovat za metaprogramování. Pro naše účely, ale tuto definici poněkud zúšíme. Tentokráte si půjčíme definici z [Perrotta2010], kde je metaprogramování definováno následovně: „Metaprogramování je psaní kódu, který dokáže za běhu aplikace manipulovat s konstrukty jazyka“. Tato definice uţ bohuţel není tak srozumitelná, ale mnohem více vystihuje, co budeme my povaţovat za metaprogramování. Důleţité je v této definici zdůraznění, ţe k manipulacím s jazykem dochází za běhu programu. Zde je základní rozdíl mezi generátory kódu, které generují fyzický kód, kdeţto metaprogramováním se tento kód generuje (vytváří) pouze v paměti a nikde fyzicky neexistuje. Metaprogramování se pak díky otevřeným třídám v Ruby pouţívá například k úpravě definic tříd aţ za běhu programu. Reflexe Reflexe je pojem, který se pojí velice úzce s metaprogramováním. Jazyk, ve kterém se píší metaprogramy se nazývá metajazyk. Pokud jazyk dokáţe být zároveň svým metajazykem, můţeme o něm říci, ţe má schopnost reflexe. Ruby je přesně tento typ jazyku. Kdyţ píšeme kód, který manipuluje s programem za běhu aplikace, nepotřebujeme k tomu ţádný jiný programovací jazyk, ale vystačíme si pořád jen s Ruby. Ruby je tedy zároveň svým vlastním metajazykem. Nyní, kdyţ známe základní termíny, se můţeme vrátit zpět k našemu příkladu. Techniku, kterou jsme v těle modulu BuildingCreator pouţili, byla právě technika reflexe. Pojďme se nyní podívat blíţe na metodu build: def self.build(name, &block) building = Building.new(name) building.instance_eval(&block) return building end
Metoda build akceptuje dva paremtry, z nichţ prvním je název budovy (name) a druhým je blok kódu (&block). Nejdříve vytvoříme instanci třídy Building, které předáme název 102
budovy. Nyní chceme vytvořit patra a místnosti, které má dům, tak jak jsme ho v našem DSL definovali, obsahovat. Inormace o patrech a o místnostech se nyní ukrývají v bloku kódu. V této chvíli je důleţité si uvědomit, co parametr &block obsahuje a jakým způsobem s ním můţeme dále pracovat: floor do room 'kitchen', :area => 30 room 'dining room', :area => 20 room 'living room', :area => 40 end floor do room 'bed room', :area => 40 room 'bed room', :area => 50 end
Výše je uveden blok kódu, který je obsaţen v parametru &block. V této podobě je blok kódu předán jako argument do metody instance_eval, která je zavolána na instanci třídy Building. Metoda instance_eval vyhodnocuje řetězec, který obsahuje kód nebo blok kódu
v kontextu svého adresáta (příjemce) [Thomas2004]. Pokud vám není jasné, co tato metoda dělá, pak nezoufejte. Jinými slovy to znamená, ţe instance_eval vyhodnotí blok kódu a výsledek zašle jako zprávu instanci building. V našem případě bude instanci building zaslána zpráva (metoda) floor, která bude mít jako argument blok kódu. Pokud to ani nyní není jasné, podívejte se následující kus kódu: building.instance_eval(&block)
Kód výše je interně za běhu programu vyhodnocen do následujcí podoby: building.floor do room 'kitchen', :area => 30 room 'dining room', :area => 20 room 'living room', :area => 40 end
Vše tedy nakonec končí voláním metody floor na instanci building. Nyní ale nastává druhý problém. Pokud si ještě vzpomínáte na implementaci třídy Building, tak ta ţádnou metodu floor neobsahovala. Nyní jsme měli na výběr dvě moţnosti. První moţností bylo doplnit do
definice třídy Building metodu floor. Tím by ale modul BuildingCreator byl závislý na vnitřní implementaci třídy Building a navíc by třída obsahovala metodu, kterou by vyuţíval pouze námi definovaný modul. Rozhodli jsme se tedy pro druhou moţnost a tou je, ţe metodu floor do třídy Building doplníme, ale aţ za běhu programu. Vyuţili jsme otevřenosti tříd 103
v Ruby a dynamicky za běhu programu jsme do třídy doplnili novou metodu instance. Přidání metody floor do třídy Building je vyjádřeno následujícím kusem kódu: Building.class_eval do define_method :floor do |&block| floor = Floor.new(@floors.size + 1) floor.instance_eval(&block) @floors << floor end end
Metoda class_eval je velice podobná metodě s instance_eval. Rozdíl spočívá v tom, ţe class_eval vyhodnocuje řetězec nebo blok, které přijámá jako parametry, v kontextu
modulu30. Metoda se pouţívá například pro přidávání metod do tříd. class_eval také umoţňuje volání privátních metod třídy. Obou těchto vlastností je vyuţito právě v našem příkladu. Pomocí soukromé metody define_method jsme dynamicky do třídy přidali novou metodu. Jak jiţ název metody napovídá, define_method slouţí k definici instanční metody v adresátovi. Jako parametry slouţí symbol, který odpovídá názvu nové metody a block kódu nebo funkce (instance třídy Proc nebo Method), které představují tělo metody. Kdyţ si odmyslíme v našem příkladu metodu class_eval, tak si lze představit pouţití define_method takto: class Building define_method(:floor) do |&block| floor = Floor.new(@floors.size + 1) floor.instance_eval(&block) @floors << floor end end
Toto pouţití přesně odpovídá také následujícímu zápisu: class Building def floor(&block) floor = Floor.new(@floors.size + 1) floor.instance_eval(&block) @floors << floor end end
30
Metoda class_eval má alias module_eval a to proto, ţe její pouţití je moţné jak na třídy tak na moduly. Vyplývá to z objektové hierarchie v Ruby, kdy předkem objektu Class je objekt Module. 104
Můţeme vidět, ţe pomocí metod class_eval a instance_method jsme byli schopni to třídy Bulding dynamicky za běhu programu přidat metodu floor, kterou bychom jinak do třídy
zapsali, tak jak je vidět v poslední ukázce. Stejným postupem je do třídy Room přidána aţ za běhu programu metoda floor, tento postup zde jiţ tedy nebudeme rozebírat. Uvedenému postupu, v němţ jsou metody do tříd přidávány, aţ za běhu programu se říká Monkey Patching. Označuje se tak hlavně postup, kdy jsou za běhu aplikace měněny staticky definované třídy. Dynamické volání metod Pokud jsme se zmínili o metodách class_eval a instance_eval, musíme se alespoň krátce zmínit také o metodě eval. Zde se dostáváme na pomezí reflexe a dynamického volání metod. Protoţe ale budeme metodu eval porovnávat spíše s konstrukcemi, které se pouţívají pro dynamické volání metod, byla právě do této sekce metoda eval zařazena. Dle [Thomas2004] eval vyhodnocuje Ruby výraz, který mu byl poskytnut v podobě řetězce. Tuto definici lze interpretovat tak, ţe eval vyhodnocuje řetězec jako jakýkoliv jiný výraz v Ruby. Síla této metody spočívá v tom, ţe lze dynamicky vytvořit výraz v podobě řetězce a ten následně nechat vyhodnotit metodou eval. Podívejte se na následující příklady, po jejichţ shlédnutí bude snad vše jasnější: eval "2 + 3" #=> 5 eval "def plus(a, b); a + b; end" eval "plus(2, 3)" #=> 5
Metoda eval můţe být vyhodnocována i v jiném kontextu, neţ je volána. Pro poskytnutí kontextu, slouţí druhý, nepovinný parametr této metody. Ukázku a podrobnější vysvětlení lze nalézt v [alHabache2008], kde lze nalézt mimo jiné i další přiklady týkající se reflexe v Ruby. Připomeňme si jen v krátkosti, jakým způsobem se v Ruby volají metody instancí. Vezmeme si jednoduchý řetězec a zavoláme na něj metodu pro zjištění jeho délky length: "This is my string".length #=> 17
Pro zjištění délky řetězce jsme řetězci zaslali zprávu length a jako odpověď jsme dostali délku řetězce, v tomto případě číslo, které odpovídá sedmáncti znakům. Důleţité je všimnout si, ţe jsme objektu zaslali zprávu. Právě na tomto principu je postavená druhá metoda, která se pouţívá pro dynamické volání metod. Jedná se o obecnou metodu send, která má jako parametry název volané metody a její případné parametry. Její vyuţití si ukáţeme opět na zjištění délky řetězce:
105
"This is my string".send(:length) #=> 17
Pro ukázku, jak lze metodu send vyuţít i pro volání metody, která má nějaké parametry, zašleme řetězci zprávu o tom, zda obsahuje slovo string: "This is my string".send(:include?, "string") #=> true
Třetí cestou, jak lze v Ruby dynamicky zavolat metodu, je pomocí techniky, kdy z metody nejdříve uděláme instanci třídy Method, na které následně zavoláme metodu call. Říká se, ţe všechno v Ruby je objekt a tak lze i metody převést do objektové podoby, konkrétně do instancí třídy Method. Metody lze v této podobě předávat jako argumenty do ostatních metod, ale tím se zde zabývat nebudeme. Pouze to vypichujeme jako informaci, která by mohla zapadnout. Pojďme se nyní podívat, jakým způsobem lze vyuţít instanci třídy Method k dynamickému volání metod: method_length = "This is my string".method(:length) method_length.call #=> 17
Pouţití stejného postupu v případě volání metody, která obsahuje argument: method_include = "This is my string".method(:include?) method_include.call("string") #=> true
Dynamické volání metod má i své stinné stránky. Flexibilita, kterou tyto techniky poskytují, je vykoupena především pomalým zpracováním. Při přemýšlení nad tím, jakou z uvedených technik ve svém programu pouţít, je třeba vţdy zváţit výkonnostní hledisko. Především zpracování metody eval je řádově pomalejší, neţ ostatní dva způsoby. Rychlost zpracování jednotlivých příkazů můţeme demonstrovat na příkladu, který byl převzat z [Thomas2004]:
106
require "benchmark" string = "This is my string" n = 100000 Benchmark.bmbm {|x| x.report("send") { n.times { string.send(:length) } } x.report("call") { n.times { string.method(:length).call } } x.report("eval") { n.times { eval "string.length" } } } #=>
user
system
total
real
#=> send
0.016000
0.000000
0.016000 ( 0.015000)
#=> call
0.047000
0.000000
0.047000 ( 0.056003)
#=> eval
0.749000
0.000000
0.749000 ( 0.754043)
107
108
Závěr Hlavním cílem této diplomové práce bylo zodpovědět otázku, zda je jazyk Ruby vhodný pro zařazení do výuky. Celá práce byla zpracovávána v kontextu Vysoké školy ekonomické v Praze (VŠE) a tak by cíl práce mohl být stanoven ještě v uţším významu. Bylo by vhodné zařadit jazyk Ruby mezi vyučované programovací jazyky na VŠE? Ani na konci celého textu, není tato otázka snadno zodpověditelná. Důvodem je fakt, ţe k problému je moţné přistupovat z několika různých hledisek. Otázka, zda by mělo smysl jazyk Ruby zařadit do výuky je sama o sobě velice široká, tím pádem nesnadno uchopitelná, a je potřeba otázku doplnit o další omezující podmínky. První klíčovou otázkou je, zda by mělo smysl, zařadit Ruby jako primárně vyučovaný jazyk pro studium informatiky na VŠE. Cílem VŠE by nemělo být vychovávat ze studentů programátory. Tím by VŠE suplovala obor informatika, který je vyučován na Matematickofyzikální fakultě Univerzity Karlovy nebo na Českém vysokém učení technickém na Elektrotechnické fakultě. Cílem VŠE by mělo být vychovávat ze studentů manaţery informatiky, kteří mají o programování dostatečné povědomí pro výkon své manaţerské pozice. Podíváme-li se na problém výběru vhodného programovacího jazyka touto optikou, vychází nám, ţe vhodný programovací jazyk nemusí být nutně ten nejrozšířenější, ale spíše ten, který na studenty klade co nejmenší vstupní nároky pro pochopení daného oboru. Vezmeme-li v úvahu moţnost, ţe cílem VŠE je poskytnout studentům dostatečné vzdělání, aby se z nich stali programátoři, je nutné pracovat se zcela jinými argumenty. Prvním z nich je určitě rozšířenost daného jazyka. Nemá smysl učit studenty jazyk, který se v praxi pouţívá pouze ve velmi malé míře. Ruby dosahuje na indexu TIOBE hodnot kolem 1,5%, zatímco Java celý ţebříček jednoznačně vede. Dalším faktorem, který je třeba brát v potaz, je směřování programátorů po absolvování školy. Má-li být budoucím zaměstnavatelem programátorů z VŠE větší korporace, je důleţité se zamyslet nad otázkou jaké programovací jazyky, se ve kterém prostředí pouţívají. Ruby je díky své dynamičnosti a volným syntaktickým pravidlům předurčeno spíše k programování v malých týmech nebo dokonce pro práci ve stylu jednotlivce na volné noze. Práci v Ruby ve větších týmech (10 a více členů) si lze představit pouze s jasně nastavenými pravidly a velkou disciplínou jednotlivých členů týmu. Naopak Java si disciplínu vynucuje sama jiţ ze své podstaty a neklade tím pádem tak velké nároky na programátory.
109
Pokud by Ruby bylo hodnoceno z hlediska své účelnosti jako jazyk, kterým je moţné navázat na studium Javy, vystupují do popředí zejména prvky, které ho od Javy odlišují. Je nutné vyzdvihnout zejména moţnost kombinovat v Ruby více druhů paradigmat. Ruby kromě imperativního stylu umoţňuje alespoň částečné vyuţití prvků převzatých z funkcionálního programování. Tento prvek je důleţitý zejména kvůli tomu, ţe pootvírá dveře k funkcionálním jazykům, které se běţně nevyučují, a také proto, ţe nutí programátory přemýšlet zcela odlišným způsobem. Tento prvek můţe být zajímavý zejména pro výchovu studentů jako manaţerů informatiky, kterým se otevírají další obzory a rozšiřují si tak svůj přehled. Jak je vidět zařazení jazyka Ruby do výuky programovacích jazyků na VŠE má své opoctatněné pro i proti. Vţdy záleţí, z jakého úhlu pohledu se na danou otázku díváme a dle jakých faktorů sestavujeme své hodnocení. Autor této práce zastává názor, ţe nejvhodnějším řešením by byl kompromis. Vyuţít rozšířenosti Javy, její celosvětové popularity a zkušeností, které byly získány při její výuce na VŠE, a zkombinovat to s novými trendy, v nichţ se začínají čím dál více prosazovat dynamické skriptovací jazyky umoţňující práci ve větší míře abstrakce. Logickým řešením pak je zůstat na platformě Java, ale jako programovací jazyk začít vyučovat Groovy. Při zpracování práce pojednávající o programování je celkem očekávané, ţe kromě klasického textu práce nabídne praktický výstup v podobě programového kódu, který slouţí k demonstraci probírané problematiky. Prvním způsobem, kterým je moţno tento úkol pojmout, je tvorba komplexního příkladu prostupujícího skrze celou práci. Tento postup je velice náročný na vymyšlení takové aplikace, na které by bylo moţné demonstrovat všechny zamýšlené programátorské postupy a techniky. Navíc je téměř nemoţné vymyslet v rámci velké aplikace příklady, které by nepůsobily uměle a nevyvolávaly dojem odtrţenosti od reality. Z výše zmíněných důvodů se proto v této práci vyskytuje pouze pár příkladů, které prostupují skrze více kapitol. Dominují příklady, které se zaměřují pouze na postihnutí vysvětlované problematiky. Jednoúčelovost příkladů umoţnila, aby byly co moţná nejvíce názorné, výstiţné a současně neztrácely nic na své srozumitelnosti. Kromě kódu slouţícího jako doprovodný materiál k probíraným tématům, práce obsahuje ještě jeden praktický výstup. Jedná se o webovou aplikaci, jejíţ existence je zmíněna jiţ v první kapitole. Čtenáři je tak poskytnuta moţnost volby, kdy si můţe zvolit, který způsob zpracování je mu pro čtení a studium práce komfortnější. Vznik aplikace ale nebyl motivován pouze moţností poskytnout text práce v podobě webových stránek. Hlavní motivací pro vytvoření interaktivní podoby tohoto textu, byla jednak snaha poskytnout čtenáři obrázek 110
ucelené aplikace napsané v jazyku Ruby s vyuţitím zmiňovaných programátorských technik, tak v neposlední řadě touha poskytnout čtenáři moţnost ihned si nabyté znalosti vyzkoušet a rychleji osvojit. Kromě textu práce je tak v rámci aplikace čtenáři poskytnut interaktivní interpret jazyka Ruby, který ihned vyhodnotí všechny zadané příkazy a výstup vypíše na obrazovku. Webová aplikace se tak snaţí řešit problém většiny učebnic programování, kdy čtenář nemá moţnost získat okamţitou zpětnou vazbu na své čerstvě nabyté znalosti. Tato diplomová práce zdaleka nepokryla a nevyčerpala celé téma a otvírá mnoho cest, kterými by bylo moţno na danou problematiku navázat. Jednou z nich by mohla být práce, která by obecně pojednávala o studiu a výuce programovacích jazyků z hlediska vnímání člověka (kognice) se zaměřením na metodologii výuky. V rámci VŠE se nabízí spolupráce s katedrou psychologie a sociologie řízení na podnikohospodářské fakultě. Studium programování a programovacích jazyků je natolik specifické, ţe by metodologie výuky mohla být stavěna s ohledem, jak na vyučovaný předmět, tak na studenty, kteří jej absolvují. Druhý moţný způsob, kterým by na práci šlo navázat je techničtějšího rázu, a sice zpracovat téma, které by pojednávalo o nástrojích, jeţ jsou v jazyce Ruby napsány. Zřejmě by se jednalo zejména o frameworky pro vývoj webových aplikací. Hlavní důraz by pak mohl být zaměřen zejména na Ruby on Rails, díky kterému se Ruby dostalo do povědomí odborné veřejnosti po celém světě. Práce byla zpracovávána na půdě ekonomicky zaměřené vysoké školy a proto třetí směr, kterým by bylo moţné navázat, je ekonomicko-manaţerský. Ruby a zejména Ruby on Rails deklarují svou vysokou efektivitu a rychlost vývoje. Bylo by určitě zajímavé zpracovat práci nebo případovou studii pojednávající o efektivitě vývoje v Ruby oproti jiným programovacím jazykům. Zpracování by mohlo brát v úvahu zejména robustnost projektu a velikost programátorského týmu.
111
112
Použitá literatura [alHabache2008]
ALHABACHE, Khaled. Khaled alHabache's official blog [online]. December 19, 2008 [cit. 2011-06-20]. Ruby reflection. Dostupné z WWW: < http://www.khelll.com/blog/ruby/ruby-reflection/>.
[alHabache2008b]
ALHABACHE, Khaled. Khaled alHabache's official blog [online]. December 28, 2008 [cit. 2011-06-20]. Ruby callbacks. Dostupné z WWW: < http://www.khelll.com/blog/ruby/ruby-callbacks/>.
[alHabache2009]
ALHABACHE, Khaled. Khaled alHabache's official blog [online]. May 24, 2004 [cit. 2011-06-19]. Ruby and Functional Programming. Dostupné z WWW: .
[alHabache2009b]
ALHABACHE, Khaled. Khaled alHabache's official blog [online]. May 24, 2009 [cit. 2011-06-20]. Ruby Currying. Dostupné z WWW: .
[Avdi2010]
GRIMM, Avdi. Virtous Code [online]. 2008 February 23 [cit. 2011-06-19]. Monkeypatching is Destroying Ruby. Dostupné z WWW: .
[Atwood2008]
ATWOOD, Jeff. Coding Horror [online]. Jul 12, 2008 [cit. 2011-06-19]. Monkeypatching For Humans. Dostupné z WWW: .
[Confreaks2010]
HANSSON, David. Confreaks [online]. 2010 [cit. 2011-06-19]. Ruby Conference 2010. Dostupné z WWW: .
[Fowler1999]
FOWLER, Martin. Refactoring: Improving the Design of Existing Code. July 8, 1999. 464 s.
[Fowler2010]
FOWLER, Martin. Martin Fowler [online]. 2010 [cit. 2011-06-20]. DomainSpecificLanguage. Dostupné z WWW: .
[Friedl2006]
FRIEDL, Jeffrey. Mastering Regular Expressions. Sebastopol, CA : O'Reilly, 2006. 544 s.
[GitHub2010]
Github [online]. 2011 [cit. 2011-02-17]. Secure source code hosting and collaborative development - GitHub. Dostupné z WWW: .
[Goldberg1996]
GOLDBERG, Benjamin. Functional Programming Languages. ACM Computing Surveys [online]. March 1996, 28, [cit. 2011-06-19]. Dostupný z WWW: .
[Haskell2008]
HaskellWiki [online]. 3 January 2008 [cit. 2011-06-20]. Currying. Dostupné z WWW: .
[Hopkins2010]
HOPKINS, Peter. Big think [online]. 2010 [cit. 2011-06-19]. Big Think Interview With David Heinemeier Hansson. Dostupné z WWW: .
[James1987]
JAMES, Geofrey. The Tao of Programming. Santa Monica, California : Info Books, 1996. 151 s.
113
[Linnebo2008]
LINNEBO, Oystein. Compositionality and Frege's Context Principle [online]. [s.l.] : University of Bristol, 2008 [cit. 2011-06-19]. Dostupné z WWW: .
[LoudThinking]
HANSSON, David. Loud Thinking [online]. 2005 [cit. 2011-06-19]. About David Heinemeier Hansson. Dostupné z WWW: .
[Malý2011]
MALÝ, Martin. Zdroják.cz [online]. 31. 1. 2011 [cit. 2011-06-20]. Javascriptaření: hrajte si s funkcemi!. Dostupné z WWW: .
[Matsumoto2001]
MATSUMOTO, Yukihiro. Ruby in a Nutshell. Sebastopol, CA : O'Reilly, 2001. 218 s.
[Moris2010]
MORRIS, Tony. Tony Morris [online]. 2010 [cit. 2011-06-20]. What Does Functional Programming Mean?. Dostupné z WWW: .
[Opensource2010]
Open Source Initiative [online]. 2010 [cit. 2011-06-19]. Open Source Initiative OSI The MIT License (MIT):Licensing. Dostupné z WWW: .
[Perrotta2010]
PERROTTA, Paolo. Metaprogramming Ruby: Program Like the Ruby Pros. February 22, 2010. 240 s.
[Prechelt2002]
PRECHELT, Lutz. Institute for Program Structures and Data Organization : Institute for Program Structures and Data Organization [online]. 2002 [cit. 2011-06-19]. Are Scripting Languages Any Good? A Validation of Perl, Python, Rexx, and Tcl against C, C++, and Java. Dostupné z WWW: .
[Raiter1993]
RAITER, Brian. Muppetlabs [online]. 1993 [cit. 2011-06-19]. The Brainfuck Programming Language. Dostupné z WWW: .
[Raymond2003]
RAYMOND, Eric. The Art of UNIX Programming. Boston, Massachusetts : AddisonWesley Professional, 2003. 560 s.
[Sosinski2008]
SOSINKI, Robert. Robert Sosinski [online]. 2008/12/21 [cit. 2011-06-20]. Understanding Ruby Blocks, Procs and Lambdas. Dostupné z WWW: .
[Spolski2006]
SPOLSKI, Joel. Joel on Software [online]. August 01, 2006 [cit. 2011-06-20]. Can Your Programming Language Do This?. Dostupné z WWW: .
[Thomas2004]
THOMAS, Dave, et al. Programming Ruby : The Pragmatic Programmer’s Guide. Second Edition. Dallas Texas : The Pragmatic Bookshelf, 2004. 833 s.
[Tiobe2011]
TPCI History for Language Ruby [online]. 2011 [cit. 2011-06-26]. TIOBE Software. Dostupné z WWW: .
[Venners2003]
VENNERS, Bill. Artima Developer [online]. September 29, 2003 [cit. 2011-06-19]. The Philosophy of Ruby. Dostupné z WWW: .
114
[Ward2004]
WARD, M. P. Language Oriented Programming [online]. October 2004 [cit. 2011-0620]. Dostupné z WWW: .
[Whamond2010]
WHAMOND, Chris. Signal vs. Noise [online]. Dec 10 2010 [cit. 2011-06-19]. Making Sense with Ruby‘s unless. Dostupné z WWW: .
[Why2005]
Why‘s (Poignant) Guide to Ruby [online]. 2005 [cit. 2011-06-19]. Dostupné z WWW: .
115