Rust | Minhas impressões até então

Desde março de 2018, eu vinha vendo uma coisa aqui, outra ali, sobre a linguagem Rust, após ter visto a apresentação do Florian Gilcher na GOTO 2017, intitulada “Why is Rust successful?”, mas nada realmente sério. Me lembro de ter ficado especialmente empolgado com duas talks do Bryan Cantrill, uma na QCon 2018, “Is it Time to Rewrite the Operating System in Rust?”, em junho de 2019, e outra em um meet up, “The Summer of Rust”, alguns dias depois, mas ainda assim, nada de pegar um livro para ler, de rabiscar algum código.

Histórico do YouTube #1 – Primeiro contato com Rust

Na época eu até tinha uma desculpa compreensível: tinha acabado de completar um bacharelado de Nutrição. Sim, isso mesmo. Quatro anos em uma sala de aula há 17km de casa, lendo um tanto de livros, artigos científicos; fazendo trabalhos, estudando para provas, apresentando seminários; estágios de 6 horas diárias em dois hospitais, uma clínica de nutrição esportiva e uma escola de educação infantil; e ainda o fadigante TCC sobre a relação entre nutrição e depressão. Tudo isso enquanto ajudava a construir a Pricefy do zero.

Dá para imaginar que o tema do TCC veio bem a calhar.

Histórico do YouTube #2 – Talks que por um momento me empolgaram

Mas então virou o ano, chegou 2020, a fadiga mental diminuiu significantemente e resolvi gastar algum tempo com Rust, estudar com um pouco mais de dedicação, rabiscar uns programas, experimentar por mim mesmo e não ficar somente no que vejo da experiência dos outros.

Esse post é para registrar um pouco das minhas impressões até aqui.

Rust, a linguagem

Não quero, aqui, dar uma introdução à linguagem, porque já existe uma documentação oficial maravilhosa, muito material educativo disponível gratuitamente, pois isso seria um tanto redundante.

O que é importante se ter em mente, a princípio, é que Rust foi criada com o objetivo de ser uma linguagem de sistema, para ser usada em casos de uso onde normalmente se usaria C/C++, como: drivers, sistemas embarcados, microcontroladores, bancos de dados, sistemas operacionais; programas que vivem extremamente próximos ao hardware, que requerem alta performance, com baixo consumo de memória e overhead de execução próximo de zero.

Portanto, algumas decisões de design foram:

– Ser compilada para binários nativos;
– Ter um sistema de tipos estático, forte e extensível;
– Não ter coletor de lixo;
– Ter um sistema seguro de gestão de memória;
– Ser imutável por padrão;
– Dar suporte a concorrência imune a data races e race conditions;
– Ter checagem de uso de memória em tempo de compilação;
– Permitir código “não seguro”, quando explicitamente desejado;
– Oferecer tratamento de erro simples, mas robusto;
– Ter um ótimo ferramental de desenvolvimento.

Dentre outras coisas. Esta não é uma lista exaustiva. Mas é o suficiente para contextualizar o que vou falar sobre minhas impressões.

Em poucas palavras, eu diria que o objetivo principal era que ela fosse uma linguagem de baixo nível, extremamente performática, porém absolutamente segura e produtiva.

Vamos então à minhas impressões.

O Compilador

Eu fiquei realmente pirado no compilador. Não, é sério. Tendo gastado boa parte dos últimos anos programando em C#, JavaScript, Go e Python, acho que não preciso dizer muito mais.

Mas vamos ver um exemplo:

O que nos diria o compilador sobre este programinha?

Hmm? E você, o que me diz?

Ao longo do post vão aparecer mais exemplos legais da atuação do compilador, portanto não vou me prolongar aqui.

Imutável por natureza

Variáveis são sempre imutáveis, a menos que explicitamente dito que não, como no caso que vimos há pouco.

Isso favorece o desenvolvimento de código concorrente seguro, o que há muito tem sido um dos principais atrativos de linguagens funcionais – ou melhor dizendo, do paradigma funcional de programação.

Não há porque temer o compartilhamento de valores que não mudam; aliás, que não podem ser modificados. Nenhuma linha de execução vai crashear esperando que a seja "Hello", quando na verdade, agora, a é "Oi".

