Portál AbcLinuxu, 25. dubna 2024 22:08

Programování v jazyce Vala - základní prvky jazyka

4. 9. 2012 | Vratislav Podzimek
Články - Programování v jazyce Vala - základní prvky jazyka  

V minulém díle tohoto „měsíčníku“ (omlouvám se, ale dříve jsem další článek napsat nestihl) jsme si ukázali některé zajímavé vlastnosti programovacího jazyka Vala spolu s krátkými vzájemně nesouvisejícími ukázkami kódu. O tom, jaký ve skutečnosti programovací jazyk je, ale jistě nejlépe napoví, když jej člověk vidí „v akci“, tedy jako zdrojový kód programu, který k něčemu slouží.

Protože jsou však většinou zdrojové kódy běžně užívaných programů velmi dlouhé a komplikované, rozhodl jsem se, že bude pro tento seriál nejvhodnější začít psát jednoduchou aplikaci od prvních řádků kódu, který se zde budu snažit dostatečně komentovat. Samozřejmě především aspekty specifické pro Valu, nikoliv aspekty programování jako takového. Předem upozorňuji, že zdrojový kód bude reflektovat fakt, že je naše aplikace psána pro účely tohoto seriálu. Věřím, že se tak vyhneme zdlouhavým diskuzím o tom, jak by se dal kód změnit, aby byl kratší, používal méně proměnných, běžel o mikrosekundu rychleji apod. Ale dost už bylo řečí kolem, pusťme se do programování.

Obsah

Základní idea

link

Nejdříve si představme základní ideu naší aplikace, abychom věděli, k čemu budeme směřovat. Bude se jednat o jednoduchý nástroj na správu úkolů, který bude umožňovat přidávání a odebírání úkolů, jejich shromažďování do kolekcí a označování za (ne)hotové. Jako pracovní název jsem zvolil DoNotForget, ale rovnou bych rád upozornil, že bych se chtěl vyhnout používání zkratky DNF, protože to je zkratka, doufejme podstatně úspěšnějšího a důležitějšího, projektu Aleše Kozumplíka. Postupně budeme aplikaci rozšiřovat a zdokonalovat, vyvineme různá uživatelská rozhraní, různé způsoby ukládání dat v paměti i na disk a podle zájmu o pokračování seriálu i různá další rozšíření tak, abychom si ukázali funkcionalitu knihoven pro obvyklé činnosti moderní aplikace (např. komunikaci přes DBus nebo získávání dat z webu). V git repozitáři pro kód spojený s těmito články jsem vytvořil samostatný adresář s názvem do_not_forget a příslušným Makefile pro snadnější práci.

Protože si Vala nejlépe rozumí s objekty a protože je objektový návrh přirozenější a v dnešní době častěji používaný, i naši aplikaci si rozdělíme na několik samostatných tříd objektů řešících různé úlohy. No a protože má DoNotForget umožňovat správu úkolů, přirozeným začátkem psaní kódu bude třída reprezentující úkol (Task). Pojďme se tedy podívat na její implementaci a vlastnosti Valy, které využívá.

namespace Tasks {

    /**
       Basic class for tasks allowing storing task's description and state.
    */
    class Task : GLib.Object {

        /* Private attributes */
        protected string desc;

        protected bool _done;

        /* Constructors */
        public Task (string desc) {
            this.desc = desc;
            this.done = false;
        }

        public Task.with_state (string desc, bool done) {
            this.desc = desc;
            this._done = done;
        }

        /* Properties */
        public string description {
            get { return desc; }
            set { desc = value; }
        }

        public virtual bool done {
            get { return _done; }
            set { _done = value; }
        }

        /* standard to_string() method that enables the @"$obj" functionality */
        public virtual string to_string() {
            var done_str = _done ? "done" : "not done";

            return @"$desc ($done_str)";
        }

    }
}

Na prvním řádku definujeme samostatný jmenný prostor pro třídy úkolů a to ze dvou důvodů. Tradiční důvod je pro zamezení kolizí jmen, ten druhý, a pro mě v případě tak malého projektu snad ještě důležitější, je to, že následným používáním using Tasks; na začátku zdrojových kódů využívajících třídy úkolů zdůrazníme, odkud tyto třídy a metody bereme. Kompilátor Valy totiž, na rozdíl např. od Ady, ale podobně jako C nebo C++, neumí při kompilaci zahrnout závislosti a musíme kompilátoru přímo vyjmenovat soubory, ve kterých jsou všechny elementy využívané v naší aplikaci nebo knihovně. Právě z tohoto důvodu považuji za velmi vhodné, když jde na začátku zdrojového souboru vidět (byť nepřímo), odkud se berou mnohé třídy, metody apod. a které soubory je tedy nutné zahrnout do kompilace. I v případě použití samostatného jmenného prostoru bychom mohli using Tasks; vynechat, ale přišli bychom o tuto nápovědu a při používání prvků z toho jmenného prostoru bychom museli psát prefix „Tasks.“.

