Erlang: funções, uma implementação alternativa
Conforme prometido, vamos ver uma implementação alternativa da função ler_arquivo/1. Para isso, vamos criar o módulo funcoes2 (“funcoes2.erl”).
-module(funcoes2).-export([ler_arquivo/1]).
ler_arquivo(Filename) -ᐳ
{ok, Fd} = file:open(Filename, [read]),
interpretar_linha(Fd).
interpretar_linha(Fd) -ᐳ
Leitura = file:read_line(Fd),
case Leitura of
{ok, Linha} -ᐳ
[PrimeiroChar|Resto] = Linha,
case PrimeiroChar of
$# -ᐳ pulando;
_ -ᐳ io:format("~s", [Linha])
end,
interpretar_linha(Fd);
eof -ᐳ file:close(Fd)
end.
Aqui a recursão permanece (vide linha 17), formando nosso “loop”, mas o tratamento dos tipos de linha é feito dentro da própria função. Para isso, usamos um case.
Os case de Erlang são tipo os switch-case de C. Dado um determinado valor, ele sai comparando com as opções que você dá. No nosso primeiro case (linha 10), as opções são “{ok, Linha}” e “_”.
Underline?
_
é uma variável especial. Ela significa “qualquer coisa, não me importa”, que chamamos de “a variável livre”.
Ela é útil para não termos variáveis não utilizadas. Na aula anterior, você deve ter reparado que o erl reclamou disso:
funcoes.erl:13: Warning: variable ‘Resto’ is unused
Pois é. O certo era termos trocado Resto por _.
Digamos, por exemplo, que você queira abrir um arquivo com file:open, mas não se importe com o file descriptor/reason. Ora, file:open retorna {Status, Fd}, certo? Você só quer o Status, então usará um casamento de padrões assim:
{Status, _} = file:open(“/etc/fstab”, [read]).
Como esse caminho existe no meu sistema, Status receberá o átomo ok (arquivo aberto com sucesso). O file descriptor eu ignoro.
E, para arrematar, _ é sempre unbound. Experimente:
87ᐳ _ = 10.
10
88ᐳ _ = 100.
100
89ᐳ _ = 1000.
1000
Vírgula e ponto-e-vírgula
Eu esqueci de explicar: as cláusulas de Erlang são terminadas em ponto (“.”), mas são separadas umas das outras, dentro de uma função, por vírgula (“,”) ou ponto-e-vírgula (“;”). Isso é outra herança do Prolog. A vírgula significa “e também”, enquanto o ponto-e-vírgula significa “ou então”.
Repare, na linha 9, em que há o bind de Leitura, que ela termina com vírgula. Isso significa: “execute essa linha e também…” – e vai pra próxima.
Já a linha 14, no meio do case, que termina com ponto-e-vírgula, diz: “se o valor de PrimeiroChar for $#, faça isso, ou então…” – e vai para a próxima opção.
A linha 16, por sua vez, quando termina o case interno (tem um dentro do outro, viu?), termina com vírgula, e significa: “Execute o case e também…” e vai pra linha de baixo, que é chamar novamente interpretar_linha/1.
Elegante ou nem tanto
Vamos rever a implementação anterior:
-module(funcoes).
-export([ler_arquivo/1]).
ler_arquivo(Filename) -ᐳ
{Status, Fd} = file:open(Filename, [read]),
ler_arquivo(Status, Fd).
ler_arquivo(error, Reason) -ᐳ
io:format("Erro '~s' ao abrir o arquivo.", [erlang:atom_to_list(Reason)]);
ler_arquivo(ok, Fd) -ᐳ
interpretar_linha(file:read_line(Fd), Fd).
interpretar_linha({ok, [$#|Resto]}, Fd) -ᐳ
interpretar_linha(file:read_line(Fd), Fd);
interpretar_linha({ok, Linha}, Fd) -ᐳ
io:format("~s", [Linha]),
interpretar_linha(file:read_line(Fd), Fd);
interpretar_linha(eof, Fd) -ᐳ
file:close(Fd).
Eu quero que você veja como a primeira implementação, chamando várias funções, é muito mais elegante que a segunda, que traz de volta um “jeitão” procedural que muitos ainda tem.
Imagine-se tendo que alterar o comportamento do seu módulo na implementação que fizemos hoje. Você tem duas funções e terá que meter a sua mão suja em uma delas, justamente a maior. Você está pondo em risco uns 60% do seu código, no mínimo, e ainda tem que interpretar quase que a função inteira para saber onde deve botar a mão.
Ja na nossa primeira implementação, todo o comportamento está bem dividido nas várias funções. São 6 funções. Se precisar alterar uma delas, você estará lidando, no máximo, em 16% da funcionalidade. Além disso, a maioria das outras funções (especialmente interpretar_linha/2) continua funcionando direito, não importa o quão lesado você esteja enquanto mexe no código.
(Depois de anos como programador, eu desenvolvi a teoria que programadores tem a “mão de Mirdas”: eles cagam em tudo o que tocam.)
Então não devo usar cases?
Opa, não exageremos! Implementar funcionalidade em várias funções costuma ser interessante, mas tem momentos em que um case é lindo. E essa é uma das forças do Erlang: ela não é uma linguagem puramente funcional no sentido estrito da definição de “linguagens funcionais”. Ela é puramente funcional na prática (ou seja: ela funciona!), pois dá liberdade para sairmos um pouco do paradigma quando isso provar-se positivo para todos e for a coisa certa a ser feita.
Um exemplo de bom uso de case é o seguinte:
connection_handle(Socket) -ᐳ
case gen_tcp:recv(Socket, 0) of
{ok, Data} -ᐳ
data_handle(Socket, Data),
%% loop again:
connection_handle(Socket);
{error, closed} -ᐳ
ok
end.
Aqui estou esperando dados de um socket. Ele fica travado no gen_tcp:recv até que apareça, de fato, algum dado. E é melhor tratar dos retornos em um case do que ter mais duas funções, sendo uma delas apenas um tratador do {error, closed}.
Uma das dicas de quando usar case e quando usar mais funções é a seguinte: nos caminhos da execução (os “ramos” do case), você pensa em implementar mais algum comportamento? Por exemplo: dado que, no exemplo acima, o socket tenha sido fechado, eu pretendo jogar isso em um log e avisar o usuário, futuramente? Se sim, é interessante jogar em mais funções (lembre-se dos 16% versus 60%, citados anteriormente). Se não, como é o caso nesse código que usei de exemplo, o case resolve bem a parada.
Resumo
Na próxima aula veremos a mesma implementação feita em Python e compararemos as duas para ver as vantagens e desvantagens de cada uma.
O resumo da aula de hoje é:
- Há várias formas de se implementar algoritmos em Erlang. Procure sempre a solução mais elegante, levando em conta legibilidade, manutenção, prevenção de erros, etc.
- O case é seu amigo. Use-o com sabedoria.
- _ é o /dev/null do Erlang. Mande para lá tudo o que não te interessa.
Este artigo foi originalmente escrito em 03/12/2014.