Possessiva, porém generosa

Agora, espere. O que aconteceria se seguíssemos a sugestão do compilador e tornássemos a variável a mutável?

O efeito colateral seria observado. A variável a poderia ter seu valor modificado e os prints refletiriam isso.

Primeiro porque ela teria sido explicitamente anotada como mutável. Justo. E depois, porque a macro println! faz parte de uma família de casos específicos de macros, em que o parâmetro é implicitamente tomado por referência (a.k.a. borrowing), por questão de conforto, praticidade, mas não causam efeitos colaterais neles.

Okay. Isso coloca em cheque o que vimos no tópico anterior, não? Nhmm… não tão depressa.

Vamos modificar um pouco o exemplo anterior e ver o que aconteceria em uma função que recebe uma variável não por referência, como é o caso da macro println!, mas por transferência de posse (a.k.a. ownership).

O compilador logo chia, dizendo que se está tentando emprestar o valor supostamente possuído pela variável a, para poder modificá-lo, enquanto este, na verdade, teve sua posse transferida para a função awesomely_crazy. Ou seja, o que quer que awesomely_crazy faça com o que recebeu, a variável a não tem mais nada a ver com isso.

O que acontece é que, em Rust, como você já deve ter percebido, um valor só pode ser possuído por uma única variável por vez; e quando o escopo em que esta está contida termina, seu valor é destruído. No entanto, essa posse pode ser cedida a outro.

Quem garante essa coisa de ownership, borrowing e lifetime em tempo de compilação é o chamado borrow checker, que muitas vezes se recusa a compilar um programa que você tem “certeza” que está tudo certo.

No nosso caso, somente a variável a era dona do valor "Hello" até ter transferido sua posse para a função awesomely_crazy. A partir de então, a função awesomely_crazy (nominalmente o parâmetro s) é quem passou a ser sua única proprietária; e ao final de sua execução, ao término de seu escopo, esse valor será destruído. É por isso que ele não pode ser emprestado novamente para modificação, através de a.push_str, ou mesmo emprestada para println!, que sequer modifica alguma coisa.

Portanto, se quisessemos fazer esse código compilar, teríamos que modificar a implementação da função awesomely_crazy, de modo que ela passasse a tomar o valor da variável a emprestado, por referência (&C/C++ feelings, anybody else?), e não por posse.

Não haveria qualquer problema.

Mas note que awesomely_crazy toma o valor de a emprestado, por referência, mas não pode modificá-lo, como é o caso em outras linguagens. Se quiséssemos permitir que awesomely_crazy modifique o valor possuído pela variável a, teríamos que fazer um desencorajador malabarismo de mut, que provavelmente nos faria pensar um pouco mais no algoritmo que estamos tentando escrever.

Eu sei que tudo isso pode parecer complicado (e na prática é mesmo; tente implementar uma estrutura de dados recursiva, por exemplo), mas essas características da linguagem:

– Imutabilidade por padrão;
– Mutabilidade por decisão explícita;
– Posse exclusiva de valor;
– Empréstimo de valor com restrições.

Com regras rigidamente observadas pelo compilador, são super interessantes na hora de escrever programas que rodam continuamente, por tempo indeterminado, sem crashear depois de devorar toda a memória disponível, por causa leaks; ou então, programas com processos concorrentes, que não crasheiam por conta de data races e race conditions.

Essa é a maneira de Rust possibilitar um runtime de alta performace, seguro para processos concorrentes, que não correm o risco de lidar com dangling pointers, data races, e ainda livres do overhead de um coletor de lixo para garantir isso.

Confesso que volta e meia ainda passo perrengue com isso e tenho que repensar meu código, mas isso tem acontecido cada vez menos e tenho gostado cada vez mais. O que realmente me deixa puto são certos casos de inferências, que penso pqp, como é que ele não consegue saber em tempo de compilação o quanto essa p@%$# vai consumir de memória.

Anyway. De qualquer forma, não existe null pointer em Rust e isso por si só já me deixa feliz.

Sintaxe, bizarra sintaxe

Olha quem está falando: alguém que gastou um tanto de horas de sua vida programando em Erlang. Okay. Não tenho muito o que reclamar.

