Perl (110) - Moose - meta API

Perl V Moose máme speciální aplikační rozhraní, díky němuž jsme schopni v jistém ohledu modifikovat samotný objektový systém.

16.5.2010 00:00 | Jiří Václavík | přečteno 10708×

Pomocí rozhraní meta API lze nastavovat základní vlastnosti objektového systému a ovlivňovat věci jako chování metod, atributů a tříd.

Získání metadat

Základní metodou pro práci s vlastnostmi objektového systému je meta, která zpřístupňuje metadata dané třídy. Zde je příklad užití. Takto lze získat seznam metod nebo atributů s vlastnostmi.

  $meta = MojeTrida->meta();

  for $atribut ($meta->get_all_attributes){
      print "Atribut: ", $atribut->name(), " ";
      print $atribut->type_constraint->name if $atribut->has_type_constraint;
      print "\n";
  }

  for $method ($meta->get_all_methods){
      print $method->name," ";
  }

Poznamenejme, že namísto MojeTrida na prvním řádku můžeme použít slovo __PACKAGE__, které vyjadřuje jméno aktuálního balíku.

$meta = __PACKAGE__->meta;

Podobně lze získat metodou linearized_isa seznam rodičovských tříd a pomocí subclasses podtřídy.

Modifikace tříd pomocí metadat

Pro zajímavost uveďme, že třídy lze upravovat přímo pomocí změny metadat. Ukažme si, jak lze přidávat metody a atributy. Máme zde k dispozici metody add_method a add_attribute, kterým předáme příslušné parametry.

$meta = __PACKAGE__->meta;
$meta->add_method("nova_metoda" => sub{udelej_neco(@_)});
$meta->add_attribute(
    name => "novy_atribut",
    is   => "rw",
    isa  => "Int"
);

Toto chování lze vypnout pomocí metody make_immutable (případně zapnout pomocí make_mutable). V takovém případě bude při pokusu o změnu třídy pomocí metadat vyvolána výjimka.

$meta->make_immutable;

Fyzická reprezentace atributů

Atribut v Moose je ve skutečnosti objekt typu Moose::Meta::Attribute. To zásadně mění náš pohled na ně.

K těmto objektům lze samozřejmě přistupovat. K tomu slouží metoda get_attribute("jméno atributu"). Například objekt reprezentující atribut nazev získáme následovně.

$nazev = $meta->get_attribute("nazev")

Tento objekt má k dispozici několik metod. Pro příklad uvedeme metodu, která vrátí název datového typu.

print $nazev->type_constraint;

Abychom lépe nahlédli na strukturu, uvědomme si, že místo předchozích volání lze typ získat jedním příkazem.

print MojeTrida->meta->get_attribute("nazev")->type_constraint;

Modifikace fyzické reprezentace atributů, metaatributy

Celá situace je o to zajímavější, že strukturu třídy reprezentující atribut lze měnit. Můžeme například přidávat atributy k atributům. Jinými slovy chceme převzít řízení toho, jaký vliv na chování bude mít hash předávaný při vytváření atributu.

Ukážeme si, jak k atributu přidat popisek. Tedy chceme v řídícím hashi vytvořit nový klíč popisek. Názorněji, požadujeme, aby fungoval následující kód.

package MojeTrida::Main;
use Moose;

has "atribut_s_popiskem" => (
    is        => "rw",
    isa       => "Str",
    popisek   => "Toto je meta-popisek atributu atribut_s_popiskem",
);

Je dobré si zde uvědomit, že zde opravdu má smysl chtít vytvářet atributy. Popisek je totiž vlastností našeho atributu a proto by nemělo smysl ho uvádět kamkoliv jinam (například do metod naší třídy).

Této funkcionality se dosáhne tak, že definujeme vlastní metatřídu. Každý atribut je objekt typu Moose::Meta::Attribute. My ji potřebujeme přetížit. To znamená, že vytvoříme třídu, kterou nazveme MojeTrida::Meta::Attribute::Popisek, která bude dědit od Moose::Meta::Attribute. V této nové třídě definujeme nový atribut. Zároveň bude třeba někde uvést, že se daný atribut má řídit naší třídou MojeTrida::Meta::Attribute::Popisek (nikoliv standardní třídou Moose::Meta::Attribute).

Poslední požadavek (nastavení metatřídy) se vyřeší přidáním klíče metaclass. Definici našeho atributu tedy lehce upravíme tím, že přidáme jeden řádek.

