Tag Archive | java

Monads

Faz algum tempo desde a última vez que escrevo sobre algum padrão de projeto. O último texto que lembro foi o texto sobre CRTP, e nesse tempo meu conhecimento sobre programação aumentou, minhas habilidades comunicativas aumentaram e eu passei a escrever textos melhores. O texto que fiz sobre CRTP, nem acho importante. Entretanto, decidi fazer um texto para explicar monads, pois existe toda essa tradição de que, ao compreender o que é monad, você atinge uma epifania satisfatoriamente envolvente, você sente um desejo incontrolável de tentar compartilhar esse conhecimento com toda a humanidade, e você FALHA, mas, até pior que isso, seu manual é diferente e as pessoas vão perder até mais tempo tentando aprender o que é monad, pois agora há até mais textos confusos que acumulamos.

Eu, é claro, demorei muito tempo para aprender monads, apesar de estar usando monads há bastante tempo. Quando finalmente aprendi o que é monad, percebi o quão simples é esse conceito. Devo essa compreensão ao Douglas Crockford.

Introdução

All told, a monad in X is just a monoid in the category of endofunctors of X, with product × replaced by composition of endofunctors and unit set by the identity endofunctor.

Entendeu? Eu também não, e esse tipo de explicação era uma das razões para eu ter demorado a aprender.

Uma coisa importante para prosseguir com a explicação, é deixar claro os termos utilizados. E o primeiro termo que pretendo deixar claro é o de valor. Em programação funcional pura, não é tão raro assim evitar o uso do termo variável e usar, no lugar, o termo associar (binding em inglês), para indicar que um nome está se referindo a algum valor. Quando você se refere a variável, talvez implicitamente você assuma que você pode mudar o valor dessa variável, mas nem sempre é essa a mensagem que queremos comunicar. Há também o termo objeto, do paradigma de programação orientada a objetos, que podemos usar no lugar do termo variável, mas o termo objeto também carrega  outras informações que podem não ser necessárias para a explicação que estamos tentando passar. Por essas razões, eu vou, durante o texto, tentar usar o termo valor, mas se você substituir esse termo por objeto ou variável, ainda é provável que o texto continue fazendo sentido.

Há também o termo função, que em programação pode acabar tendo um significado diferente. Em programação, há o termo subrotina, que não equivale ao termo de função na matemática. Entretanto, algumas linguagens de programação usam o termo função para se referir ao conceito de subrotina, criando uma situação de falso cognato. Para “diminuir” a confusão, passamos a chamar de funções puras, as funções que possuíam o mesmo significado que as funções possuem em matemática. Essa curiosidade não é tão importante para o entendimento de monads, mas é bom que você comece a consumir algumas informações relacionadas para estar estimulado o suficiente quando a explicação de monads aparecer.

Ainda no tópico de funções, temos a diferença de funções membros e funções não-membros. Funções membros são funções que recebem o this/self, que fazem parte de alguma classe e não são métodos estáticos, funções que chamamos de métodos na programação orientada a objetos. Funções não-membros são funções livres, que não fazem parte de nenhuma classe. Quando eu me referir a uma função que recebe a e b como argumentos, as duas situações exemplificadas no seguinte código são interpretações válidas:

Nos dois casos, temos uma função foo que recebe a e b como argumentos. Há até esforços para tornar a sintaxe de chamada de funções em C++ mais uniforme. A linguagem Rust está ligeiramente à frente nessa corrida, enquanto outras linguagens já chegaram lá.

Você pode enxergar monad como um padrão de projeto, e, dentro desse padrão de projeto, há o “objeto” que de fato representa o monad. Assim como ocorre no padrão Singleton, onde o mesmo termo, Singleton, se refere (1) ao padrão de projetos e a (1) um objeto que respeita algumas características. Estou citando o padrão Singleton, porque esse é, pela minha experiência, o padrão mais famoso e mais fácil de entender. A parte importante para manter em mente é que monad é um valor, mas, em alguns momentos, pode ser que eu me refira a monad como um padrão de projeto.

Valores embrulhados

Em alguns cantos na terra da programação, podemos encontrar valores que são embrulhados. Por exemplo, temos a classe Integer, na linguagem Java, que embrulha um valor primitivo int. Temos também, em C++, a classe auto_ptr<T>, que embrulha um valor do tipo T. Instâncias dessas classes não são monads.

Valores embrulhados por outro valor são isolados, tornando-se acessíveis somente a partir de uma interface. Eles não podem ser acessados diretamente, pois estão embrulhados em outro valor, seja lá qual for o propósito. No caso da classe Integer, você precisa usar a função intValue para acessar o valor embrulhado. No caso da classe auto_ptr<T>, você precisa usar a função que sobrecarrega o operador de desreferenciamento da classe.