Mas a real é que esse título é mais clickbait do que verídico.

A verdade é que eu gosto da sintaxe de Rust. Sempre fui fã de linguagens com sintaxe C-like, tipo: Java, JavaScript, C#, Scala, Go, e outras. Mas em todas essas linguagens sempre tem alguma coisa que acho chata, irritante ou bizarra. Às vezes as três.

No caso de Rust, o que acho bizarro é a anotação de escopo para ajudar na validação de tempo de vida de referências à memória. Como vimos anteriormente, não há coletor de lixo, então toda referência tem um tempo de vida baseado no escopo em que esta está contida. Terminado o escopo, essa referência é destruída e sua memória liberada. A maior parte do tempo, o compilador consegue inferir isso sem ajuda, mas às vezes, você precisa dar uma mãozinha, usando o chamado generic lifetime parameter, para garantir que em tempo de execução as dadas referências serão de fato válidas.

De novo, não quero aqui, neste curto espaço, me atrever a explicar algo que já está muito bem explicado gratuitamente e online, mas vamos ver um exemplo disso.

Side note – Olha o compilador aí, dando aquela ajuda Google-like

O código abaixo implementa um if-ternário sem sucesso, porque há uma ambiguidade sobre que referência será retornada.

O compilador, naturalmente, não gosta disso, diz o motivo e sugere uma solução.

Implementada a solução sugerida; ou seja, anotado o escopo de vida do que a função recebe e do que retorna, para que a ambiguidade seja eliminada.

Vòila! O código compila e, a menos que tenha um erro de lógica, roda perfeitamente como esperado.

Tudo bem. Foi só uma anotaçãozinha. Mas isso porque também foi só uma funçãozinha. Imagine algo com escopo de vida um pouco mais completo, que precise de mais de uma anotação de lifetime e ainda outras anotações de tipos genéricos.

É, a coisa pode escalar bem rápido. Generics é um recurso fantástico, sem sombra de dúvidas, mas imagine isso na assinatura de uma função, que também tem outros parâmetros, e retorno, e… Pff!

Mas por outro lado, o lado bom, agora, é que há um recurso robusto de definição de tipos. Em lugar de escrever algo assim:

Você pode definir um tipo que defina essa especificação gigantesca, fazendo bom uso de generics e tudo mais; e inclusive, dar um nome para o que ela representa. Só espero que você seja melhor do que eu com nomes.

Vê? Com isso é possível tornar o código bem mais semântico, comunicar mais significado, porque no final das contas, você passa mais tempo lendo código do que escrevendo ou apagando código.

Side note – Outra vez, o compilador amigão

Pattern Matching e tratamento de erros

Quem me conhece e já trocou ideias de programação comigo, sabe que sou bem fã de pattern matching. Essa é uma das coisas que mais gosto em Erlang e é também uma das que mais gosto em Rust.

Lindo, não? Okay. Eu sei que estou exagerando um pouco.

Mas a questão é que este recurso, além de favorecer que se escreva código mais declarativo e com viés mais funcional, também encoraja o tratamento de erros mais simples, menos complicado.

Em Rust, a forma idiomática de tratamento de erros é que o retorno de uma função seja uma enum Result, que pode conter o resultado do sucesso ou o famigerado erro.

Este é um exemplo que peguei da própria documentação da enum Result. Ele dispensa explicação, tão simples e declarativo que é.

Uma coisa interessante para se mencionar aqui, que tem tudo a ver com pattern matching e tratamento de erros, é que enums em Rust estão muito próximas do que em linguagens funcionais chamamos de tipos algébricos, o que favorece muito a expressividade do código.

Okay. Mas voltando à Result e ao tratamento de erros, expandindo um pouco no exemplo acima, retirado da própria documentação, perceba que há uma série de funções bacanas, incluindo map e or_else.

De novo, essa é a maneira idiomática de se lidar com erros em Rust. Sim, existe uma macro panic!, muito parecida com o que há em Go, mas use com moderação.

Aliás, falando em Go, eu tenho que dizer que gosto bastante de Go e prova disso é que desde 2012 tenho estudado e feito Go aqui e ali, quando faz sentido. Dito isso, eu acho a forma idiomática de tratamento de erros super simples e compreensível, porém tosca e pobre. Nada sofisticada. Mas tudo bem, este não é o ponto da linguagem.

