Erlang: funções

Eu cá estava pensando em pegar leve e mostrar comparações entre valores, shifts, aritmética, etc. Depois iria ver estruturas de dados. Mas, seria muito, muito chato. É melhor já entrarmos nas funções para que você possa se animar com a linguagem. :)

Mas, primeiro…

Átomos

Pois é, antes de entrarmos no reino das funções, precisamos entender o que é um átomo.

Ao contrário de Python, por exemplo, em que as coisas são, basicamente, valores ou keywords (10, “x”, while), Erlang, sendo um bom filho de Prolog, tem também átomos.

Sobre eles:

Experimente fazer o seguinte: no erl (na linha de comando, digite “erl” para chamar o “shell” do Erlang), escreva o seguinte:

1ᐳ atomo == atomo.true

Em Python, aconteceria o seguinte:

ᐳᐳᐳ atomo == atomo
Traceback (most recent call last):
  File "ᐸstdinᐳ", line 1, in ᐸmoduleᐳ
NameError: name 'atomo' is not defined

Python não tem átomos. Erlang tem. erl permite que você simplesmente defina um átomo chamado “atomo”.

Agora repare no que o operador “==” retornou: true. Este true, per si, é também um átomo.

Quem programa em Ruby já conhece um conceito semelhante, os símbolos. Eles são definidos/usados assim:

:simbolo;

Ao contrário das strings, em que cada string é um objeto distinto na memória, cada átomo é meramente uma representação local de uma mesma coisa, um mesmo elemento.

true e false, em Erlang, são átomos, não keywords. E muitas opções de certas funções são indexadas via átomos. Veja, por exemplo, como abre-se um socket em Erlang:

    {ok, LSocket} = gen_tcp:listen(Port, [list, {packet, 0}, {active, false}, {reuseaddr, true}]).

O equivalente em Python seria:

    retorno, sock = listen(port, return=“list”, packet=0, active=False, reuseaddr=True)

Veja que ok, list, packet, active, false, reuseaddr e true são todos átomos. Elegante, não? Ruby tem uma solução parecida, mas acrescenta o horrível “:” à frente do símbolo – que me desculpem os rubystas que lêem este artigo, mas a quantidade de sinais dentro do código Ruby, a meu ver, é assustadora, tornando o eye scan rápido do código algo meio críptico. Erlang, ao contrário, não acrescenta um identificador ao átomos. O átomo simplesmente está lá e, veja que bom: funciona!

Ah, e lembra dos valores e variáveis, que começam com letras maiúsculas? Átomos começam com letras minúsculas.

Agora, sim, as funções

Primeiramente, e isto é importante ressaltar, saiba que você não pode definir funções dentro do erl. Ponto.

“Mas, mas, mas…”