Monads também embrulham valores. Monads são valores que embrulham outros valores e apresentam três propriedades. Entretanto, eu só vou informá-las ao final do texto.

Função unit e função bind

Unit é uma função que recebe como argumento o valor a ser embrulhado e retorna o monad. É também conhecido como construtor em outros locais. Seria o “construtor” do monad.

Bind é uma função que recebe dois argumentos, um monad e uma função que tenha a “mesma assinatura” de unit, e retorna um novo monad. Usei a expressão “mesma assinatura” entre aspas, pois, na verdade, a situação não precisa ser tão estrita assim. O tipo de retorno pode ser um monad diferente, mas o argumento de entrada precisa ser do mesmo tipo ou compatível.

Em um monad, deve existir uma forma de acessar o valor embrulhado através da função bind, mesmo que essa não seja a única forma de acessar o valor, e mesmo que a função receba outro nome que não seja bind. Daí que encontramos algumas discussões usando termos como “monadic operations“.

A ideia por trás dessa estrutura é a componibilidade. Talvez não seja tão incrível para eu ou você, pois estamos acostumados a ter outras ferramentas disponíveis, mas parece ser um conceito incrível para os programadores da comunidade de programação funcional pura.

Em Haskell, monads costumam ser usados para isolar comportamentos que não são livres de efeitos colaterais, como I/O. Rust, uma linguagem que possui “;”, não precisa de monads, mas o núcleo de seu sistema da tratamentos de erros é feito em cima das ideias de monads.

Alguns monads

Maybe

Maybe é um monad, em alguns locais recebendo o nome de Option ou optional, e às vezes nem sequer obedecendo as propriedades de um monad. O propósito de Maybe é simples: possivelmente armazenar um valor. Você pode encarar esse monad como uma abstração que lhe permite expressar a semântica de valores opcionais, que em C são representados através de ponteiros que possivelmente possuem o valor NULL, ou em Java, onde qualquer objeto pode ser null.

O legal desse monad é que você ganha segurança, pois verificar pela existência do valor para então acessá-lo não acontece. É mais seguro, porque, no modelo antigo, era possível você tentar acessar o valor esquecendo de verificar antes se ele existia, esquecer do if. Com o padrão de monad/Maybe, você só usa a função bind que já recebe o valor “extraído” e ela só é chamada se o valor for diferente de None.

A verdade é que esse monad fica bem mais legal se houver suporte a nível de linguagem para exhaustive pattern matching, como ocorre em Rust. Em Rust, a abstração que equivale a esse monad é a abstração Option.

Na verdade, o exemplo de código anterior usa um bind “não-conformante”, pois a função/closure passada como argumento não retorna outro monad, mas escrevi mesmo assim para ser breve. Pattern matching costuma ser mais interessante para acessar o valor e você abstrai outros comportamentos que são mais interessantes para serem encapsulados como “operações monádicas” (a documentação da abstração Option em Rust é um ótimo exemplo).

Result

Se Maybe é a abstração para valor-ou-nada, Result é a abstração para valor-ou-erro. Suponha uma função que converta uma string para um inteiro de 32 bits. Erros podem acontecer e pode ser que você queira usar o Maybe<int> como retorno da função, mas talvez seja interessante diferenciar o porquê da função ter falhado, se foi devido a uma string inválida ou overflow, por exemplo.

Result como monad é bem interessante, porque lhe permite encadear uma cadeia de ações e, caso alguma delas retorne erro, a cadeia para, e o erro é armazenado, sem que você precise explicitamente verificar por sua existência. Na linguagem Rust, até existe uma macro, try!, que torna o código até mais legível a agradável.

É interessante ter operações monádicas em Result que só atuam em um dos valores (o esperado ou o valor de erro), então você teria Result<T, E>::map e Result<T, E>::map_err.

Future

Futures e promises se referem a um padrão na computação para abordar o problema de concorrência e dependência de dados. Nesse padrão, o future é um valor que embrulha o resultado, que é computado assincronamente. Alguma função como get pode ser usada para obter o resultado e, caso não esteja pronto, a thread atual é bloqueada até que ele esteja disponível.

A adição de operações monádicas permite definir uma API que anexe continuações que seriam executadas quando o resultado estiver pronto, livrando-lhe do problema de bloquear a thread atual (que essencialmente mata o paralelismo). C++ é uma linguagem que atualmente possui abstrações de futures e promises, mas não possui as operações monádicas que tornariam essa abstração muito mais agradável de ser utilizada, como é explicado em um bom texto do Bartosz Milewski.