has "atribut_s_popiskem" => (
    metaclass => "MojeTrida::Meta::Attribute::Popisek",
    is        => "rw",
    isa       => "Str",
    popisek   => "Toto je meta-popisek atributu atribut_s_popiskem",
);

Nyní již Moose ví, že chceme přetěžovat. Zbývá tedy pouze vytvořit třídu MojeTrida::Meta::Attribute::Popisek, která zdědí vše od Moose::Meta::Attribute. Vložíme sem navíc nový atribut popisek. Pomocí predicate=>je_popisek navíc přidáme detektor, protože bude časem potřeba.

package MojeTrida::Meta::Attribute::Popisek;
use Moose;
extends "Moose::Meta::Attribute";

has "popisek" => (
      is        => "rw",
      isa       => "Str",
      predicate => "je_popisek"
);

Nyní můžeme standardně pracovat s objekty typu MojeTrida::Main. Podívejme se pro zajímavost, jak získáme text popisku.

package main;
print MojeTrida::Main->meta->get_attribute("atribut_s_popiskem")->popisek;

Lze sem samozřejmě přistupovat i pomocí objektu.

$a = MojeTrida::Main->new(atribut_s_popiskem=>"hodnota");
print $a->meta->get_attribute("atribut_s_popiskem")->popisek;

Pro přehlednost napíšeme v MojeTrida::Main ještě metodu metainfo, která nám vytiskne informace o všech atributech a jejich popiscích. Odlišíme tak hodnotu atributu a popisek. Metoda metainfo může vypadat takto.

sub metainfo {
      my $self = shift;
      my $info = "";

      for my $a ($self->meta->get_attribute_list){
          my $atribut = $self->meta->get_attribute($a);
          $info .= $atribut->name. ":\n";
          $info .= "  popisek: ".$atribut->popisek."\n"
  if $atribut->isa("MojeTrida::Meta::Attribute::Popisek") and $atribut->je_popisek;
          $info .= "  hodnota: ". $self->{$atribut->get_read_method}. "\n";
      }

      return $info;
}

V našem příkladu bychom po zavolání print $a->metainfo; obdrželi následující výstup.

atribut_s_popiskem:
  popisek: Toto je meta-popisek atributu atribut_s_popiskem
  hodnota: hodnota

Více popisků pro atribut

Budeme-li chtít pro každý atribut vytvořit více různých atributů, pak se stává naznačený postup nečitelný - lze totiž sice podobně jako pro popisek vytvářet další a další podtřídy, avšak po jisté době se každý zamyslí, zda-li je kombinování nesourodých tříd opravdu nezbytné.

Řešení nabízejí role. Klíčové slovo has totiž přijímá jako parametr mimo jiné i traits=>[qw(role1 role2 ...)]. Trait je speciální role, která se použije na objekt a od běžných rolí se jinak ničím neliší.

Modifikujme tedy předchozí příklad tak, aby dělal to samé, ale využil přitom rolí.

Nejprve definujme parametry pro atribut_s_popiskem. Zde vložíme klíč traits, do kterého nyní zadáme pouze jednoprvkový seznam (ačkoliv bychom zde mohli použít více rolí).

has "atribut_s_popiskem" => (
    traits    => ["MojeTrida::Meta::Attribute::Trait::Popisek"],
    is        => "rw",
    isa       => "Str",
    popisek   => "Toto je meta-popisek atributu atribut_s_popiskem",
);

Podtřídu MojeTrida::Meta::Attribute::Popisek nahradíme naši novou rolí MojeTrida::Meta::Attribute::Trait::Popisek. Celkem bude vypadat následovně.

package MojeTrida::Meta::Attribute::Trait::Popisek;
use Moose::Role;

has "popisek" => (
      is        => "rw",
      isa       => "Str",
      predicate => "je_popisek"
);

V metodě metainfo pouze zaměníme volání metody isa na volání does. To proto, že metoda isa je vztah pro podtřídu, avšak nyní máme roli a tak použijeme does.

Vše ostatní může zůstat stejné. Nyní tedy pro přidání dalších atributů stačí do traits přidat MojeTrida::Meta::Attribute::Trait::Cokoliv a tuto roli implementovat podobně jako jsme to dělali u popisku.

Rozšíření Moose - MooseX

Jednou z aplikací meta API je též tzv. MooseX. Díky MooseX si každý může napsat vlastní Moose rozšíření. Řada rozšíření je již k dispozici na CPAN. Návod a možnosti pro rozšiřování je v dokumentaci.

Online verze článku: http://www.linuxsoft.cz/article.php?id_article=1701