Nikoho asi nepřekvapí, že dále definujeme třídu Task, která dědí od třídy GLib.Object. Zde si dovolím dvě krátké poznámky – prefix „GLib.“ bychom mohli vynechat, neboť Vala v každém zdrojovém souboru implicitně používá jmenný prostor GLib, nicméně já používám plný název pro zdůraznění, odkud třída Object pochází; neplatí však, že by implicitně i každá třída dědila od třídy GLib.Object podobně, jako to známe např. z Pythonu, ale zároveň bychom neodvozením našich tříd od GLib.Object přišli o některé možnosti. Podrobnosti s dovolením nechám na laskavém čtenáři, věřím, že si vystačíme s poučením, že je doporučováno dědit vlastní třídy od GLib.Object.

U definice atributů asi nepřekvapí nic, snad jen podtržítko v identifikátoru _done, jež je zde použito, aby přirozenější identifikátor done zůstal k dispozici pro veřejně přístupnou vlastnost. Třída Task má definovány dva konstruktory. Vala sice neumožňuje přetěžování metod, takže nemůžeme definovat dva konstruktory pojmenované názvem třídy s různým počtem nebo typy parametrů, ale ve stylu GObject můžeme definovat konstruktory s různými „přídomky“ napovídajícími, jaké parametry očekávají. Dále definujeme dvě vlastnosti, z nichž druhá je označena modifikátorem virtual. Znalým C++ asi není třeba nic vysvětlovat, pro ostatní raději uvádím, že kombinace modifikátorů virtual(u metody předka) a override(u metody potomka) umožňuje polymorfismus.

Klasikou je i použití this pro odkazování se na data instance (i když Vala při překladu do C používá self). to_string je metoda se syntaktickou podporou a její definování zpřístupní infixový zápis proměnných v řetězcích – např. @„ukol: $task“, kde task je instance třídy Task. Metoda využívá klasický ternární operátor známý z jiných programovacích jazyků. Dále je ve zdrojovém kódu definována odvozená třída LongTimeTask určená pro úkoly s delší dobou trvání, u nichž chceme sledovat postup (v procentech). Z hlediska Valy je na této třídě zajímavé jen to, že používá klíčové slovo base pro volání konstruktoru předka (což je vyžadováno) a metody to_string předka. Použití těchto dvou tříd ukazuje test, který provádí základní operace. Jedinou věcí, která by zde mohla překvapit, je klíčové slovo is, jež slouží pro testování typu instance za běhu.

Kolekce úkolů

link

Už tedy umíme uložit informace o jednotlivých úkolech. Chybí nám ale možnost pracovat se skupinami úkolů. Napravíme to tedy třídou, která bude reprezentovat kolekci úkolů, a na její implementaci si ukážeme možnosti pro práci se skupinami objektů. Pro ty z nás, jež s programováním nezačali u „javovských“ kolekcí ani STL knihovny C++, je asi přirozeným základním způsobem pro práci se skupinou objektů použití pole. Stejně přirozené je však očekávat, že nám moderní programovací jazyk nabídne jiné, z hlediska programátora praktičtější, možnosti. Proto rovnou vytvoříme rozhraní, které bude muset každá kolekce úkolů implementovat. Získáme tak možnost jednoduše zaměňovat používání jedné třídy třídou druhou. Na způsobu definování rozhraní ve Vale asi nic nepřekvapí..., nebo ano?

interface TaskCollection : GLib.Object {

    /* Methods for adding and removing tasks */
    public abstract void add_task (Task task) throws TaskCollectionError;
    public abstract void remove_task (Task task);

    /* Method making the indexing syntax for getting items work */
    public abstract Task? get (int index);

    /* Method making the 'in' operator work */
    public abstract bool contains (Task task);

    /**
       Method making the iteration work.
       Vala's interfaces can be used as mixins, so this method is not
       abstract and have its implementation in the interface code.
    */
    public TasksIterator iterator () {
	return new TasksIterator (this);
    }   

