Thu Nov 18 15:30:35 2010

Digg! submit to reddit Delicious RSS Feed Readers

Columna 94 del Linux Magazine (juny de 2007)

[títol suggerit: «El Moose està volant (1a part)»]

El sistema d'objectes de Perl és molt «flexible», és a dir, es construeix de baix cap a dalt. Podeu construir objectes tradicionals basats en taules de dispersió, o objectes més exòtics basats en matrius o objectes capgirats. Aleshores heu de crear els mètodes accessors, definir les polítiques d'accés i generar un munt de codi repetit.

Afortunadament, Perl és prou introspectiu com perquè li encomaneu la majoria del treball dur i avorrit. Això ha provocat que un bon nombre «d'entorns de treball per a classes» hagin estat publicats al CPAN. L'enton de treball Moose va sorgir fa un any i inicialment jo el vaig descartar com «un altre entorn de treball per a classes», de la mateixa manera que un altre sistema de plantilles o un altre mapeig d'objectes relacional em fan sentir.

Tanmateix, vaig fer un cop d'ull recentment al que s'havia convertit Moose i em vaig endur una sorpresa agradable. Així que em vaig posar a jugar-hi, sovint vaig exclamar que aquest entorn de treball m'hauria estalviat un bona pila de temps en alguns projectes passats, com ara el text que vaig escriure per al nostre curs i llibre Intermediate Perl, algunes parts del qual s'han inclòs com la pàgina de manual perlboot a la distribució. Posem-nos a refer les classes «d'animals» d'aquell text utilitzant Moose per veure com aquest entorn de treball emergent simplifica les coses.

Primer crearem la classe cavall a Horse.pm que té un nom i un color:

  package Horse;
  use Moose;
  has 'name' => (is => 'rw');
  has 'color' => (is => 'rw');
  1;

Incorporant Moose es defineix has, al qual se li passa el nom d'un atribut juntament amb les seves propietats. En aquest cas, diem que els dos atributs són de «lectura/escriptura». Ara podem utilitzar aquesta classe:

  use Horse;
  my $talking = Horse->new(name => "Mr. Ed");
  print $talking->name; # prints Mr. Ed
  $talking->color("grey"); # sets the color

Fixeu-vos que no m'ha calgut definir un mètode new: Moose ho fa per mi.

En el text original Horse heretava d'Animal. Podem fer això molt fàcilment. Posem a Animal.pm:

  package Animal;
  use Moose;
  has 'name' => (is => 'rw');
  has 'color' => (is => 'rw');
  1;

I aleshores actualitzem el nostre Horse.pm:

  package Horse;
  use Moose;
  extends 'Animal';
  1;

Fixeu-vos que aquí extends substitueix el tradicional use base i fixa completament @ISA, enlloc d'afegir-hi un element. (És possible que vulgueu posar això dins d'un bloc BEGIN, tot i que encara no he vist cap exemple que ho necessiti).

En aquest punt, Horse i Animal són idèntics. Els dos es poden instanciar i se'ls pot assignar atributs. En l'exemple original, el que distingia un cavall era el so que feia, cosa que podem afegir aquí:

  package Horse;
  use Moose;
  extends 'Animal';
  sub sound { 'neigh' }
  1;

i aleshores fer-hi referència al mètode comú speak d'Animal:

  package Animal;
  use Moose;
  has 'name' => (is => 'rw');
  has 'color' => (is => 'rw');
  sub speak {
    my $self = shift;
    print $self->name, " goes ", $self->sound, "\n";
  }
  sub sound { confess shift, " should have defined sound!" }
  1;

Fixeu-vos en l'ús de confess, un altre regal de Moose. Si la classe derivada no ha definit un so, vull queixar-me. Però com que Horse defineix sound, no veure mai això per un cavall. Amb aquest codi puc crear el meu clàssic cavall parlador:

  my $talking = Horse->new(name => 'Mr. Ed');
  $talking->speak; # says "Mr. Ed goes neigh"

Fins ara només estic programant coses que serien senzilles sense Moose, així que comencem a fer alguns canvis per veure'n tot el poder. Primer, un Animal és en realitat una classe abstracta, utilitzada només per proveïr atributs i mètodes comuns per a una classe concreta (en aquest cas, la classe cavall). En la terminologia de Moose això es descriu millor com un rol. Un rol és com un mix-in, que proveeix una col·lecció d'atributs i mètodes que utilitzen aquells atributs. Un rol mai té cap instància perquè no és una classe completa.

