Tecnologia / Artigos / Erlang /
Mão na massa Erlang: funções, uma implementação alternativa

Cléber

* Date: 2014-12-03 12:34:56 * Modified: 2018-11-30 22:13:45 ----- Conforme prometido, vamos ver uma implementação alternativa da função ler_arquivo/1. Para isso, vamos criar o módulo funcoes2 (“funcoes2.erl”). ```elixir -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: ```elixir {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: ```elixir 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: ```elixir -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: ```elixir 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.

Curti

45 visitantes curtiram esse Item.

Anterior: Erlang: melhor que Python? | Próximo: Dica rápida: spawn e self