    /* Properties */
    public abstract string title {
	get;
	set;
    }

    public abstract int number_of_tasks {
	get;
    }

    /* for debugging */
    public abstract string dump {
	owned get;
    }

}

Přece jen se nějaké nezvyklosti v kódu nebo jeho významu objevují, a tak si je opět jednu po druhé projděme. Dovolím si přeskočit komentář, proč zrovna takové názvy metod a proč zrovna tyto metody, o tom doufám dostatečně vypovídají komentáře přímo v kódu. Nicméně hned první řádek, i když se zdá „neškodný“ skrývá jednu malou záludnost. Možná si říkáte: „Rozhraní zděděné z klasické neabstraktní třídy? Co to?“. Prozradím vám, že význam : GLib.Object je zde trochu jiný. Jedná se totiž o tzv. prerekvizitu, kterou musí splňovat každá třída, která o sobě chce „prohlašovat“, že implementuje námi definované rozhraní. Asi nikoho nepřekvapí, že těchto prerekvizit může být u definice rozhraní uvedeno více. Mohou tam být jiná rozhraní, případně třídy, jejichž potomky musí být třídy implementující rozhraní. Mnohonásobnou dědičnost však Vala nepodporuje (tedy pokud „nedědíme“ rozhraní, kde se nejedná o dědičnost).

I další řádky vypadají trochu podivně, nemyslíte? Deklarování metod v  rozhraní jako abstraktních? Také máte pocit, že by to snad Vala mohla odhadnout z toho, že jsou v definici rozhraní? Mohla, ale je třeba kompilátoru říct, které metody si může implementující třída dovolit přímo používat. Rozhraní ve Vale totiž nejsou jen rozhraní v tradičním významu tohoto slova v objektovém programování, ale tzv. mixiny, což nám dává možnost omezené mnohonásobné dědičnosti. Vedle rozhraní Vala zná i abstraktní třídy (modifikátor abstract) s (volitelně) abstraktními metodami. S deklarací první požadované metody bychom se rovnou měli vrhnout i na výjimky, ale to si, myslím, můžeme nechat na později.

Co znamená otazník u typu, to jsme si již vysvětlili v minulém článku. Tak jen pro zopakování – otazník říká, že se na místě jako hodnota může objevit i null. Na příkladu metody iterator vidíme, že můžeme využít rozhraní i pro definici metody, abychom nemuseli stejný kód opakovat v každé třídě, která rozhraní implementuje. Poslední zajímavostí, než se dostaneme k výjimkám a třídě TaskIterator, je modifikátor owned u definice get části vlastnosti dump. Vala totiž implicitně u vlastností vrací tzv. „unowned“ (nevlastněné) objekty, což jsou, ve zkratce řečeno, jen odkazy na objekty, které nezvyšují počítadlo odkazů daných objektů. Trochu srozumitelnější možná bude, když se zamyslíme nad významy slov „owned“ (vlastněný) a „unowned“ (nevlastněný). Pokud vrátíme objekt jako nevlastněný, znamená to, že volajícího nepřidáváme jako vlastníka objektu, a tudíž naznačujeme, že o (ne)existenci takového objektu rozhodujeme pouze my (což je na místě zejména právě u vlastností, protože jejich hodnoty jsou velmi často odkazy na interní atributy objektů, u kterých nechceme, aby zůstávaly v paměti po dealokaci objektu samotného).

Protože však v našem případě collection.dump vrací nově zkonstruovaný řetězec (který na rozdíl např. od typu int není jednoduchým typem, ale objektem) vytvořený v těle get části, musíme jej vracet jako owned, abychom předali vlastnictví (my o něj totiž přijdeme na konci těla get části, kdy se sníží počítadla odkazů u lokálních proměnných a dojde k případné dealokaci).

Výjimky

link

Konečně se tak dostáváme k výjimkám a třídě TaskIterator. Začněme výjimkami. V úvodu zdrojového kódu našich kolekcí úkolů je tato část:

public errordomain TaskCollectionError {
    MAX_TASKS_LIMIT_EXCEEDED,
}

