User Tools

Site Tools


perl:hashantipatterns

Hashes Anti Patterns

Este artigo é uma tradução de um outro escrito por mim para a The Perl Review.

Quando é que que devemos usar hashes?

Um dos primeiros exemplos sobre a necessidade de uso de hashes é quando temos um conjunto de variáveis diferentes para representar propriedades diferentes para determinado objecto. Por exemplo, se queremos descrever duas ligações a base de dados podemos escrever:

  $main_server_addr = "some.server.url";
  $main_server_port = 4000;
  $main_server_username = "me";

  $backup_server_addr = "back.server.url";
  $backup_server_port = 4444;
  $backup_server_username = "me";

Este tipo de código tem dois problemas principais. Primeiro, estou a poluir a tabela de símbolos do Perl. Em segundo lugar, se precisar de passar esta informação para uma função, terei de usar três parâmetros diferentes:

  $connection = make_connection($main_server_addr,
                                $main_server_port,
                                $main_server_username);

No entanto, poderia ter escrito a mesma informação usando duas tabelas de hashing:

  $main_server = { addr => "some.server.url",
                   port => 4000,
                   username => "me" };
  $backup_server = { addr => "back.server.url",
                     port => 4444,
                     username => "me" };

Este código, além de ser bastante mais limpo, define apenas duas variáveis que podem ser facilmente passadas para uma função:

  $connection = make_connection($backup_server);

Podia ter usado directamente uma tabela de hashing em vez de uma referência:

  %main_server = ( addr => "some.server.url",
                   port => 4000,
                   username => "me" );

e chamar a função com

  $connection = make_connection(\%main_server);

No entanto, se eu tiver mais que um servidor de backup talvez tenha melhores resultados utilizando uma tabela de hashing de tabelas de hashing:

  %servers = (
              main => { addr => "some.server.url",
	                  port => 4000,
                        username => "me" },
              backup1 => { addr => "back.server.url",
		             port => 4444,
	             username => "me" },
             )

Outro exemplo do uso de hashes é a implementação de uma estrutura case:

  if    ($x eq 'pdf')  { $type = 'application/pdf' }
  elsif ($x eq 'xml')  { $type = 'text/xml' }
  elsif ($x eq 'html') { $type = 'text/html' }
  ...
  else { $type = 'unknown' }

Imaginem-me agora a escrever todos os tipos existentes. As tabelas de hashing podem ajudar definindo uma associação entre a extensão dos ficheiros e o tipo do documento. Assim, o nosso código torna-se tão simples quanto consultar uma tabela de hashing:

   %type = ( 'pdf' => 'application/pdf',
             'xml' => 'text/xml',
            'html' => 'text/html', );
   
   if (exists($type{$x})) {
      $type = $type{$x}
   } else {
      $type = 'unknown'
   }

Embora tenha escrito este código de forma mais limpa e usando mais linhas que no exemplo anterior, a verdade é que este é bastante mais fácil de manter. Uma estrutura bastante semelhante a esta é a conhecida como dispatch tables. Se em vez de querer apenas o nome do tipo de ficheiro eu quisesse invocar um parser específico, podia usar uma abordagem semelhante:

   %type = ( 'pdf' => \&parse_pdf,
             'xml' => \&parse_xml,
             'html' => \&parse_html, );
  
   if (exists($type{$x})) {
      $type{$x}->($filename)
   } else {
      die "Unknown filetype"
   } 

Técnicas no uso de Hashes

Supondo que temos de contar as palavras de um texto e, para simplificar o exemplo, vamos considerar que o ficheiro de texto tem as palavras separadas por espaços. Uma abordagem inicial para a solução passaria por:

   while(<>) {
     chomp($_);
     my @words = split /\s+/, $_;
     for my $word (@words) {
        if (exists{$hash{$word}}) {
           $hash{$word}++
        } else {
           $hash{$word}=0
        }
     }
  }

Este exemplo esquece-se de que o Perl converte valores indefinidos automaticamente para o valor nulo, e que portanto, não preciso de inicializar os valores da tabela de hashing:

  while(<>) {
     chomp;
     for (split /\s+/) {
        $hash{$_}++
     }
  }

Como exemplo seguinte, vamos considerar dois arrays, ambos do mesmo tamanho, e que queremos criar uma tabela de hashing a associar os primeiros elementos de cada um dos arrays, a associar os segundos elementos, os terceiros, e por aí fora. Um programador pouco experiente escreveria qualquer coisa como:

  my $i = 0;
  for my $key (@first) {
     $hash{$key} = $second[$i];
     $i++;
  }

enquanto podia ter escrito simplesmente:

  @hash{@first} = @second;

Outras vezes, se quero tirar os elementos duplicados de um array, posso utilizar uma tabela de hashing de forma muito semelhante à anterior. Como as tabelas de hash não têm chaves repetidas, se executar:

  @hash{@array} = @array;

fico com os elementos do array como chaves da tabela de hashin, sem valores repetidos. Uma forma mais complicada seria associar a cada elemento do array um valor fixo, por exemplo, o valor 1:

  @hash{@array} = (1) x @array;

Os exemplos seguintes mostram a utilidade das tabelas de hashing para a gestão de conjuntos:

  sub union {
    my ($set1, $set2) = @_;
    my %h;
    @h{@$set1} = @$set1;
    @h{@$set2} = @$set2;
    return [keys %h];
  }
  sub intersection {
    my ($set1, $set2) = @_;
    my %h;
    @h{@$set1} = @$set1;
    return [grep {exists($h{$_})} @$set2];
  }

Erros comuns

Um dos erros mais comuns na utilização de tabelas de hashing é a atribuição do valor undef a uma chave para remover essa chave da tabela.

   $hash{$key} = undef;

Isto não irá apagar a chave, mas mudar o seu valor. Isto significa que a chave continua a existir e que o comando keys continua a retornar a chave em cause. A forma correcta para a remover da hash é usar a função delete:

   delete($hash{$key});

Outro erro comum é o uso de tabelas de hashing aninhados sem o devido cuidado de verificar se as chaves existem. Se executar:

   if ($hash{a}{b}) { ... do something ... }

o que na verdade estou a fazer é:

   $hash{a} = {} unless exists $hash{a};
   if ($hash{a}{b}) { ... do something ... }

Isto significa que da próxima vez que pedir as chaves da tabela de hashing ela irá retornar a chave “a” mesmo que ela nunca tenha sido definida. A isto é chamado auto-vivificação. Se eu não quiser este comportamento (o mais certo), então usaria a função exists para verificar a existência das chaves:

  if (exists($hash{a}{b})) { .. do something .. }

Quando se criam tabelas de hashing com muitas chaves, sempre que executo:

  for (keys %hash) { ... }

o Perl irá criar um novo array com todas as chaves do hash. Se precisar de poupar memória, o mais certo é usar um ciclo while com o comando each.

  while( ($key,$value) = each %hash) {
    ...
  }

O each é um iterador, que guarda informação sobre a posição actual no hash, de forma a que sempre que seja chamado retorne um par diferente até que a tabela de hashing termine. As tabelas de hashing são os tipos de dados mais comuns em Perl, e um dos mais eficazes. Podem ser usados simplesmente como estruturas de dados mas também como ferramentas: remoção de duplicados, criação de conjuntos, contagem de ocorrências… Além disso, também são muito fáceis de usar. Para começar a usar é só preciso lembrar-se da sintaxe correcta para armazenar e consultar valores, e de uma ou duas formas para iterar sobre eles.

Alberto Simões: 2008/07/10 14:17

perl/hashantipatterns.txt · Last modified: 2008/07/10 22:17 by ambs