Recursos funcionais

Rust não é uma linguagem funcional. Rust não foi desenvolvida para ser uma linguagem funcional. Mas tendo sido significantemente influenciada por programação funcional, Rust oferece muitas ferramentas para que programadores experientes em programação funcional escrevam código com conformação funcional.

– Imutabilidade por padrão;
– Iterators;
– Clousures;
– Block expressions;
– Pattern matching;
– Function composition;
– Tuplas;
– Enumerações.

São todos recursos disponíveis na linguagem, que possibilitam escrever código expressivo, com estilo funcional; e com um detalhe importante: em geral, uma das reclamações que se faz de linguagens funcionais (e não quero aqui discutir isso) é a questão de se preterir performance de execução em função do rigor conceitual do código, mas este não é o caso de Rust, que tem massiva influência da filosofia C++ de custo zero de abstração.

Tudo ao mesmo tempo agora

Concorrência em Rust, na minha visão, não é tão natural como é em Erlang ou Go, mas isso é ok. Por outro lado, Rust oferece mais de uma ferramenta para se implementar concorrência:

– Message passing;
– Shared-state concurrency;
– Futures.

Este é um exemplo simples de message passing que peguei emprestado de Klabnik & Nichols. Bem semelhante ao que se tem em Go, por exemplo, porém um tanto mais verboso.

Aliás, um parênteses aqui: às vezes acho Rust um pouco verbosa de mais. Fecha parênteses.

Este modelo de troca de mensagens é um que me agrada bastante e que estou bem familiarizado. Em muitas situações tendo a pensar primeiro neste modelo antes de considerar outro, porque ele favorece o desacoplamento.

Abaixo, o modelo de compartilhamento de estado entre threads usando Mutex.

Sinceramente, não gosto muito deste modelo. Mas programação não é sempre sobre o que se gosta, mas sobre o que se precisa fazer para ir da maneira mais segura e eficiente possível do ponto A ao ponto B. Então, quando é necessário usar o bom e velho lock, cá está ele à disposição. Implementação de estruturas de dados thread safe, semáforos de acesso a recursos, são exemplos de uso.

Já este exemplo abaixo é de concorrência com futures, usando a relativamente nova implementação de async/await, que também segue a filosofia de abstração com custo zero. Acho que este modelo dispensa qualquer introdução, por ter se popularizado tanto nos últimos anos, desde que foi implementado em F# e C# e mais recentemente em JavaScript.

Pensando bem, nos últimos anos, escrevendo um monte de C# e Node.js quase diariamente (e ocasionalmente algum Python com Async IO/ASGI nos finais de semana) este é o modelo que mais uso. É simples de ler código com async/await, fácil de entender, de explicar, não tem tempo ruim.

É bom ter mais de uma ferramenta à disposição e ser capaz de implementar mais de um modelo de concorrência, para então escolher o que melhor atende à tarefa em questão; e isso não é exclusivo, um ou outro. Em um sistema, pode haver uma combinação desses modelos que vimos. Aliás, esse é o mais provável.

A propósito, se você ainda se confunde um pouco com concorrência vs paralelismo, recomendo 2 minutos de leitura aqui.

Bem, apesar da minha reclamação sobre a verbosidade do message passing de Rust, no final do dia, a conta ainda fica positiva.

Odeio redefinição de variável

Uma coisa que odeio com todas as minhas forças é shadowing de variável.

Pqp, que p%#*@ é essa?!?!?!

Tá. Eu sei que shadowing não é o puro mal encarnado, tem lá sua razão de ser, blá, blá, blá…

I’m done.

Conclusão

Não existe bala de prata e isso você já deveria saber. Também não existe a linguagem perfeita e própria para todas as situações. O que existe são ferramentas em uma caixa; e o que se espera de você é que você saiba escolher a ferramenta certa, para o trabalho certo, na hora certa.

Eu tenho gostado bastante de Rust até então. Tenho tropeçado em alguns pontos aqui e ali, odiado uma coisa ou outra, mas no geral, estou muito satisfeito.

Até aqui.

Entendendo um tiquinho de self()