Vala pro výjimky interně využívá metody dostupné v GLib – GError. Protože však tento systém funguje poněkud jinak než např. výjimky v Javě, měl by být využíván opravdu jen pro chyby vzniklé za běhu, které nelze předem vyloučit a ze kterých je možné se „vzpamatovat“. Pro ostatní případy jsou určené různé testy na omezení představené v předchozím dílu seriálu. Pokud však chceme používat výjimky, měli bychom si definovat chybovou doménu, tedy v podstatě jakousi kategorii chyb. V této kategorii poté můžeme definovat jednotlivé typy chyb. Zachytávat však lze výjimky jen podle kategorie. Až pozdějším testem pomocí klíčového slova is můžeme zjistit, jakého typu je zachycená výjimka. Použití metody, která může „vyhodit výjimku“, pak vypadá např. následovně:

try {
    collection.add_task (task1);
}   
catch (TaskCollectionError e) {
    stdout.printf (@"Failed to add task '$task1': %s\n", e.message);
    if (e is TaskCollectionError.MAX_TASKS_LIMIT_EXCEEDED) {
         stdout.printf ("Cannot add more tasks!");
    }
}

Iterátory

link

Dále je v kódu definována třída TaskIterator, která slouží pro iteraci přes kolekci úkolů, zejména pomocí foreach-cyklu. Abychom mohli přes objekt nějakého typu T iterovat pomocí foreach-cyklu, musí mít buď

Protože máme definováno rozhraní, které musí každá kolekce úkolů implementovat, můžeme definovat obecný iterátor pro všechny naše kolekce. Implementace je samozřejmě k nahlédnutí, testování i různým úpravám na GitHubu.

Pole

link

Už tedy víme, jaké veřejné metody má naše kolekce úkolů mít, a tedy i to, k čemu ji bude možno používat. Co se týče vlastní implementace vnitřních mechanismů, tak ta samozřejmě rozhraním omezena není. Jak už jsem si troufl odhadnout výše, základním a nejpřirozenějším principem, jak ukládat skupinu objektů nebo údajů, je pravděpodobně pole. Vala samozřejmě práci s poli nabízí a přidává i některé praktické mechanismy, které tuto práci usnadňují. Pojďme ale pěkně od začátku. Naše první třída implementující rozhraní kolekce úkolů bude využívat pole o statické velikosti, kde bude položky ukládat jednu po druhé. To samozřejmě přináší několik problémů v podobě plýtvání s pamětí, omezení maximálního počtu položek, přeskládávání položek při smazání atd. To nám však pro ukázku práce s poli nijak nevadí, a proto se podívejme, jak takový kód využívající pole o statické velikosti vypadá. Nejdříve musíme pole deklarovat a vytvořit:

private static const int MAX_TASKS = 100;

private Task[] tasks_array = new Task[MAX_TASKS];

Tento zápis se, nemýlím-li se, nijak neliší od zápisu v Javě nebo C#, zkrátka definujeme pole o velikosti dané konstantou MAX_TASKS. Problémem ale je, že budeme muset hlídat, kde se v poli nachází poslední vložený úkol a kontrolovat, abychom při vkládání nepřesáhli velikost pole. K tomu nám poslouží soukromý atribut private int tasks_top = -1 inicializovaný na hodnotu -1. Tento atribut se používá jako horní mez při procházení pole, zvýšený o jedničku dává počet položek v poli a umožňuje nám vkládat úkoly na nové pozice dále v poli tak, abychom nepřepisovali ty vložené dříve. Musíme jej však samozřejmě správně inkrementovat a dekrementovat při přidávání resp. odebírání položek. Jedinou metodou, kterou si z definice této třídy kolekcí ukážeme, bude právě metoda pro odebírání úkolu, ostatní jsou triviální. Kompletní implementace třídy je samozřejmě k dispozici na GitHubu.

public void remove_task (Task task) {
    int i = -1;
    bool found = false;

    /* find the task or go through the whole array */
    while ((i <= tasks_top) & !found) {
	i++;
	found = tasks_array[i] == task;
    }

    /* if task not found, just return (nothing to do) */
    if (!found) {
	return;
    }

    /* else remove the task (replace by null) */
    tasks_array[i] = null;

    /* and move the rest of the tasks to left */
    for (var j = i; j < tasks_top; j++) {
	tasks_array[j] = tasks_array[j+1];
    }

    /* finally, decrease the tasks_top value */
    tasks_top--;
}

Jak již určitě všichni pochopili, metoda funguje tak, že se pokusí najít daný úkol v interním poli; pokud jej nenajde, skončí, pokud jej najde, odstraní ho z pole přepsáním hodnoty na null a posunutím zbývajících položek s vyššími indexy o jednu pozici směrem k začátku, abychom zaplnili mezeru po tomto úkolu. Věřím, že všechny syntaktické prvky jsou známy a nepotřebují žádný zvláštní komentář. V rámci této třídy se ještě podívejme na metodu get:

public new Task? get (int index)·
    requires (index <= tasks_top)
{
    if (index > tasks_top)
	return null;
    else
	return tasks_array[index];
}

Zajímavé jsou první dva řádky. Klíčové slovo new říká, že toto je nová metoda get, nikoliv nová implementace metody předka, tedy GLib.Object.get. Klíčové slovo new způsobí zamaskování metody předka touto novou metodou; pokud bychom jej vynechali, překladač by nás varoval, že tuto metodu má i předek. Typ s otazníkem a použití omezujících podmínek jsme si vysvětlili již v minulém díle seriálu. Zde si však dovolím malé upozornění – při použití omezujících podmínek v kombinaci s dědičností jsem narazil na chybu, kdy přeložený program nefungoval správně. Později přidám do komentářů odkaz na bugreport, kdyby měl někdo zájem o bližší informace.

Dynamická pole

link

Chceme se ale zabývat především využíváním polí, a proto se pojďme podívat, co dalšího Vala nabízí. Další třída implementující rozhraní kolekce úkolů používá jako interní strukturu pro ukládání dat pole s dynamickou velikostí, které je automaticky zvětšováno v případě potřeby. Zvětšování probíhá tradičním způsobem, tedy vždy na dvojnásobek původní velikosti. Dynamicky zvětšované pole deklarujeme jako:

private Task[] tasks_array = {};

Protože má se však pole dynamicky zvětšuje, nemáme kontrolu nad jeho velikostí. Ta by nás sice nemusela zajímat, ale opět bychom si museli udržovat počet úkolů v kolekci v nějakém atributu. Pole ve Vale mají však pro podobné účely vlastnost length, která, jak název napovídá, vrací délku pole. U pole se statickou velikostí je to velikost pole zadaná při jeho vytvoření (nebo zvětšení – viz dále), u dynamicky se zvětšujícího pole je to aktuální počet položek v poli. Naše nová kolekce úkolů s dynamickým polem tedy nemá žádný atribut tasks_top, ale využívá přímo tasks_array.length s případnou úpravou o -1. Do dynamicky se zvětšujícího pole přidáváme položky pomocí operátoru +=, což ukazuje např. metoda add_task:

public void add_task (Task task) {
    tasks_array += task;
}

Operátor -= však prvky neodebírá, a proto metoda remove_task vypadá téměř stejně jako v případě kolekce se statickým polem jen s tím rozdílem, že používá tasks_array.length místo atributu tasks_top. V závěru metody pak provádíme dekrementaci tasks_array.length, aby následující vložení nové položky proběhlo dle očekávání. Doporučuji si prohlédnout plný zdrojový kód tříd kolekcí úkolů spolu se zdrojovým kódem jejich testů.

Na závěr této podčásti o polích se sluší dodat pár poznámek, pro které se mi nepodařilo vymyslet rozumnou ukázku v kódu. U polí můžeme používat výřezy (pole[1:3]) a samozřejmě můžeme klasickým způsobem definovat vícerozměrná pole (int[,] 2Dpole = new int[4,5];). V případě vícerozměrných polí se z vlastnosti length stává pole hodnot, určující délku jednotlivých dimenzí (v našem případě 2Dpole.length[0] == 4 a 2Dpole.length[1] == 5). Vala nám však neumožní přístup k jednotlivým „řádkům“ pole (de facto vektorům matice), výraz 2Dpole[0] je neplatný a neprojde přes kompilátor a stejně tak i pokus o vytvoření výřezu (slice) z vícerozměrného pole. Výřez z jednorozměrného pole není problém, výsledkem je nové pole o odpovídající velikosti.

Pokud při vytváření pole zadáme do hranatých závorek velikost a vynecháme část od '=' dále (int f[10];), dostaneme pole s fixní velikostí alokované na zásobníku. Ostatní druhy polí můžeme zvětšovat příp. zmenšovat použitím jejich metody resize (např. pole.resize(10)) s tím, že při zmenšování jsou zachována pouze data, která byla na indexech nižších, než je nová velikost. Poslední poznámkou budiž upozornění, že Vala nedělá žádnou kontrolu při přistupování k položkám pole, a tak se snadno dostaneme k „oblíbenému“ zásahu jádra operačního systému v podobě „Segmentation fault“.

