Muitas pessoas vêm ao Clojure de uma linguagem imperativa e se encontram fora de seu elemento quando se vêem em frente à abordagem do Clojure sobre como fazer as coisas, enquanto outros vêm de um background mais funcional e pressupõem que, uma vez que eles deixarem o subconjunto funcional do Clojure, serão confrontados pela mesma história: estado como encontrado no Java. Este artigo tem o objetivo de iluminar a abordagem do Clojure aos problemas enfrentados pelos programas imperativos e funcionais no mundo do modelamentos.
Programação imperativa
Um programa imperativo manipula seu próprio mundo (por exemplo, a memória) diretamente. Ele é fundado a partir de uma premissa insustentável de threads únicas – que o mundo para enquanto você olha para ele e o modifica. Você diz “faça isso” e acontece, “muda aquilo”, e ele muda. Linguagens de programação imperativas são orientadas em volta dos comandos “faça isso”, ou “faça aquilo”, e da mudança dos locais de memória.
Isso nunca foi uma ótima memória, mesmo antes do multithreading. Adicione a simultaneidade e você tem um problema real, porque a premissa de que “o mundo para” simplesmente não é mais verdade, e para recriar esta ilusão é bastante difícil e propenso a erros. Múltiplos participantes, cada um deles age como se fossem onipotentes, e devem, de alguma maneira, evitar destruir as presunções e afetar os outros. Isto requer mutexes e locks para isolar áreas para cada participante manipular, e muitas despesas para propagar as mudanças para a memória compartilhada, para que elas possam ser visualizadas por outros núcleos. Só que isso não funciona muito bem.
Programação funcional
A programação funcional tem a abordagem mais matemática do mundo, e vê os programas como funções que pegam certos valores e produzem outros. Programas funcionais evitam os “efeitos” externos dos programas imperativos, e assim se tornam mais fáceis de entender, raciocinar sobre e testar, uma vez que a atividade das funções seja completamente local. Assim, a porção de um programa é puramente funcional e a simultaneidade não é um problema, uma vez que não existe nenhuma mudança para ser coordenada.
Modelos de trabalho e identidade
Enquanto alguns problemas são meramente grandes funções, por exemplo, os compiladores ou provadores de teoremas, muitos outros não são – eles são mais como modelos de trabalho, e como tais precisam suportar o que eu me refiro nesse discussão como identidade. Por identidade, eu me refiro a uma entidade estável e lógica associada com uma série de valores diferentes ao longo do tempo. Modelos precisam de identidade pelos mesmos motivos que os humanos precisam de identidade – para representar o mundo. Como poderia funcionar se identidades como “hoje”, ou “América”, tivessem que representar um único valor constante por todo o tempo? Note que por identidades não quero dizer nomes (eu chamo minha mãe de mamãe, mas você não).
Portanto, para esta discussão, uma identidade é uma entidade que tem um estado, que é seu valor em algum ponto no tempo. E um valor é algo que não muda. 42 não muda. 29 de Junho de 2008 não muda. Pontos não movem, datas não mudam, não interessa o que algumas bibliotecas mal encaradas podem fazer você acreditar. Mesmo os agregados são valores. O conjunto das minhas comidas favoritas não muda, por exemplo. Se eu preferir comidas diferentes no futuro, este conjunto será diferente.
Identidades são ferramentas mentais que usamos para sobrepor a continuidade em um mundo que está, constantemente e funcionalmente, criando novos valores de si mesmo.
Programação orientada para o objeto (OO)
OO é, entre outras coisas, uma tentativa de promover ferramentas para modelar a identidade e o estado em programas (bem como o associando comportamento com estado e classificação hierárquica, ambos ignoradas aqui). OO tipicamente unifica a identidade e o estado, por exemplo, um objeto (identidade), é um indicador para a memória que contém o valor do seu estado. Não tem como obter o estado independente da identidade a não ser copiando. Não há outra maneira de observar um estado estável (ou até copiá-lo) sem bloquear outros que possam mudá-lo. Não tem como associar o estado da identidade com um valor diferente que não seja no local de mutação da memória. Em outras palavras, a típica OO tem a programação imperativa dentro dela! OO não tem que ser dessa maneira, mas normalmente, ela é (Java/C++/Python/Ruby etc).
As pessoas estão acostumadas com a concepção OO dos seus programas como valores mutantes de objetos. Eles compreendem a verdadeira noção de uma valor, digamos 42, como algo que nunca irá mudar, mas normalmente não estendem esta noção de valor para o estado de seu objeto. Esta é uma falha da sua linguagem de programação. Essas linguagens usam os mesmos construtores para valores modeladores que usam para identidades, objetos e padrão para mutabilidade, impedindo que os programadores mais disciplinados criem muito mais identidades do que deveriam, ou criando identidades de coisas que deveriam ser valores.
Programando em Clojure
Existe uma outra maneira, que é para separar a identidade do estado (mais uma vez, as vias indiretas salvam o dia na programação). Precisamos nos afastar da noção de estado como “o conteúdo deste bloco de memória” e nos aproximarmos de uma que diz que “o valor atualmente associado com esta identidade”. Assim, uma identidade por estar em estados diferentes e em tempos diferentes, mas o estado não muda. Isso é, uma identidade não é um estado, mas uma identidade tem um estado. Exatamente um estado em um ponto qualquer no tempo, e este estado é um valor verdadeiro, que nunca muda. Se uma identidade parecer mudar, é porque ela se torna associada com valores de estado diferentes ao longo do tempo. Este é o modelo Clojure.
No modelo Clojure, o cálculo do valor é puramente funcional. Os valores nunca mudam e novos valores são funções de outros já velho, não mutações. Mas a identidade lógica é bem suportada, via referências atômicas para valores (Refs e Agents). Mudanças nas referências são controladas/ coordenadas pelo sistema -por exemplo, a cooperação não é opcional e não é manual. O mundo segue em frente devido aos esforços cooperativos dos seus participantes e do sistema/ linguagem de programação, Clojure, está no comando do gerenciamento da consistência do mundo. O valor como referência (estado de uma identidade) é sempre observável sem coordenação, e livremente compartilhado entre threads.
Vale a pena construir programas dessa maneira, mesmo quando existe apenas um participante (thread). Programas são mais fáceis de compreender quando o cálculo do valor funcional é independente da associação da identidade/ valor. E é fácil adicionar outros participantes quando eles são (inevitavelmente) necessários).
Concorrência
Lidar com a concorrência significa desistir da ilusão da onipotência. Um programa deve reconhecer que irão existir outros participantes, e o mundo irá continuar mudando. Portanto um programa deve compreender que se ele observar os valores dos estados de algumas identidades, o melhor que ele pode conseguir é um snapshot, uma vez que ele pode subseqüentemente adquirir novos estados. Mas muitas vezes isto é bom o suficiente para a tomada de decisão, ou relatórios. Nós, humanos, lidamos muito bem com os snapshots fornecidos pelos nossos sistemas sensoriais. O lado bom é que tal valor do estado não irá mudar durante o processo, pois é imutável.
Por outro lado, mudar o estado para um novo valor necessita acesso ao valor “atual” e à identidade. Os Refs e Agents do Clojure fazem isso automaticamente. No caso dos Refs, qualquer interação que você faz deve ocorrer dentro de uma transação (ou o Clojure irá lançar uma exceção), todas essas interações irão ver uma visualização constante do mundo como um ponto no tempo, e nenhuma mudança irá prosseguir, a não ser que os estados a serem modificados não o tenham sido pelos outros participantes neste intervalo. As transações suportam mudanças síncronas para Refs múltiplos, agentes, OTOH e oferecem mudanças assíncronas a uma referência única. Você passa uma função e os valores e, em algum ponto no futuro, essa função será passada para o estado atual do Agente e o valor retornado da função irá se tornar o novo estado do Agente.
Em todos os casos, o programa irá ver visualizações estáveis dos valores em todo o mundo – uma vez que esses valores não podem mudar – e compartilhar eles entre os núcleos é simples. O truque é: “valores nunca mudam”. Isso significa que gerar novos valores, a partir de valores antigos, deve ser eficiente e no Clojure, devido às suas estruturas persistentes de dados. Elas permitem que você finalmente siga o conselho frequentemente oferecido em favor da imutabilidade. Portanto, você configura o estado da sua identidade para um novo estado ao ler seu valor atual, chama uma função pura sob aquele valor, para criar um novo valor, e configura aquele valor de acordo com o novo estado. Estas operações complexas são facilitadas pelas funções alter, commute e send.
Passando mensagens e atores
Existem outras maneiras de modelar a identidade e o estado. Uma das mais populares é o modelo de passagem de mensagens e um modelo ator, melhor exemplificado pelo impressionante Erlang. Em um modelo ator, o estado é encapsulado em um ator (identidade) e somente pode ser afetado/ visto através da passagem de mensagens (valores). Em um sistema assíncrono, como o Erlang, ler algum aspecto de um estado do ator requer o envio de uma mensagem de pedido, a espera pela resposta, e o envio da resposta pelo ator. É importante compreender que o modelo do ator foi criado para resolver problemas de programas distribuídos. E esses problemas são muito mais difíceis – existem palavras múltiplas (espaços de endereços), a observação direta não é possível, a interação ocorre em canais não muito confiáveis, etc. A localização real dos outros atores, permite que o sistema se espalhe em múltiplos processos/ máquinas sem mudanças no código.
Eu escolho não utilizar o modelo do ator do Erlang para o mesmo processo de gerenciamento de estado no Clojure por vários motivos:
- É um modelo de programação muito mais complexo, que necessita de duas conversas de mensagens. Para as leituras de dados mais simples, e força o bloqueio do recebimento de mensagens, o que introduz o potencial para dreadlock. Programar para os modos de falhas de distribuição significa utilizar timeouts, etc. Isso causa uma bifurcação dos protocolos de programas, alguns dos quais são representados por funções e outros por valores de mensagens;
- Ele não deixa que você utilize completamente as eficiêncuas de estar nos mesmos processos. É possível compartilhar diretamente de forma eficiente uma grande estrutura de dados imutáveis entre os threads, mas o modelo do ator força a intervenção nas conversas e, potencialmente, a cópia. A produção de leituras e escritas são em série e bloqueiam umas as outras, etc;
- Ele reduz sua flexibilidade em modelar – este é um mundo em que todos estão em um mundo sem janelas e se comunicam somente por email. Os programas são decompostos como pilhas de declarações bloqueadas. Você somente pode lidar com mensagens que você antecipa que irá receber. Coordenar atividades envolvendo atores múltiplos é muito difícil. Você não consegue observar nada sem sua cooperação/ coordenação – é o que faz os relatórios de ad-hoc, ou as análises muito impossíveis, ao invés de forçar todo ator a participar em cada protocolo;
- Muitas vezes é o caso de pegar algo que funciona bem localmente, mas que ao distribuí-lo, ele não funciona – a granularidade da conversa é muito tagarela, ou as cargas são muito pesadas, ou os modos de falhas mudam o particionamento do trabalho otimizado. Por exemplo, a distribuição transparente não é transparente, e o código tem que mudar de qualquer maneira.
O Clojure pode eventualmente suportar o modelo do ator para a programação distribuída, pagando o preço quando a destruição for necessária. Mas eu acredito que seja bastante pesado para a programação que apresenta os mesmos processos. YMMV, claro.
***
Para saber mais sobre Clojure: http://clojure.org/
***
Texto original disponível em: http://clojure.org/state