Pois é. Não pode. O Joel Armstrong, considerado o pai da linguagem, chegou a comentar que é fácil de resolver isso, e implementou no seu “erl2” [https://github.com/joearms/erl2]. Entretanto, faz sentido que não se possa simplesmente declarar uma função do nada: acontece que uma função “plantada” na raiz do módulo no meio da execução é, a bem da verdade, um enorme efeito colateral. Funções de módulos não surgem do nada e nem deixam de existir de um momento para outro!

Por isso, você terá que abrir seu editor de textos favorito. Menos mal que o meu editor favorito é o vim. Se este também é o seu caso, eu recomendo usar o excelente vimerl [https://github.com/oscarh/vimerl]. Ele facilita muito sua vida ao desenvolver.

Que tal se fugirmos dos exemplos-padrão de Erlang e fazermos algo diferente para começarmos a entender funções? Ao invés de implementar o fatorial ou fibonacci, vamos ler as linhas de um arquivo e ignorar as que começam com “#” – que, geralmente, são comentários.

Nosso programa será beeem simples, e não verificará “#” perdidos no meio das linhas. Só cortaremos uma linha fora se ele vier já como primeiro caracter.

Então, para começar, apresento a função file:open. Experimente-a no erl, primeiro:

9ᐳ file:open("/etc/passwd", [read]).
{ok,ᐸ0.44.0ᐳ}
10ᐳ

Você pediu para abrir o “/etc/passwd” (uma string representando o caminho até o arquivo) e usou como opções o átomo read, ou seja, vamos apenas ler do file descriptor. O retorno foi uma tupla.

Uma tupla é uma lista imutável. Dentro da tupla pode ir de tudo, inclusive outras tuplas. Já as listas tem uma série de propriedades diferentes, e explicá-las-ei posteriormente.

O primeiro valor da tupla é um átomo ok, dizendo que, beleza, o arquivo foi aberto e está disponível para lermos. O segundo valor é um PID, ou seja, um identificador de processo. Por hora, você não precisa saber muito mais. Esse PID é como se fosse um id de file descriptor.

O modo como costuma-se abrir arquivos é o seguinte:

{ok, Fd} = file:open(“/etc/passwd”, [read]).

Assim, se o retorno não for ok, o sistema acusa um erro, pois ok da tupla que demos só vai casar com outro ok (átomos casando com átomos iguais, lembra?). Experimente jogar a linha acima no erl, mas com um caminho que não existe (como “/blebs”).

5ᐳ {ok, Fd} = file:open("/blebs", [read]).
** exception error: no match of right hand side value {error,enoent}

E por que isso acontece? Porque o “casamento de padrões” (pattern matching) dá errado. Veja qual é o retorno de file:open com argumento “errado”:

6ᐳ file:open("/blebs", [read]).
{error,enoent}

Ou seja: na prática, estavamos tentando casar duas coisas distintas:

7ᐳ {ok, Fd} = {error,enoent}.
** exception error: no match of right hand side value {error,enoent}

ok não casa com error!

Mas vamos prosseguir.

No primeiro exemplo, vamos lidar bastante com chamadas de funções usando casamento de padrões. Fazer isso é quase como aquele chamado polimorfismo do C++, lembra? Eu defino “f(int x)” e “f(float x)” e a linguagem cuida de chamar a “encarnação” certa da função dependendo do tipo de x. Em Erlang é até parecido. Eu posso definir, por exemplo, “f(ok)” e “f(error)”, e a VM cuida de chamar a “encarnação” correta de “f” dependendo do argumento a ser passado. No caso de eu tentar chamar f(10), por exemplo, Erlang acusará um erro, pois não há função “f” cujos parâmetros casam com “10”.

Criando um módulo

Para podermos trabalhar no erl com nosso arquivo, é conveniente que criemos um módulo.

Em Python, qualquer arquivo .py já é um módulo per si, não sendo necessário nada além de alguns pequenos cuidados no caso de nosso arquivo ser um script (checar se name == “main” antes de sair executando coisas, no caso). Já em Erlang, temos duas diferenças básicas:

E isso é bem simples. Vamos começar o arquivo assim:

-module(funcoes).
-export([ler_arquivo/1]).

Cláusulas que começam com “-” são diretivas de compilação, ou “dicas” para a VM do Erlang. São análogas às cláusulas que começam com “#” em C/C++ (#define. Aqui, meu módulo diz “meu nome é ‘funcoes’” e “eu exporto a função 'ler_arquivo’ com 1 parâmetro”.

Essa notação, “funcao/aridade” é herança do Prolog. “funcao” é o nome da função e “aridade” (arity) é a quantidade de argumentos que ela recebe. Em Erlang você pode criar várias funções com o mesmo nome, mas com parâmetros diferentes e até quantidades diferentes.

Veja na prática, com a continuação do nosso arquivo.

Implementando as funções

Escreveremos a função ler_arquivo, que recebe o endereço do arquivo a ser lido:

ler_arquivo(Filename) -ᐳ
    {Status, Fd} = file:open(Filename, [read]),
    ler_arquivo(Status, Fd).

Existem várias formas de implementar isso. Nessa aula, veremos apenas uma.

Nessa implementação, nós não checamos se Status é um ok ou um error, ou se Fd é mesmo um file descriptor ou uma “reason” (razão de ter dado error). Nós simplesmente tentamos abrir o arquivo em lerarquivo/1 e deixamos que lerarquivo/2 cuide do resto.

“Mas essa função chama a si mesma!!!”

Não. Observe a aridade: lerarquivo/1 chama lerarquivo/2. Na prática, são duas funções diferentes! Vamos ver a implementação de ler_arquivo/2, agora, lembrando o seguinte:

Lá vai:

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).

Repare, agora, no seguinte:

Alguns puristas podem dizer que não são “duas” funções. De fato, seria como uma função só, mas com uma execução condicional dentro dela. Algo como uma função Python com um grande if já no começo. Sem problemas. Considere como quiser, contanto que você entenda como funciona.

Se eu chamar ler_arquivo/2 com ok como primeiro argumento, eu caio na definição de baixo. Se chamar com error, caio na primeira. Se chamar com algo que não seja nem um nem outro, dá erro (o que não é problema, já que não esperamos que file:open retorne algo diferente disso).

Nesse exemplo conhecemos outra função interessante: io:format. Ela é tipo o famoso printf, mas ao invés de “%”, ela usa “~”. Nessa caso, “~s” é “imprima como string”. Também conhecemos file:read_line, que dispensa explicações sobre o que faz e retorna {Status, Linha}, sendo Status ok ou error.

Se tudo der certo, a gente chama a função interpretar_linha/2. Ei-la:

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).