Programadores de NodeJS querem usar futures para se livrar de códigos profundamente aninhados (também conhecido como nesting hell). Futures são monads que podem livrar o programador da verificação explícita de erros para cada operação. Se bind for uma função-membro, o aninhamento infernal é, de fato, eliminado. O benefício é um ganho na legibilidade que pode ter grandes consequências como produtividade aumentada e diminuição de erros.

As três propriedades de um monad

A terceira propriedade existe para garantir ordem. E isso é importante para composibilidade.

Conclusão

Utilidade. A utilidade de você entender monads é que agora, quando os programadores de Haskell invadirem sua comunidade de programação, você vai entender o que eles querem dizer por monad. A utilidade se resume a comunicação, mas eu não recomendo que você use o termo monad para documentar abstrações que você crie que modelem o conceito de monad, pois é inútil. Dizer que uma abstração modela o conceito de monad não vai me fazer querer usar tal abstração. Demonstrar a utilidade da abstração vai me fazer querer usá-la e omitir o termo monad não vai tornar a demonstração mais longa ou menos clara. Só use o termo monad para questão de comunicação, quando você estiver tentando se comunicar com programadores de Haskell.

Tratamento de erros usando exceções sob a perspectiva de um programador de C++

Um tópico sobre o qual sua compreensão pode evoluir muito com o tempo, pois pouca experiência tem uma grande contribuição para impedir que os conceitos se tornem claros é o tópico de tratamento de erros usando exceções. Em C++, esse é um assunto até mais polêmico que em muitas outras linguagens e eu resolvi compartilhar um pouco de minha compreensão nesse texto.

Eu quero que você se esforce muito para não pensar nas implicações de performance que exceções introduzem no sistema, pelo menos até terminarmos de analisar as outras facetas dessa ideia, pois há muitos argumentos irracionais, que, na minha experiência, vêm principalmente de pessoas que estão falsamente preocupadas com performance. Se esforce para manter em mente as frases “um erro é uma exceção?” e “o que é um caso excepcional?”. Elas vão ajudar a entender motivações e serão discutidas mais adiante no texto.

Exceções – resumo/revisão rápida

Exceções formam um conceito de programação idealizado para tornar difícil o ato de ignorar erros, separar código do fluxo normal do programa do código de tratamento de erros — aumentando a legibilidade, diminuindo erros e melhorando a manutenção do sistema — e transportar tanta informação sobre o erro quanto possível.

A motivação para exceções terem sido idealizadas é inspiradora, principalmente quando se pensa em estilos de notificações de erros alternativos que eram/são comuns, como variáveis globais (errno), valores de retorno onde alguns valores possíveis são reservados para indicar erro (normalmente -1), valores de retorno representando um código de erro, valores de retorno indicando sucesso ou erro e outros. Algumas dessas técnicas alternativas poluem abstrações, como no caso onde o valor de retorno da função torna-se reservado para o erro e a função precisa ser refatorada para enviar o resultado através dos argumentos passados para ela.

Algumas das técnicas alternativas evoluíram para soluções que chegaram a adquirir o meu selo de “minimamente respeitável”, e agora nós temos outras soluções para se levar a sério, como, por exemplo, verificações forçadas estaticamente através do sistema de tipos (veja o exemplo de Rust). Mas, esse texto é sobre exceções, então, a partir de agora, evitarei menções desnecessárias a essas abordagens alternativas.

Uma proposta de “expected” para C++ sugere quatro características importantes para um sistema de tratamento de erros:

  • Visibilidade do erro.
  • Informação nos erros.
  • Código limpo.
  • Erros não intrusivos.

Eu recomendo a leitura da parte inicial da proposta para entender essas quatro características. Elas ajudam a julgar o que é um bom sistema de tratamento de erros, ressaltando indicativos importantes.

Isso é introdução mais que o suficiente para se começar a descrever o que são e como funcionam exceções. Exceções definem construções para o controle de fluxo de execução do programa onde separamos código “normal” de código “excepcional”. E existe a exceção, usada para representar e transportar informações sobre um erro. Quando um erro é detectado e a exceção correspondente é gerada, o fluxo de execução normal é abortado e o fluxo de execução excepcional é iniciado, recebendo a exceção lançada para que se possa fazer a decisão de tratamento de erro apropriada.

Sintaticamente, envolvemos o fluxo de código normal para o qual somos capazes de tratar alguma exceção usando um bloco “try” e, logo em seguida, introduzimos um bloco “catch”, que contém o código para o fluxo excepcional. Um exemplo é dado abaixo:

Bem simples e, para muitos novatos, epifanicamente elegante.

Onde exceções decepcionam