Quan fem que Animal sigui un rol també obtenim suport addicional:

  package Animal;
  use Moose::Role;
  has 'name' => (is => 'rw');
  has 'color' => (is => 'rw');
  sub speak {
    my $self = shift;
    print $self->name, " goes ", $self->sound, "\n";
  }
  requires 'sound';
  1;

Fixeu-vos que hem substituït l'«stub» que incloïa el confess per un requires. Això informa a Moose que les classes que usin aquest rol han de proveir el mètode sound, que serà verificat en temps de compilació. Per atorgar un rol utilitzem with enlloc d'extends:

  package Horse;
  use Moose;
  with 'Animal';
  sub sound { 'neigh' }
  1;

Si ens haguéssim oblidat d'incloure sound hauríem rebut un avís de bon començament. Genial. En aquest punt, Horse segueix funcionant com abans.

Què passa amb les paraules clau with i requires? Com que estan definits per les importacions de Moose i Moose::Role, romandran com a part del paquet. Per als puristes com nosaltres als que no ens agrada aquest tipus de pol·lució, podem eliminar-los quan hàgim acabat utilitzant la paraula clau no (de forma similar a use strict i no strict). Per exemple, podem netejar Horse.pm amb:

  package Horse;
  use Moose;
  with 'Animal';
  sub sound { 'neigh' }
  no Moose; # gets rid of scaffolding
  1;

I de forma similar, Animal.pm necessita no Moose::Role al final.

Moose ofereix suport per a la noció de valor predeterminat. Afegim el color predeterminat i fem que això també sigui responsabilitat de la classe:

  package Animal;
  ...
  has 'color' => (is => 'rw', default => sub { shift->default_color });
  requires 'default_color';
  ...

Si no s'indica el color, es consultarà el color predeterminat de la classe i requires garanteix que la classe concreta proveeix aquest color predeterminat. Les nostres classes derivades ara tenen aquest aspecte:

  ## Cow.pm:
  package Cow;
  use Moose;
  with 'Animal';
  sub default_color { 'spotted' }
  sub sound { 'moooooo' }
  no Moose;
  1;
  ## Horse.pm:
  package Horse;
  use Moose;
  with 'Animal';
  sub default_color { 'brown' }
  sub sound { 'neigh' }
  no Moose;
  1;
  ## Sheep.pm:
  package Sheep;
  use Moose;
  with 'Animal';
  sub default_color { 'black' }
  sub sound { 'baaaah' }
  no Moose;
  1;

Ara «ovella» és una més de les nostres classes implementades:

  use Sheep;
  my $baab = Sheep->new(color => 'white', name => 'Baab');
  $baab->speak; # prints "Baab goes baaaah"

Bé, això és força pim pam. Anem a resoldre alguns dels altres problemes del material original.

