Hlavní navigace

Perličky: jednoduché výjimky

Michal Svoboda

Více nebo méně rozvinutý systém výjimek existuje v každém vysokoúrovňovém programovacím jazyce. V Perlu stojí implementace výjimek na velmi jednoduchém principu, nad kterým lze podle potřeby vybudovat aparát s požadovaným stupněm rozvinutí. Dnes bude probrán ten nejjednodušší, ovšem často postačující.

Výjimky v Perlu

Základy vyvolání a zachycení výjimek v jazyce Perl leží ve dvojici syntaktických konstrukcí dieeval. Vestavěná funkce die v běžném programu způsobí jeho ukončení, s případnou doplňující zprávou. Uvnitř bloku eval nedojde k ukončení programu, pouze tohoto bloku. Případnou doplňující hlášku nalezneme v proměnné $@ (pro angličtináře $EVAL_ERROR).

eval { die "vyjimka" };         # není-li řetězec ukončen \n,
                    # přidá se na konec informace
                    # o místě výskytu výjimky
print '$@: ', $@;           # $@: vyjimka at a.pl line 6.

eval { die "vyjimka\n" };
print '$@: ', $@;           # $@: vyjimka


eval {
    eval {
        die "vyjimka\n"
    };
    die             # prázdné die s neprázdným
                    # $@ přidá informaci o propagaci
};

print '$@: ', $@;           # $@: vyjimka at a.pl line 8.
                    #        ...propagated at a.pl line 10.

Ošetřit takovouto výjimku je možné pomocí klasického porovnání řetězců vůči $@. Jelikož je $@ globální, výskyt výjimky v ošetřovacím kódu má za následek přepsání $@. Při složitějším ošetřování je tedy vhodné si uložit nejprve obsah $@ do lokální proměnné.

sub f1 {
    die "vyjimka";
};

sub f2 {
    die "neco jineho";
};

for my $f (\(&f1, &f2)) {
    eval {
        $f->();
    };
    my $error = $@;

    if ($error ~~ /^vyjimka/) {
        print 'Chycena vyjimka: ', $@;
    } else {
        $@ = $error;
        die;
    }
}

print 'Konec programu';

Při volání první funkce f1 je vyvolána výjimka "vyjimka". Dejme tomu, že náš program je schopen tuto výjimku ošetřit, porovnáváme tedy $@ s jejím výskytem. (Zde je malá reklama na nový smart-match operátor ~~, dostupný od verze Perlu 5.10 a téměř výhradně používaný v Perlu 6.) Při volání funkce f2 se vyskytne jiná výjimka, kterou nejsme schopni ošetřit, a tudíž propagujeme výjimku dále. V našem programu již další nadřazený blok eval není, výskyt druhé výjimky tedy ukončí celý program a na vykonání posledního print již nedojde.

Umřít nebo zhebnout?

Představme si, že máme následující modul pro vypsání řady čísel:

package Rada;

use Exporter::Easy ( EXPORT => [ qw/&rada/ ] );

sub rada {
    my $max = shift // 0;
    die "Parametr musi byt kladny" if $max <= 0;

    local ($\, $,) = (qq/\n/, q/ /);
    print (1 .. $max);
};

1;

A v programu vypisradu.pl jej voláme takto:

use Rada;

rada(5);
rada(-1);

První volání vypíše správně řadu čísel, druhé volání (samozřejmě) umře s nepříliš vypovídající hláškou.

1 2 3 4 5
Parametr musi byt kladny at Rada.pm line 14.

Pomocí funkce croak z modulu Carp můžeme posunout „perspektivu“ chybové hlášky do volajícího modulu.

package Rada;

use Carp;
…
    croak "Parametr musi byt kladny" if $max <= 0;
…

Výsledná hláška pak bude „ukazovat“ na místo volání funkce rada.

Parametr musi byt kladny at vypisradu.pl line 9

Úplné doznání k posloupnosti volání dostaneme pomocí funkce confess, případně pomocí použití parametru -MCarp=verbose při volání skriptu.

Parametr musi byt kladny at Rada.pm line 13
        Rada::rada(-1) called at vypisradu.pl line 9

Automatické vyvolání výjimky

Řada zabudovaných funkcí Perlu, například open, (bohužel) používá namísto výjimek sémantiku vracení false při neúspěchu. Pedantické testování návratových hodnot je velmi nezáživné, a proto většina programátorů návratové hodnoty kontroluje pouze v těch nejvážnějších případech, či vůbec. Neošetřená chyba se ale může propagovat dále v kódu a vynořit na místě, který s původní funkcí nesouvisí.

open my $file, ">/etc/ahoj";
print $file "ahoj";
close $file;

Za podmínky, že nemáme přístup pro zápis do /etc, bude výstup programu vypadat nějak takto:

print() on closed filehandle $file at error.pl line 7.

… což nám o skutečném problému vypovídá přesně nic. (Hláška se objeví, pouze pokud máme use warnings; a ke všemu program drze skončí úspěšně.) Abychom nemuseli každou operaci testovat ( dadadada or die "selhalo dadadada"), použijeme modul Fatal, který umí kolem požadovaných funkcí nainstalovat pohodlný wrapper. Volaná funkce pak při selhání nevrací speciální hodnotu, ale výjimku.

use Fatal qw/open close/;

# zbytek jako předtím

Výstup programu pak bude:

Can't open(GLOB(0x606c50), >/etc/ahoj): Permission denied at (eval 1) line 3
        main::__ANON__('GLOB(0x606c50)', '>/etc/ahoj') called at error.pl line 7

Modul Fatal bohužel nejde použít na všechno. Některé funkce nelze univerzálně nahradit žádným wrapperem (například

print). Další skupina funkcí vrací při neúspěchu jinou hodnotu než false, například system. Konečně, některé funkce je potřeba volat v seznamovém kontextu a wrapper modulu Fatal by tento kontext nezachoval. Řešením všech případů je buď si napsat vlastní wrappery, nebo použít již existující moduly, např. IO::Moose pro objektové I/O včetně výjimek, nebo IPC::System::Simple pro náhražku funkce

system.

Na CPANu lze také (ačkoliv zatím pouze v beta verzi) nalézt modul autodie, který řeší některé z těchto problémů, ale zejména přidává novou funkcionalitu – lexikální omezení efektů, kde lze používat use autodieno autodie pro každý blok zvlášť, podobně jako  use strict.

Pokračování příště

V dnešním článku byly ukázány principy, na kterých stojí aparát výjimek v jazyce Perl. Prozatím jsme se však zabývali pouze výjimkami reprezentovanými řetězci. Takováto reprezentace v řadě případů plně postačuje, zejména pokud není potřeba různé výjimky třídit a ošetřovat zvlášť (princip vše nebo nic). V příštím článku si ukážeme, jak elegantně jsou tyto principy našroubovány na již probraný aparát objektového programování (což, jako v jiných programovacích jazycích, dává výjimkám úplně novou dimenzi), zejména pak moduly ErrorException::Class. Samozřejmě, má-li některý z čtenářů zkušenosti s nějakým jiným zajímavým modulem, ozvěte se, a případně jej včleníme do článku také.

Demonstrační příklady pro další experimenty: jednoduché generování a chytání výjimek, modul Radaprogram využívající modul Rada.

Našli jste v článku chybu?