O principal problema de usabilidade com exceções, tal como é implementado na linguagem C++, é que você nunca está certo de que está tratando todas as exceções possíveis. Você chama uma função, verifica a assinatura dela, e não sabe quais possíveis exceções serão lançadas. E se, após uma atualização, a função passar a lançar um novo tipo de exceção, todo o código que a chama vai continuar compilando normalmente, sem nem mesmo um aviso sobre a necessidade de atualizar o código chamador, mas causará um erro em tempo de execução nos piores momentos — que são os momentos quando o programador está confiante que o sistema está pronto para produção.

Em Java, há uma funcionalidade chamada de exceções verificadas, em que as possíveis exceções a serem lançadas por uma função fazem parte de sua assinatura. Ou seja, o sistema de exceções é integrado ao sistema de tipos para aumentar a segurança do sistema. Com tal funcionalidade, se, para uma dada função, não há a declaração de que uma exceção X é possivelmente lançada, e essa possibilidade existe no corpo da função, um erro de compilação é gerado. Ou você trata todas as exceções que podem ser lançadas dentro da própria função, ou informa ao código chamador que é possível que algumas exceções sejam lançadas.

Há quem considere catch(…) com rethrow um devido tratamento de exceção, mas a exceção não está sendo tratada. Você está meramente ignorando toda a informação sobre o erro e transferindo a responsabilidade de tratá-la para algum outro lugar. Esse padrão é útil, no entanto, para fornecer garantias de exceções básicas e fortes, dois conceitos presentes na STL.

Não é possível deferir o tratamento de exceções, situação que já podemos encontrar em casos apenas um pouco mais distantes que os imaginados e idealizados. Em programação concorrente (assíncrona ou paralela), o ponto onde uma operação é iniciada e onde é completada podem ser diferentes. Tais pontos sendo diferentes, a exceção não é retornada para o código que requisitou a realização da operação, e você simplesmente usa outro paradigma para tratamento de erros. É até mais complicado quando se envolve threads — problema para o qual o padrão C++11 adicionou exception_ptr. Um dos paradigmas, o paradigma de futures & promises, restaura o suporte a exceções nesses casos, fazendo com que a operação de desembrulhar o valor, através da função-mebro get, possivelmente lance uma exceção, mas, para funcionar, esse truque se apoia no fato de que as exceções em C++ não são verificadas.

Um ponto final sobre a complexidade que exceções introduzem está relacionada a código de finalização, que sempre precisa ser chamado, independente de uma exceção ter sido lançada ou não. Se a linguagem não suporta RAII, adiciona blocos “finally” para resolver o problema. Se a linguagem suporta RAII, um conjunto de convenções e regras é criado e a possibilidade de se lançar uma exceção enquanto outra já está sendo lançada nasce, o que normalmente provoca o programa a ser terminado.

Basicamente, exceções podem se tornar o novo goto e, elas são até abusadas, para, por exemplo, se implementar sistema de pattern matching em C++.

Performance

Finalmente, o tópico que causa polêmica e que desencoraja o uso de exceções em qualquer caso que não seja excepcional, mas cuja preocupação ajuda a definir o que é um caso excepcional.

A primeira observação a se fazer sobre exceções é que elas não são lentas. Citando outro texto de minha própria autoria:

O documento TR18015 detalha duas abordagens principais para a implementação de suporte a exceções e o que o documento menciona como “abordagem tabela” é muito barata, realizando boa parte do que precisa ser computado em tempo de compilação e estruturando o código de forma que não há penalidade em tempo de execução para o fluxo de execução normal do programa. O custo, nessa abordagem, é que o programa fica maior, mas a parte extra pode ser movida para não interferir negativamente no uso de cache e, em sistemas com suporte a memória virtual, essa parte pode até ficar na swap até que torne-se necessária.

uma resposta no StackOverflow, caso você não queira se dar ao trabalho de passar pelo TR18015. A implementação do suporte a exceções é bem elegante e a discussão deveria deixar de ser se exceções são lentas e passar a ser quando usar exceções.

E cuidado com os benchmarks injustos que alimentem esse mito, assim como o P. alerta.

Então usar exceções pode conduzir a um código até mais rápido do que manualmente verificar se erros ocorreram, mas caso um erro aconteça, haverá uma drástica redução na performance. E, como exceções dependem do mecanismo de RTTI, pode ser que tratar erros usando exceções deixe de ser uma opção em sistemas de tempo real.

A solução para esse problema está em colocar uma linha divisória entre erros e exceções. Ou, mais objetivamente, o que é um caso excepcional. Se você usar exceções apenas em casos excepcionais, não há problema nenhum em utilizá-las devido a alguma degradação de performance, pois você atingiu um caso excepcional de qualquer forma.