Kolekce kolekcí

link

V aplikaci DoNotForget budeme chtít spravovat více kolekcí úkolů. Mohli bychom tedy vytvořit třídu pro kolekci kolekcí a možná i pokračovat v podobném duchu. Uděláme to ale jinak, vytvoříme třídu TaskManager, která bude umožňovat správu kolekcí a s níž později bude komunikovat uživatelské rozhraní aplikace. A protože chceme zabránit inkonzistenci dat, kdy by různé části aplikace používaly různé manažery a vkládaly tak úkoly nebo celé kolekce na různá místa, uděláme za třídy TaskManager tzv. singleton. Vytvoříme třídní atribut (K. Marx by měl radost) udržující jedinou existující instanci třídy, soukromý konstruktor a veřejnou metodu, která naši jedinou instanci vrátí. Vše nám zařídí následující kód:

/** 
    Class for a singleton object, that manages task collections.
*/
class TaskManager : GLib.Object {

    private static TaskManager instance;

    private HashMap<string, TaskCollection> _collections;

    public static TaskManager get_instance () {
	instance = instance ?? new TaskManager (); 
	return instance;
    }   

    private TaskManager () {
	_collections = new HashMap<string, TaskCollection> (); 
    }

Operátor ?? nám ze dvou výrazů vybere ten, který je různý od  null, případně ten druhý. instance ?? new TaskManager (); je tedy ekvivalentní výrazu a != null ? a : b;. Privátní atribut _collections využívá generický typ HashMap z knihovny Gee. Proto to musíme při kompilaci kompilátoru říct pomocí přepínače --pkg, konkrétně --pkg=gee-1.0, který přidáme do našeho Makefile. Podrobněji se na tuto knihovnu i její využití podíváme v příštím díle, prozatím si doufám vystačíme s vysvětlením, že TaskManager využívá mapu, která mapuje názvy kolekcí na kolekce samotné. Ve zbývající části zdrojového kódu této třídy není z  hlediska programovacího jazyka nic zajímavého ani nového.

Pomocné funkce

link

Konečně máme „zázemí“ připraveno a můžeme se pustit do psaní aplikace DoNotForget a jejího uživatelského rozhraní. Začneme tím nejjednodušším, tedy rozhraním využívajícím příkazový řádek (CLI). A protože očekáváme, že budeme mít různá uživatelská rozhraní, začneme vytvořením jednoduchého rozhraní programového, jež bude muset každé uživatelské rozhraní implementovat. Požadujeme jen, aby bylo možné instanci rozhraní nastavit manažer úkolů a zjistit, jaký manažer úkolů má nastaven (vím, že vzhledem k tomu, že je TaskManager singleton, je tato vlastnost zbytečná, ale bývá zvykem definovat privátní atributy a k nim veřejné vlastnosti) a aby mělo rozhraní metodu run, která po inicializaci aplikace a manažera umožní spuštění rozhraní. Protože začínáme s rozhraním využívajícím příkazový řádek, pomůžeme si malou „knihovničkou“ s několika funkcemi, jejichž krátký kód bychom jinak opakovali pořád dokola. Začneme tou úplně nejjednodušší, jednořádkovou:

void println (string text = "") {
    stdout.printf (@"$text\n");
}

Tato funkce se možná může zdát úplně zbytečná, ale ušetří hodně psaní a zpřehlední ta místa v kódu, kde se odehrává hodně výpisů na standardní výstup. Abychom si usnadnili vypisování prázdného řádku (vytvoření vertikální mezery), používáme parametr s výchozí hodnotou. Následně je možné tuto funkci (nebo proceduru, chcete-li) volat buď jako println (msg);, nebo println ();. Ty z vás, kteří právě zajásali, že je Vala v oblasti parametrů stejná jako Python, budu muset bohužel zklamat. Nic jako println (text = msg); by vám neprošlo a kompilátor by si stěžoval, že nezná název text. Nicméně, pokud si ještě pamatujete, někde výše v tomto článku jsem psal, že Vala neumožňuje přetěžování metod, a nemůžeme tedy mít dvě metody se stejným názvem ale různým počtem parametrů. Častým důvodem použití takových metod je, že chceme dát volajícímu možnost vybrat si tu variantu, jež za něj nastaví předané parametry a zbývající nastaví na výchozí hodnoty. Právě tyto případy můžeme jednoduše pokrýt použitím parametrů s výchozí hodnotou. A nyní se můžeme přesunout k dalším dvěma funkcím:

string raw_input (string prompt) {
    stdout.printf (@"$prompt ");
    var ret = stdin.read_line();

    return ret._strip();
}

bool positive_answer (string answr) {
    return (answr.down() == "y") || (answr.down() == "yes");
}

Zde snad jen krátký komentář, že metoda strip třídy string vrací nový řetězec s ořezanými přebývajícími bílými znaky na začátku i konci řetězce. _strip se liší pouze tím, že tuto úpravu dělá „in-place“, a mění tedy řetězec samotný ('_' na začátku názvu funkce/metody plní obvykle tento význam). Metoda down pak ve stejném duchu vrací nový řetězec, kde jsou všechna písmena převedena na malá. Poslední funkce v naší „knihovničce“ je trochu delší, ale o to důležitější. Budeme chtít, aby uživatel mohl zadávat na vstupu čísla. Zároveň ale budeme chtít zkontrolovat, jestli uživatel opravdu zadal číslo, a podle toho řídit běh programu.

Vala nabízí funkci int.parse, která bere řetězec a vrací hodnotu typu int. Bohužel je však až příliš „bezpečná“, protože pokud předaný řetězec není řetězcovou reprezentací čísla, vrátí hodnotu 0 a programátor se nemá jak dozvědět, že při parsování nastal nějaký problém. A pokud je i 0 validním vstupem, ale potřebujete vědět, jestli uživatel opravdu zadal 0, nebo nějaký nečíselný řetězec, nechá vás Vala na holičkách. Proto budeme potřebovat následující funkci (a patřičnou výjimku):

int checked_int_parse (string number_arg) throws ParsingError {
    unichar c;
    var number = number_arg.strip();
    int i = 0;

    if (number == "")
	throw new ParsingError.NOT_A_NUMBER ("Empty string");

    number.get_next_char (ref i, out c);
    if (!(c.isdigit() || c == '-'))
	throw new ParsingError.NOT_A_NUMBER ("Cannot parse: %s".printf(
								number));

    for (; number.get_next_char (ref i, out c);) {
	if (!c.isdigit())
	    throw new ParsingError.NOT_A_NUMBER ("Cannot parse: %s".printf(
								number));
    }

    return int.parse (number);
}

Navíc nám opět poslouží jako ukázka několika konstrukcí specifických pro Valu. Ve zkratce tato funkce z předaného řetězce ořeže bílé znaky, zkontroluje, jestli je prvním znakem '-' nebo číslice a dále pokračuje znak po znaku a kontroluje, jestli jsou to samé číslice. V případě, že narazí chybu, vyvolá výjimku („oficiálně“ se ve Vale používá název „chyba“) ParsingError. Pokud kontrola projde až za poslední znak, zavolá funkci int.parse, která se postará o převedení řetězce na číslo. Funkce checked_int_parse nás dostává k další oblasti, která zasluhuje komentář a vysvětlení. Určitě si všichni všimli, že při ořezávání předaného řetězce používáme metodu strip, tedy variantu bez podtržítka. A to z toho důvodu, že řetězce se, stejně jako ostatní objekty, předávají pomocí odkazu a „in-place“ varianta by tedy modifikovala předaný řetězec, což nemusí být žádoucí.

Ve Vale existují různé možnosti, jak se předává parametr. Pokud funkce/metoda označí parametr jako ref, očekává, že bude inicializovaný a dává najevo, že jej může změnit (takto se implicitně předávají objekty). Pokud funkce/metoda označí parametr jako out, považuje jej za neinicializovaný a zavazuje se k tomu, že jej inicializuje. Pokud volající označí parametr jako ref, říká, že je inicializovaný a dovoluje metodě/funkci jej změnit. V případě out ze strany volajícího je, jak asi všichni čekáme, možno předat neinicializovaný parametr, u nějž očekáváme, že bude funkcí/metodou inicializován. Rozdíl mezi ref a out tedy určují jen to, kde se provádí testy na různost od null. Hodnotové typy (základní) se kopírují do lokálního prostoru funkce/metody.

Věřím, že teď už vypadá srozumitelněji i volání metody get_next_char, ke kterému ještě dodám, že toto je doporučovaný způsob iterace přes znaky řetězce. Ptáte se proč nestačí obyčejný for-cyklus s inkrementací i1 a adresováním znaků pomocí [i]? Vysvětlením je to, že ve Vale jsou všechny řetězce implicitně v kódování UTF-8 (proto také typ unichar u proměnné c), a některé znaky tedy nemusí být jednobytové.

Uživatelské rozhraní

link

Konečně se tedy dostáváme k uživatelskému rozhraní a samotné aplikaci DoNotForget. CLI využívá funkce z naší „knihovničky“ pomocných funkcí a je implementováno v souboru interfaces.vala, doporučuji prohlédnout si tento zdrojový kód celý. Novinek je pro nás zde jen pár, tak se na ně pojďme v rychlosti podívat. Naše první rozhraní využívá výčtový typ Responses, aby si jednotlivé části mohly předávat informaci o tom, jaký má být další krok z hlavní smyčky (while cyklu), kde jsou pomocí konstrukce switch volány jednotlivé podčásti. Implementace právě popsané části vypadá následovně:

enum Responses {
    NONE,
    SHOW_COLLECTIONS,
    ADD_COLLECTION,
    REMOVE_COLLECTION,
    EDIT_COLLECTION,
    ADD_TASK,
    REMOVE_TASK,
    EDIT_TASK,
    ABOUT,
    QUIT
}

Responses response = main_menu ();

while (response != Responses.QUIT) {
    println ();
    switch (response) {
	case Responses.SHOW_COLLECTIONS: response = show_collections ();
					 break;
	case Responses.ADD_COLLECTION: response = add_collection ();
				       break;
	case Responses.REMOVE_COLLECTION: response = remove_collection ();
					  break;
        case Responses.EDIT_COLLECTION: response = edit_collection ();
					break;
	case Responses.ADD_TASK: response = add_task ();
				 break;
	case Responses.REMOVE_TASK: response = remove_task ();
				    break;
        case Responses.EDIT_TASK: response = edit_task ();
					     break;
	case Responses.ABOUT: response = show_about ();
			      break;
	case Responses.NONE: response = main_menu ();
			     break;
	default: assert_not_reached ();
    }
}

Věřím, že není třeba dalšího komentáře a vše je jasné přímo z kódu. Doporučuji udělat si klon repositáře z GitHubu (pokud jste tak ještě neučinili), prohlédnout si zdrojový kód, spustit příkaz make a trochu si s aplikací DoNotForget pohrát. Rozhodně není dokonalá a určitě jsou v ní chyby, ale jako ukázka možností Valy snad prozatím postačí. Budu samozřejmě vděčný za patche poslané na moji emailovou adresu. Pokud se vám stane, že příkaz make selže, zkontrolujte, jestli máte nainstalován překladač Valy a knihovnu libgee i s devel částí. Kdo by se chtěl vyhnout používání gitu, může si stáhnout archív odpovídající tagu pro tento článek v git repozitáři.

A to je pro tentokrát vše, co se do článku vešlo. Doufám, že se mi podařilo ukázat základní prvky programovacího jazyka Vala a povzbudit alespoň pár čtenářů, aby o Vale uvažovali jako o jednom z kandidátů pro některý z příštích projektů. V dalším díle seriálu napravíme největší nedostatek aplikace DoNotForget přidáním ukládání dat do souboru a jejich opětovného načítání při spuštění. Při té příležitosti si ukážeme i práci s regulárními výrazy, argumenty příkazového řádku a jednoduchými konfiguračními soubory. No a samozřejmě se trochu více podíváme na knihovnu Gee a možnosti, které nabízí, protože bez vymožeností jako jsou množiny, mapy, seznamy apod. se dnes již téměř žádná aplikace neobejde. Konec konců i my jsme si v tomto díle již knihovnou Gee vypomohli pro náš TaskManager.

Seriál Programování v jazyce Vala (dílů: 3)

První díl: Programování v jazyce Vala – úvod, poslední díl: Ze 4 s na 0,9 s – programovací jazyk Vala v praxi.
Předchozí díl: Programování v jazyce Vala – úvod
Následující díl: Ze 4 s na 0,9 s – programovací jazyk Vala v praxi

Další články z této rubriky

LLVM a Clang – více než dobrá náhrada za GCC
Ze 4 s na 0,9 s – programovací jazyk Vala v praxi
Reverzujeme ovladače pro USB HID zařízení
Linux: systémové volání splice()
Programování v jazyce Vala - základní prvky jazyka

ISSN 1214-1267, (c) 1999-2007 Stickfish s.r.o.