Uma confusão bem comum quando se começa a escrever programas concorrentes em Erlang é quanto ao uso da BIF (built-in function) self(). Mais especificamente, quanto ao seu retorno.

A BIF self() é analoga ao this do Java, por exemplo, que é capaz de responder quem é o objeto contenedor do método atualmente em execução. Semelhantemente, em Erlang, self() é capaz de dizer quem é o processo contenedor da função atualmente em execução avaliação. Assim, se você chamar self() no Erlang shell, você vai receber como retorno o Pid (identificador de processo) do próprio Erlang shell.

Faça um teste no seu Erlang shell:

1> self().

Você deve ter recebido algo semelhante a <0.31.0> com retorno. Isto porque o Erlang shell nada mais é do que um processo Erlang com um comportamento REPL.

Ok. Agora, o que acontece se você tiver um programa com um único módulo em que há duas funções que trocam mensagens entre dois processos? Qual seria o retorno de self() nestas duas funções?

Um pequeno exemplo

Vejamos um exemplo bem simples deste caso extraído do livro Erlang Programming:

-module(add_two).
-export([start/0, request/1, loop/0]).

start() ->
  process_flag(trap_exit, true),
  Pid = spawn_link(add_two, loop, []),
  register(add_two, Pid),
  {ok, Pid}.

request(Int) ->
  add_two ! {request, self(), Int},
  receive
    {result, Result}       -> Result;
    {'EXIT', _Pid, Reason} -> {error, Reason}
    after 1000             -> timeout
  end.

loop() ->
  receive
    {request, Pid, Msg} ->
       Pid ! {result, Msg + 2}
  end,
  loop().

De maneira bem objetiva, o que este código faz é o seguinte:

1- Quando um processo já existente — que no nosso caso será o próprio Erlang shell — faz uma chamada à função start(), um novo processo é gerado, tendo como ponto de partida a função loop(), o seu identificador é associado à variável Pid e, por fim, ele recebe o apelido add_two.

2- Todas as vezes que a função request(Int) é chamada, uma mensagem é enviada para o processo add_two, para que este some 2 ao número passado como parâmetro e envie o resultado de volta ao processo solicitante.

3- Sempre que o processo add_two recebe uma nova mensagem, esta é capturada na sentença receive ... end da função loop(), que verifica se é um “pedido de soma”, e então, envia o resultado da soma ao processo solicitante, identificado por Pid.

A confusão

Bem simples mesmo, certo? Então, por que acontece a tal confusão?

A confusão acontece, porque o retorno de self() não é o mesmo em todas as funções deste módulo. Isto porque a função loop(), apesar de estar contida no mesmo módulo que as funções start() e request(Int), não está rodando sendo avaliada no mesmo processo que elas estão. A função loop() está sendo avaliada no processo add_two, enquanto que start() e request() estão sendo avaliadas no primeiro processo — o Erlang shell. Assim, self() em loop() retorna um identificador de processo diferente do que retornaria as demais funções.

Quer tirar a prova?

Mais um simples exemplo, só que desta vez, esclarecedor!

Eu adicionei um bocado de “prints” no código que apresentei anteriormente e se você executá-lo agora, terá a prova do que foi discutido. (Tá tudo bem, você não precisa de uma prova a estas alturas do campeonato, mas vai ser divertido.)

-module(add_two).
-export([start/0, request/1, loop/0]).

start() ->
io:format(": start -> self() = ~w~n", [self()]),
  process_flag(trap_exit, true),
  Pid = spawn_link(add_two, loop, []),
  io:format(": start -> Pid  = ~w~n", [Pid]),
  register(add_two, Pid),
  {ok, Pid}.

request(Int) ->
  io:format(": request -> self() = ~w~n", [self()]),
  add_two ! {request, self(), Int},
  receive
    {result, Result}       -> Result;
    {'EXIT', _Pid, Reason} -> {error, Reason}
    after 1000             -> timeout
  end.

loop() ->
  receive
    {request, Pid, Msg} ->
       io:format(": loop/receive -> self() = ~w~n", [self()]),
       io:format(": loop/receive -> Pid    = ~w~n", [Pid]),
       Pid ! {result, Msg + 2}
  end,
  loop().