Um exemplo de um caso excepcional é quando o seu sistema encontra um erro de lógica, pois continuar executando o programa pode causar corrupção do seu estado interno e ele deveria ser encerrado o mais cedo possível, parando somente para mostrar uma mensagem de erro ao usuário.

Outro exemplo de caso excepcional é um cliente de um jogo online, onde, caso a conexão caia, você não se importa com o tempo gasto para lançar a exceção, pois o sistema não pode fazer nada de útil enquanto o erro não for tratado, e reconectar ao servidor é uma operação que vai custar mais tempo que o tratamento da exceção, de qualquer forma. Esse mesmo exemplo de “queda de conexão”, entretanto, pode virar um caso não excepcional em um contexto diferente. No contexto de um servidor que está servindo milhares de clientes, por exemplo, quedas de conexões são esperadas o tempo todo e você não quer que o gargalo de lançar milhares de exceções diminua a capacidade do seu servidor.

O usuário do reddit @sazzer ilustra mais alguns casos do que é uma situação excepcional.

Conclusão

Responder a pergunta “esse é um caso excepcional?” é uma tarefa complicada, mas, pode se tornar mais fácil, fazendo uso de bom senso, das ferramentas certas e de um bom entendimento de suas implicações.

Além disso, um mesmo erro pode ser excepcional ou não a depender do contexto, então é preferível que se evite a dependência em exceções.

Aliado a todos os problemas que exceções trazem, eu sugiro que seu uso, em C++, seja reduzido. Entretanto, você simplesmente não pode se livrar de exceções usando C++ idiomático, então compreendê-las é importante.

Um sistema de tratamento de erros unificado é atraente, pois elimina a chance de escolhermos a abordagem errada em algum ponto. Entretanto, um sistema de tratamento de erros unificado deveria ter todas as vantagens que exceções possuem e se livrar de seus problemas mais sérios, inclusive os problemas de performance que impossibilita seu uso em sistemas de tempo real.

Pessoalmente, o sistema de tratamento de erros que eu vejo como ideal é a abordagem que é adotada em Rust, que emprega uso de conceitos como algebraic data types e pattern matching para oferecer um sistema de tratamento de erros bem expressivo e que é difícil de usar erroneamente.

UPDATE:

Acabei encontrando um texto interessante sobre a luta de um usuário para diminuir o tamanho da imagem do programa quando exceções são habilitadas.

Why does /prog/ hate Java?

What can happen in an anonymous  discussion board about programming languages? I found this ancient post made in 4chan. Enjoy it.

Why does /prog/ hate Java?
11 Name: Anonymous : 2007-06-13 04:47 ID:8/uv1if0

My god, I’m >>2 and you’ve all mentioned retarded/irrelevant/wrong stuff.

I don’t fucking care it’s interpreted. I don’t fucking care people calls some languages “scripting languages” and thinks worse of them because of a name (what do you think these people are for labelling, then judging by labels?). I don’t fucking care it’s slower than C/Asm (any language other than C/Asm is slower than C/Asm, that’s the point: a contract trading execution speed for development speed and fun, which saves lots of money because developers are more expensive than hardware).

Java’s problems and the reason why I said it sucks are:

1. Lame ass object system shoved up your ass because the language doesn’t properly support any programming paradigm: not good OO (not even decent), not good good old imperative (objects up ass), functional programming not (really) possible, others absolutely impossible. You can’t model your application properly when the language designers are forcing stupid rules on you. Lacks multiple inheritance. Interfaces are a stupid hack to make the lack of multiple inheritance look less bad.

2. Yet built-in types are not objects. Way to go. This introduces wrapper classes (which are pure shit) and difficults heterogeneous collections.

3. Static typing with all sorts of anal rules that don’t really reduce development or debugging time, don’t really make stupid programmers write reliable code, and really pisses some smart programmers off.

4. Lacks builtin lists or dictionaries.

5. Lacks many advanced features you’d ask of a modern language which attempts to be productive: first class functions, first class classes, anonymous functions (now it has anonymous classes, not the same, objects up ass, etc.), static nested scoping (closures), mixins, introspection, dynamically created and modified functions, classes, and inheritance, …

6. Can’t override operators. If you think implementing a method “add” is less potentially dangerous than implementing an perator or method “+”, you’re pretty fucking stupid.

7. The standard library is a stinking fucking piece of shit, the worst ever created by man. It’s pure blOOat, making you create three instances of three different classes just to open a fucking file! And have a look at the two failed attempts at dates and the second one’s class hierarchy! Factory classes are pretty fucking stupid! sin, cos, tan are not methods! “a”.equals(“b”) is pretty fucking ugly (and a result of not being able to overload operators)! It’s all full of stupid shit and insanity, it’s hyperstructured, it’s overgeneralized, it’s ENTERPRISE.

