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 é:


Este artigo foi originalmente escrito em 03/12/2014.