Após fazer a devida compilação, faz o seguinte teste no Erlang shell:

1- Veja o identificador de processo (aka Pid) do Erlang shell:

1> self().

2- Inicie o processo add_two:

2> add_two:start().

3- Chame a função request(Int):

3> add_two:request(10).

Executou? Comparou os Pids? Viu a diferença de escopo entre as três funções? Pois muito bem, então fica aqui a lição:

“Módulos servem para agrupar funções com um mesmo escopo conceitual, mas nem sempre com o mesmo escopo de processo”

Você deveria considerar Erlang para seu próximo grande projeto

Ao ler o título deste post, talvez você esteja se perguntando: por que eu deveria considerar Erlang para meu próximo grande projeto? Bem, meu objetivo com este post é te apresentar alguns importantes motivos para fazer isto.

Erlang nasceu no laboratório de ciência da computação da Ericsson na década de 1980, influenciada por linguagens como ML, Ada, Module, Prolog e Smalltalk, quando um time de três cientistas — entre eles, o grande Joe Armstrong receberam a missão de descobrir qual seria a melhor linguagem de programação para escrever a próxima geração de sistemas de telecom. Após dois anos de investigação e prototipação, estes cientistas descobriram que nenhuma linguagem era completa o bastante para tal objetivo e, conclusivamente, decidiram criar uma nova linguagem. Nasceu então Erlang, the Ericsson Language.

De lá pra cá, Erlang tem sido evoluida e usada para escrever grandes sistemas críticos, porque é exatamente nesse cenário que Erlang faz mais sentido. Se seu projeto é construir um sistema crítico, altamente tolerante a falhas, com disponibilidade 24×7 e veloz como o papa-léguas, sim, Erlang é para você. Mas se não é bem esta sua necessidade, se você quer apenas construir um pequeno sistema, com baixa concorrência, poucos usuários, pouca necessidade de performance e possibilidade de passar horas down em manutenção, não, você não precisa de Erlang. Que tal Basic?

Diferente de algumas linguagens que nascem para encontrar um nicho, Erlang nasceu com um problema a ser resolvido, com requisitos a serem atendidos. Tolerância a falhas, concorrência realmente pesada, computação distribuída, atualização da aplicação sem derrubá-la, sistemas de tempo real, este é o nicho de Erlang; foi para isto que Erlang nasceu.

Quem usa Erlang atualmente?

Além da Ericsson, é lógico, há algumas outras grandes empresas e projetos usando Erlang, como por exemplo:

Facebook, no backend de seu sistema de chat, lidando com 100 milhõs de usuários ativos;
Delicious, que tem mais de 5 milhões de usuários e mais de 150 milhões de bookmarks;
Amazon SimpleDB, o serviço de dados do seu poderoso EC2;
Motorola, CouchDB, RabbitMQ, Ejabbed, etc.

Ok, mas Erlang é propriedade da Ericsson?

Não, felizmente, não. Em 1998 a Ericsson tornou Erlang open source sob a licença EPL.

Quer mais uma boa notícia? Você não precisa de um servidor de aplicações para rodar sua aplicação Erlang, nem mesmo uma pesada IDE para escrevê-las. Tudo o que você precisa é de uma distribuição de Erlang para sua plataforma, seja Mac OS X, Linux, Windows, Solaris, ou qualquer outro sistema Unix-like, que traz consigo máquina virtual, compilador e vasta bibliotéca de módulos muito úteis — além do banco de dados Mnesia; e um editor de textos de sua preferência TextMate, por exemplo, tem um ótimo bundle para Erlang.

Algumas características de Erlang

1- Expressividade e beleza

Há quem diga que não, mas Erlang é uma linguagem muito bonita — ao menos pra mim. Dado as já citadas influências de Erlang, ela é uma linguagem bem expressiva e declarativa, que encoraja a escrita de código simples e objetivo, o que certamente torna seu código fácil de ler, de escrever e de organizar [em módulos reutilizáveis]. O snippet abaixo é um exemplo de implementação do famoso fatorial:

-module(sample1).
-export([fac/1]).

fac(0) -&gt; 1;
fac(N) -&gt; N * fac(N-1).

2- Forte uso de recursividade