8. It encourages terrible programmers and terrible ways which they call “best practices”, “enterprise-grade software” and “code patterns”. There’s a whole universe of wankery over the worst pieces of shit ever designed by man. Best practices considered harmful.

9. In practice, it’s terribly unproductive. Anything written in pretty much any other high-level language, from Lisp to Python, from PHP to Perl, from OCaml to Haskell is 5-10 times shorter, 5-10 times faster to write, 5-10 times easier to maintain, and 5-10 times more flexible.

10. Its world is full of businessspeak, pointy-haired managers (all worthless), and retards who went for a 20 hour Java “programming” course and think they can code. It has a terrible community, I’m starting to think even PHP “designers” are better. And every site talking about Java doesn’t say (or know) shit, and is all about “scalable enterprise-grade Web 2.0 XML-based AJAX-based professional n-tier business solutions”, “discovering business logic”, “lowering the Total Cost of Ownership”, “optimizing cash flows”, “converting visitors into customers”, “maximizing profits”, and “best-practices”.

Gerenciamento de recursos em linguagens de programação

É época de carnaval e provavelmente muito menos pessoas irão ler esse texto, mas, ainda… eu irei digitar, pois há essa necessidade estranha que eu tenho de me agarrar a essa pequena impressão de que o tempo está me tornando melhor, de alguma forma…

Pois bem, gerenciar recursos é um dos maiores desafios presentes em linguagens de programação e, dois indícios fortes que posso citar para sustentar esse argumento fraco é a existência do Buffer Overflow, um dos tipos de ataques mais explorados na história da computação que torna-se possível devido ao mau gerenciamento de memória, e a quantidade de recursos que investimos em ferramentas como a Valgrind.

Pretendo, nesse post, demonstrar o problema, apresentar conceitos simples e, logo após, analisar duas formas de gerenciamento de memória bem comuns, RAIIGarbage Collection.

E no princípio, tínhamos ponteiros…

Um ambiente familiar cria conforto para algumas pessoas, incluindo a mim e, sendo assim, começarei essa parte do texto com algo que me é familiar, um código-fonte:

Os programas que criamos gerenciam recursos e, no princípio, sequer existiam abstrações para procedimentos. O que dizer sobre abstrações para gerenciamento de recursos, então? O código anterior exemplifica o problema de lidar com recursos.

No exemplo, mesmo havendo apenas dois recursos a se gerenciar, torna-se evidente que esse modelo de programação torna o código mais complexo, dificulta a leitura e nos distancia do problema que queremos resolver, nos forçando a pensar em como os recursos se comportam e sendo fácil cometer erros como esquecer de liberar algum recurso ou liberá-los na ordem errada.

Sempre que inicializarmos um novo recurso, devemos verificar se a inicialização falhou, caso em que devemos liberar os recursos que haviam sido adquiridos antes de usar alguma construção da linguagem para quebrar o fluxo de instruções. É importante também analisar cuidadosamente o fluxo de instruções e detectar em quais momentos qualquer um dos recursos pode tornar-se inacessível, ponto em que devemos inserir mais código, redundante, de limpeza. Por último, e não menos importante, é uma boa prática liberar os recursos na ordem contrária em que eles foram adquiridos, para evitar referências inválidas, caso no qual um recurso referencia um outro recurso que já foi liberado.

Como eu chamaria toda essa bagunça, confusão, sujeira, desordem, porcaria, trapalhada, barafunda, balbúrdia, embaraço… que esse modelo de programação está criando? Código espaguete?

Formalizando…

Há o gerenciamento errôneo de um recurso quando:

  • Um recurso é destruído antes de ser usado. Nesse caso referências inválidas para o recurso irão existir e o uso dessas referências pode criar um comportamento imprevisível cuja origem será difícil de rastrear ou o encerramento da aplicação.
  • O recurso não é destruído após ser utilizado. Nesse caso chamamos o erro de vazamento de recurso e isso impede que poder computacional seja reciclado para outra tarefa.

Podemos evitar o primeiro problema deixando de fazer a limpeza de recursos manualmente, mas com as técnicas que (não) discutimos, essa decisão implica o segundo problema.

Há três definições diferentes que podemos usar para nos referir a vazamento de recursos.

A definição do usuário

Do ponto de vista do usuário:

Um recurso vazado é qualquer recurso que não pode ser possivelmente utilizado para qualquer propósito útil.

A definição poderia ser simplificada para “qualquer recurso que eu, como usuário, não estou interessado”, mas essa definição incluiria o uso de cache, por exemplo, o que seria inutilizável na nossa pesquisa.

A definição do desenvolvedor

Do ponto de vista do desenvolvedor:

