Hashes Anti Patterns
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.
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.
Comment