Isto certamente é uma herança da veia funcional de Erlang que torna o código muito menos imperativo e mais declarativo. Mas além da beleza declarativa óbvia, é importante saber que Erlang foi projetada para lidar com iterações recursivas gigantescas sem qualquer degradação — e sem estourar o “heap” de memória.

3- Livre de efeito colateral

Uma das grandes capacidades dadas por Erlang é a construção de sistemas altamente concorrentes rodando em processadores com multiplos núcleos. Isto não combina nada com efeito colateral, por isso, em Erlang, uma vez que um dado valor tenha sido atribuído a uma variável, esta não poderá mais ser modificada — ou seja, estão mais para o que conhecemos por constantes do que para o que conhecemos por variaveis.

Se você já escreveu código concorrênte sabe o quanto tê-lo livre de efeitos colaterais te faz livre de dores de cabeça. Poucas coisas são tão deprimentes quanto debugar código concorrênte repleto de efeitos colaterais.

4- Pattern matching

Pattern matching em Erlang é usado para associar valores a variáveis, controlar fluxo de execução de programs, extrair valores de estruturas de dados compostas e lidar com argumentos de funções. Mais um ponto para código declarativo e expressividade. Vejamos o código abaixo:

-module(sample2).
-export([convert_length/1]).

convert_length(Length) -&gt;
    case Length of
        {centimeter, X} -&gt;
            {inch, X / 2.54};
        {inch, Y} -&gt;
            {centimeter, Y * 2.54}
    end.

Fala por si, não?

5- Concorrência baseada em passagem de mensagens (a.k.a. Actors)

Acho que concorrência baseada em passagem de mensagem entre atores é uma das features mais populares de Erlang. Vejamos o porque com o famoso exemplo do Ping-Pong:

-module(sample3).

-export([start/0, ping/2, pong/0]).

ping(0, Pong_PID) -&gt;
    Pong_PID ! finished,
    io:format(&quot;Ping finished~n&quot;, []);

ping(N, Pong_PID) -&gt;
    Pong_PID ! {ping, self()},
    receive
        pong -&gt;
            io:format(&quot;Ping received pong~n&quot;, [])
    end,
    ping(N - 1, Pong_PID).

pong() -&gt;
    receive
        finished -&gt;
            io:format(&quot;Pong finished~n&quot;, []);
        {ping, Ping_PID} -&gt;
            io:format(&quot;Pong received ping~n&quot;, []),
            Ping_PID ! pong,
            pong()
    end.

start() -&gt;
    Pong_PID = spawn(sample3, pong, []),
    spawn(sample3, ping, [3, Pong_PID]).

Neste pequeno snippet podemos observar algumas características de Erlang já citadas neste post, tal como pattern matching na captura das mensagens e recursividade no controle das iterações.

Agora, falando do aspecto concorrente em sim, algumas coisas são particularmente interessantes aqui:

a. Em Erlang, a concorrência acontece entre processos leves, diferente de linguagens como C++ e Java, que baseiam sua concorrência em threads nativas de sistema operacional [caríssimas];
b. Em Erlang, há um tipo de dado chamado PID, o qual é o identificador do processo paralelo (mais conhecido como Actor) e para o qual as mensagens podem ser enviadas.

Releia o código acima com estas informações em mente e veja como concorrência em Erlang é algo completamente descomplicado e natural. Depois, pense no mesmo código implementado em C#, Java ou C++.

Gostei de Erlang, há algum livro para eu começar a estudar?

Sim, há dois livros excepcionais. Um, do próprio criador da linguagem, Joe Armstrong:

E outro de Francesco Cesarini e Simon Thompson:

Além disso, há o próprio material on line que é muito bom e barato. 🙂

Conclusão

Erlang possui muitas outras características e informações bem interessantes, mas que me falta espaço neste post para citar e apresentar, senão ele ficará absurdamente gigantesco para o meu gosto. Mas acredito que pelo que já apresentei até aqui, você já tenha motivos suficientes para pensar em Erlang com carinho e conciderá-la para seu próximo grande projeto.

Em breve devo escrever algo sobre OTP, já que neste post apresentei características mais inerentes à linguagem em si e nem tanto sobre a plataforma.

Até mais!