Um recurso vazado é qualquer recurso que não é alcançável.

Um recurso alcançável é qualquer recurso para o qual exista uma referência, também alcançável, que você possa utilizar para acessá-lo. Deve-se notar que essa é uma definição recursiva e, quando o recurso observado é memória, podemos quebrar essa recursão afirmando que qualquer objeto alocado na stack é alcançável.

Essa é uma definição bem mais formal e, como consequência, é mais fácil criar uma ferramenta para detectar vazamentos de recursos quando utilizamos essa definição.

Entretanto, ela é menos abrangente que a definição do ponto de vista de usuário e um exemplo simples que, segundo essa definição, não apresenta nenhum vazamento de recursos, mas, de acordo com a definição do ponto de vista de usuário, possui um vazamento de recurso, segue:

No exemplo anterior, a variável buffer contém referência a um recurso que não é mais útil quando o fluxo de repetição presente é quebrado, mas, ainda assim, o recurso é alcançável. Essa situação exemplifica a diferença entre as duas definições.

É garantido que um objeto inalcançável não será usado novamente, mas o contrário não é verdade. Algumas pessoas sugerem que essa definição seja uma forma de “aproximação”.

A definição do depurador

Do ponto de vista do depurador:

Um recurso vazado é um recurso que não foi destruído durante o encerramento da aplicação.

Essa é uma definição muito usada em programas de detecção de vazamentos de memória, como Valgrind ou Visual Studio.

Uma situação onde essa definição se distingue da definição do ponto de vista do usuário é a mesma situação demonstrada anteriormente.

Outra situação onde ela se distingue é quando um recurso é utilizado até o final da aplicação, mas não é destruído. Caso o recurso em questão seja memória, isso não costuma ser um problema, pois sistemas operacionais modernos irão lidar com essa situação. Além disso, o objetivo do programa é solucionar um problema do usuário e, assim, a definição do usuário deveria ter precedência caso não haja outras questões envolvidas (como manutenção de código, desempenho, …).

Garbage Collection

Uma das soluções que foi sugerida para resolver esse problema é chamada de Garbage Collection. A premissa é que o desenvolvedor não deveria se preocupar com o gerenciamento de recursos e alguma outra ferramenta, como o compilador ou o interpretador da máquina virtual, é que devia lidar com essa tarefa. Essa técnica depende de uma estratégia e um algoritmo e é a solução que linguagens como Java adotam.

Para automatizar o gerenciamento de memória, a definição do ponto de vista do programador é utilizada. Então a pergunta que resta para solucionar o problema é “como podemos saber que um objeto nunca mais será usado novamente?”.

Estratégias de Garbage Collection decidem quando a limpeza de recursos não utilizados deve ocorrer e podem ser eventos como “quando a memória acabar” ou “a cada 5 minutos”. Alguns algoritmos serão discutidos isoladamente a seguir, mas eles não deveriam depender de uma estratégia específica. Uma forma de conseguir um algoritmo de Garbage Collection desacoplado da estratégia é projetando um algoritmo in situ.

Mark and Sweep

Mark and Sweep é um algoritmo de Garbage Collection que obriga que todos os objetos tenham o campo mark bit e cujo funcionamento acontece em duas fases, mark e sweep.

Na fase mark, o algoritmo deve identificar os objetos alcançáveis, marcando-os através do campo mark bit. Um pseudocódigo para essa fase segue:

No pseudocódigo anterior, root é a variável que contém os objetos raízes, que são objetos localizados na stack.

Na fase sweep, todos os objetos devem ser vasculhados. Os que tiverem o campo mark bit com o valor falso devem ser destruídos e os objetos restantes devem ter o campo mark bit atribuídos ao valor falso. Um pseudocódigo segue:

O algoritmo apresentado não é in situ, como observado, pela recursão que acontece na fase mark. É possível remover essa recursão usando uma lista, mas a lista em si faz com que o algoritmo não seja in situ. Um truque usado para transformar o algoritmo mark and sweep em um algoritmo in situ é inverter os ponteiros quando nós os seguimos, fazendo o objeto observado apontar para o objeto pai. Dessa forma, podemos armazenar a trajetória de quais objetos foram observados sem usar qualquer espaço extra quando o algoritmo é executado.

Stop and Copy

No algoritmo Stop and Copy, a região de memória é dividida em duas metades, uma reservada para a aplicação e outra reservada para o Garbage Collector. Quando o algoritmo é executado, ele começa a rastrear os objetos alcançáveis e, sempre que um novo objeto alcançável é encontrado, ele é movido para a região de memória reservada para o Garbage Collector e toda referência para esse objeto é atualizada. Ao final do processo, a região de memória reservada ao Garbage Collector passa a ser a região de memória reservada a aplicação e vice-versa.