La classe Mouse era especial perquè extenia el mètode speak amb una línia addicional de sortida. Tot i que podríem utilitzar els mètodes tradicionals basats en SUPER:: per cridar els comportaments de la classe pare, això no funciona amb els rols. (Els rols no acaben dins de @ISA perquè estan «empegats per dins» enlloc d'«enganxats al damunt»).

Afortunadament, Moose ofereix convenientment la crida after per afegir passes addicionals al final d'una subrutina existent. Moose fa això substituint la subrutina original per una nova subrutina que crida l'orginal i tot seguit crida al codi addicional. El context (llista, escalar, o buit) es conserva de forma adequada, així com també el valor de retorn original. El nostre speak retocat té aquesta pinta:

  package Mouse;
  use Moose;
  with 'Animal';
  sub default_color { 'white' }
  sub sound { 'squeak' }
  after 'speak' => sub {
    print "[but you can barely hear it!]\n";
  };
  no Moose;
  1;

Això ens dóna un ratolí que funciona correctament:

  my $mickey = Mouse->new(name => 'Mickey');
  $mickey->speak;

que dóna com a resultat:

  Mickey goes squeak
  [but you can barely hear it!]

També podem utilitzar before per a precedir el comportament original o around per a controlar com es crida el comportament original, segons calgui. Per exemple, per a permetre que name s'utilitzi com un mètode accessor però que segueixi retornant an unnamed Horse quan s'utilitzi com un mètode de classe, podem posar un «around» del mètode accessor resultant:

  package Animal;
  ...
  has 'name' => (is => 'rw');
  around 'name' => sub {
    my $next = shift;
    my $self = shift;
    blessed $self ? $self->$next(@_) : "an unnamed $self";
  };

El has crear el comportament original. L'around intercepta el nom de la subroutina original, fent que la referència al codi original es passi com a primer paràmetre d'aquesta nova subrutina, que nosaltres capturem a $next. Desencuem el $self original i el verifiquem per veure si és un objecte o no, via blessed (exportat per Moose convenientment). Per a un objecte obtenim el comportament original (un «getter» o «setter»), però per a una classe obtindrem la cadena literal.

Què passa si no li posem un nom al nostre animal? Obtindrem avisos sobre valors indefinits. Podem assignar un nom predeterminat tal com vam fer amb el color predeterminat:

  has 'name' => (
    is => 'rw',
    default => sub { 'an unnamed ' . ref shift },
   );

Novament, voldríem aquell around immediatament després d'aquest pas.

Si no volem que la gent canviï el color després de la creació inicial de la instància, declarem l'atribut no modificable:

  has 'color' => (is => 'ro', default => sub { shift->default_color });

Ara un intent de canviar el color s'avortarà amb Cannot assign a value to a read-only accessor.... Si realment volíem una manera de canviar el color ocasionalment, podem definir a banda un anomenat escriptor:

  has 'color' => (
    is => 'ro',
    writer => 'private_set_color',
    default => sub { shift->default_color },
  );

D'aquesta manera no podem canviar el color d'un ratolí directament:

  my $m = Mouse->new;
  my $color = $m->color; # gets the color
  $m->color('green'); # DIES

Però en canvi podem utilitzar un mètode privat:

  $m->private_set_color('green'); # sets the color to green

Tot utilitzant un nom llarg és menys probable que el cridem accidentalment, a menys que tinguem la intenció de canviar el color.

Anem a crear un RaceHorse afegint «característiques de carrera» a un Horse.

Primer definim «característiques de carrera» com, sí, un altre rol:

  package Racer;
  use Moose::Role;
  has $_ => (is => 'rw', default => 0)
    foreach qw(wins places shows losses);
  no Moose::Role;
  1;

Com que has és només una crida a una subrutina, fixeu-vos que podem utilitzar estructures de control tradicionals de Perl (aquí, un bucle foreach). Amb una mica de codi hem afegit uns altres quatre atributs. El valor inicial 0 significa que no ens caldrà escriure per separat codi d'inicialització al nostre constructor. Tot seguit podem afegir alguns mètodes accessors:

  package Racer;
  ...
  sub won { my $self = shift; $self->wins($self->wins + 1) }
  sub placed { my $self = shift; $self->places($self->places + 1) }
  sub showed { my $self = shift; $self->shows($self->shows + 1) }
  sub lost { my $self = shift; $self->losses($self->losses + 1) }
  sub standings {
    my $self = shift;
    join ", ", map { $self->$_ . " $_" } qw(wins places shows losses);
  }
  ...

Cada crida a won incrementa el número de victòries. Això seria més senzill si assumíssim que aquests objectes s'implementen amb taules de dispersió (per defecte és així), com ara:

  sub won { shift->{wins}++; }

Tanmateix, utilitzant la interfície pública (una crida al mètode) podríem canviar més endavant la implementació amb objectes capgirats o objectes basats en matrius sense trencar aquest codi. Això és especialment important quan es crea un rol genèric, que es podria mesclar amb qualsevol tipus d'objecte.

Per a crear la carrera de cavalls només cal que mesclem un cavall amb un corredor:

  package RaceHorse;
  use Moose;
  extends 'Horse';
  with 'Racer';
  no Moose;
  1;

I ara ja podem cavalcar els ponis:

  use RaceHorse;
  my $s = RaceHorse->new(name => 'Seattle Slew');
  $s->won; $s->won; $s->won; $s->placed; $s->lost; # run some races
  print $s->standings, "\n"; # 3 wins, 1 places, 0 shows, 1 losses

Fins ara només he rascat la superfície del que proporciona Moose. El proper mes miraré algunes de les característiques més avançades de Moose que ajuden a mantenir les coses complexes de manera relativament simple. Fins aleshores, gaudiu!

 


Randal L. Schwartz és un expert de renom sobre el llenguatge de programació Perl (l'essència vital d'Internet), que ha contribuït en una dotzena de llibres supervendes en la matèria i més de 200 articles en revistes. En Schwartz té una empresa de formació i consultoria en Perl (Stonehenge Consulting Services, Inc de Portland, Oregon) i és un conferenciant molt buscat gràcies a la seva destresa damunt l'escenari, que combina coneixements tècnics, pauses dramàtiques i sintonia amb el públic. I és força bon cantant de Karaoke, que guanya concursos sovint.

Podeu contactar en Schwartz per a comentaris a merlyn@stonehenge.com o al telèfon +1 503 777-0095, i accepta preguntes sobre Perl i d'altres temes relacionats.