Aqui eu levei o casamento de padrões mais além. E, com isso, veremos uma propriedade interessante das listas.

Listas

Erlang não tem um tipo específico para strings. Strings, em geral, são meramente listas.

E por que isso é assim? Bem, lembre-se que Erlang surgiu na área de telefonia, e manipular strings era coisa muito rara naquele contexto.

Isso é bom, em geral, e às vezes é ruim. Ou somente chato, talvez. Mas, no geral, é um conceito tranquilo com o qual se conviver.

Você pode brincar com listas usando o módulo lists. Experimente fazer o seguinte:

    Lista = [10,20,30,40,50].
    lists:ᐸtabᐳ

ᐸtabᐳ, acima, é a tecla tab, mesmo. Isso faz com que o erl te dê a lista de funções dentro de “lists:”.

As que você vai acabar usando mais são lists:nth e lists:sublist.

69ᐳ Lista = [10,20,30,40,50].
[10,20,30,40,50]
70ᐳ lists:nth(3, Lista).
30

É, pois é. Listas, em Erlang, começam no índice 1.

“Whaaaaaat?!?”

É, ok. Você pode ir embora, se quiser. Muitos não conseguem viver com esse conceito.

Eu sempre lidei com listas começando no índice zero e, sinceramente, acho até bom começar no índice 1. Faz mais sentido semanticamente. Pense em “o elemento 1” e “o elemento 2”. Diga isso a uma criança e ela apontará para o 10 e para o 20 da lista acima. Com “o elemento zero” ela acabará apontando para o lado esquerdo e fora da lista. :)

Cabeça e Cauda

As listas não-vazias sempre casam com o padrão [Cabeça|Cauda] (geralmente chamado [Head|Tail]). Experimente:

71ᐳ [Head|Tail] = Lista.
[10,20,30,40,50]
72ᐳ Head.
10
73ᐳ Tail.
[20,30,40,50]
74ᐳ

A parte antes do “pipe” (“|”) é a cabeça da lista, e a parte depois dele é o “resto”.

Você também pode brincar com mais de um elemento na cabeça:

74ᐳ [H1,H2,H3|Resto] = Lista.
[10,20,30,40,50]
75ᐳ H1.
10
76ᐳ H2.
20
77ᐳ H3.
30

De volta à interpretar_linha/2

Repetindo o código dela:

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).

Vamos começar de baixo para cima: “interpretar_linha(eof, Fd)” trata do caso de ter terminado o arquivo (end of file). Nesse caso, eu serei gentil e fecharei o mesmo com file:close/1.

Agora vamos voltar ao topo. interpretar_linha({ok, [$#|Resto]}, Fd) considera que a leitura da linha retornou {ok, Linha}, mas aqui nós fazemos um casamento de padrão com o que nos interessa: dado que Linha virá como [Cabeça|Resto], eu quero somente as linhas cuja Cabeça é “#”.

E o detalhe: em Erlang, strings são só listas. Listas de inteiros. Por isso eu me refiro a “#” como “o valor inteiro da tabela ascii que representa #”. Notação: $#. Para “o valor da tabela ascii que representa o 'a’ minúsculo” eu uso $a.

$# é 35, e eu poderia ter escrito interpretar_linha({ok, [35|Resto]}, Fd), também. Mas isso deixa a impressão que meu interesse é em um número inteiro, quando, na verdade, o que eu quero é um caracter.

Bem, se esse padrão casar, é sinal que eu tenho uma linha cujo primeiro caracter é “#”. Essas linhas nós queremos eliminar. Logo, eu meramente chamarei interpretar_linha/2 novamente, para continuar o “loop”, ou seja: “ignore essa e interprete a próxima”.

Agora vem interpretar_linha({ok, Linha}, Fd). Aqui eu estou dizendo: “se não casar com a definição acima, que caia nessa aqui, onde o conteúdo da linha não me importa”. Ora, as linhas que começam com “#” já foram tratadas acima. As que não começam com “#” eu trato agora.

O corpo é simples: imprima a linha e vá interpretar a próxima. Assim que todas as linhas forem lidas, chegamos no “eof” e o arquivo é fechado.

Fim.

O arquivo final

-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).

Para executar, salve o arquivo como “funcoes.erl”. Depois, no mesmo diretório, abra o erl e:

c(funcoes).

Isso irá compilar seu módulo e disponibilizá-lo para uso. Agora experimente sua nova função:

funcoes:ler_arquivo(“/etc/fstab”).

Na próxima aula

Veremos, depois, uma implementação alternativa da mesma funcionalidade, mas estruturando o código de maneira diferente. Também compararemos a complexidade desta solução recursiva com uma versão feita em Python.

Resumão


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