Uma forma de atualizar as referências aos objetos é, no momento em que um objeto for copiado, utilizar a região na qual ele residia para armazenar um ponteiro que aponta para sua nova região.

Garantir a propriedade de algoritmo in situ nesse caso é bem mais fácil e uma forma de se fazer isso seria dividir a região reservada ao Garbage Collector em 3 partes, sendo a primeira a de objetos analisados e copiados, a segunda apenas de copiados e a terceira a região livre. Tudo que é necessário para manter esse modelo de memória são dois ponteiros, além do ponteiro que aponta para o começo da região de memória.

Exceptions

Exceções formam uma técnica de tratamento de erros que visa diminuir o número de erros e melhorar a manutenção de código. Um assunto interessante, de fato, porém, não é um objetivo desse texto explicá-las. É um objetivo, entretanto, explicar que elas impõem novas restrições nas soluções de gerenciamento de recursos. Considere o seguinte código:

No exemplo anterior, o que acontece quando uma exceção é lançada antes do arquivo ser fechado? A linguagem Java permite que objetos tenham destrutores, métodos que executam instruções quando a máquina virtual destruir o objeto. Nada impede que a classe FileReader implemente um destrutor que chame o método close, mas, por outro lado, não há garantias de quando, ou mesmo se, o objeto será destruído. O comportamento não é determinístico.

A falta de determinismo pode parecer um problema pequeno a olhos desatentos, mas é um problema tão sério que o tratamento de exceções de linguagens como Java tem uma palavra-chave extra, a palavra-chave finally.

Considere o caso em que o programa fica aberto por bastante tempo ou trata uma quantidade de arquivos razoavelmente grande. Vários sistemas operacionais seguros costumam impor limites configuráveis aos processos e há um limite também para o número de arquivos abertos. O limite de arquivos abertos por processo, por exemplo, no sistema que estou usando nesse momento, é 1024. Recursos são finitos e não é desejável desperdiçá-los.

O bloco finally sempre será executado, independente se uma exceção foi lançada ou não.

Eu não quero discutir aqui, nesse momento, os impactos na performance introduzidos pelo uso de Garbage Collection, mas Garbage Collection, de fato, resolve o problema de gerenciar memória. Entretanto, esse texto é sobre gerenciamento de recursos, um problema mais geral e, por outro lado, o principal foco das técnicas de Garbage Collection é o gerenciamento de memória.

RAII

Agora que você já conhece a proposta Garbage Collection, é chegado o momento de conhecer a técnica Resource Acquisition Is Intialization, ou RAII, para economizar. A técnica, em contraste com Garbage Collection, assume que a responsabilidade de gerenciar os recursos do programa não é inteiramente do compilador/interpretador. Essa é a solução adotada por linguagens como C++.

A ideia é que todos os objetos tenham um construtor e um destrutor, sejam eles fornecidos por você ou pelo compilador. Quando um objeto entra em escopo, ele é construído, e quando sai de escopo, ele é destruído, havendo assim um gerenciamento de recursos automático e determinístico.

Como mostra o exemplo anterior, você não precisa criar seus próprios construtores e destrutores para se beneficiar dessa técnica. Por ser determinístico, a técnica RAII pode ser utilizada onde a técnica GC não seria, sozinha, o suficiente, o que exigiria o uso de finally. Um uso é demonstrado no exemplo a seguir:

O código anterior mostra a simplicidade e elegância da técnica RAII. O RAII exige que você encapsule cada tipo de recurso em sua própria classe, o que poderia significar uma quantidade maior de código, porém, apesar de serem ambos comuns, aquisições de recursos são bem mais comuns que recursos personalizados que precisam de comportamento especializado. Um fato que ajuda ainda mais a aumentar a produtividade é a presença de abstrações genéricas como unique_ptr e shared_ptr que encapsulam comportamentos comuns e podem ser usadas para qualquer classe.

Referências

Além da experiência de memórias profundas cuja origem não é mais rastreável, esse post foi baseado, principalmente, nas aulas sobre Garbage Collection do Alex Aiken disponíveis no Coursera e em alguns artigos das edições 106 e 107 da revista Overload.

Esse texto não traz nenhuma contribuição nova para a área e o único diferencial entre ele e as referências citadas é o conteúdo em português. Entretanto, esse texto pode ajudar a divulgar conhecimento útil e alimentar a discussão sobre o tema. Além disso, até onde lembro, não é plágio se você citar as referências, então minha consciência permanece limpa, caso ela exista.

EDIT (2015/01/15):

Exemplo de tentativa desesperada em C.

%d blogueiros gostam disto: