BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Notícias Loop: uma linguagem para a JVM com foco em programação multicore

Loop: uma linguagem para a JVM com foco em programação multicore

O Loop é uma linguagem de programação compacta para máquina virtual Java, influenciado pelas linguagens Haskell, Scheme, Ruby e Erlang, e que tenta reunir as melhores características da programação funcional e de linguagens orientadas a objetos, de forma consistente e pragmática. Veja aqui

Com o Loop, os programas são compilados em tempo real, gerando bytecode otimizado para a JVM. Não sofrem, portanto, perda de desempenho devido à interpretação de código; ao mesmo tempo é mantida a responsividade e velocidade do esquema "editar-executar" característico do código interpretado.

A estrutura de um arquivo do Loop é a seguinte:

declaração do módulo (module)

declaração dos imports

definição de funções e tipos

expressões livres

Aqui está um exemplo de um programa em Loop:


module mymodule

require othermod
require yet.another

class Pair ->
  left: 0
  right: 0

main ->
  new Pair()   # comentários podem aparecer em qualquer lugar

# expressões livres devem aparecer apenas no final
print('mymodule is go!')

O InfoQ.com ouviu o criador do Loop, Dhanji R. Prasanna, que é um ex-funcionário do Google e coautor da especificação JAX-RS, além de autor do livro "Injeção de Dependências: Design Patterns" pela Manning (título original "Dependency Injection: Design Patterns", não publicado no Brasil).

InfoQ: Como podemos comparar o Loop às demais linguagens para a JVM?

Dhanji: Eu diria que a filosofia do Loop é oferecer uma experiência consistente, simples e divertida na codificação. Todos os recursos são projetados com essa perspectiva abrangente em mente, além do cuidado com a forma que recursos interagem entre si sintaticamente e semanticamente.

Em outras linguagens, às vezes há várias maneiras de fazer a mesma coisa, como se isso fosse uma característica da linguagem; mas algumas dessas maneiras parecem artificiais. Já no Loop tentei diminuir o número de formas canônicas de se fazer as coisas, buscando concisão e simplicidade. O resultando foi uma sintaxe mais confortável e legível. Acho que deve ser tão fácil ler o código quanto escrevê-lo; e igualmente divertido.

Outro ponto de diferença é que o Loop compila o código diretamente para o bytecode da JVM, mas o faz em tempo real. Isso significa que se comporta e se "parece" com uma linguagem de script (no estilo REPL, como Lisp), mas é bem melhor que uma linguagem interpretada. Vou deixar, claro, que outros executem seus benchmarks, mas até agora nos meus testes rápidos, o Loop tem se mostrado extremamente veloz. Dei também muita ênfase ao tempo de inicialização; por isso o Loop inicia rápido quanto o próprio Java permite. Esse aspecto muitas vezes é ignorado por linguagens modernas para a JVM.

O Loop também se integra bem com o Java. É fácil chamar métodos ou instanciar objetos do Java a partir de um programa em Loop. Listas, conjuntos e mapas são simplesmente membros do pacote java.util, mas com várias extensões (isso vale também para Strings). Isso difere de algumas linguagens, que usam dois conjuntos de bibliotecas de dados para fornecer extensões.

Por fim, o Loop tem suporte nativo à concorrência, com imutabilidade e compartilhamento seguro de estado embutidos na linguagem.

InfoQ: Você diz que muitas das características do Loop são inspiradas por linguagens como Haskell, Scheme e Ruby. Poderia mencionar exemplos?

Dhanji: É um assunto delicado, pois quando se diz "foi inspirado em", as pessoas tendem a pensar "é uma cópia de", e terminam usando um pente-fino para procurar as diferenças. A influência maior vem da sintaxe dessas linguagens. As mais fáceis de se perceber são: correspondência de padrões (pattern matching) e os blocos "where" e "do" do Haskell; o sistema de tipos, os módulos, TCO (Tail Call Optimization) e construções léxicas (closures) do Scheme; além de ideias tais como símbolos e o scripting de forma livre do Ruby.

Um exemplo sintático dessas influências é a possibilidade de chamar funções em notação pós-fixada:

print(36)
# pode ser escrito como:
36.print()

Isso torna as chamadas parecidas com as de métodos Ruby, mas na realidade são simplesmente funções polimórficas (overloaded). Acho isso muito útil para melhorar a legibilidade de alguns "tipos específicos" de código, especialmente quando "se estende" a funcionalidade existente em objetos Java. Claro, há um equilíbrio a ser alcançado, mas acredito que isso virá na medida em que o Loop amadurecer.

As influências semânticas mais profundas, do Haskell e do Scheme (dessa última em particular), também estão presentes no design funcional da linguagem. Um exemplo é o distanciamento do modelo stateful, e o design orientado ao encapsulamento para uma abordagem mais stateless e declarativa. Assim como no Scheme, o Loop também traz um "superconjunto impuro" para IO. Por outro lado, impõe a imutabilidade sempre que lida com a concorrência. Esta última é uma influência filosófica do Haskell.

Além disso, a ênfase em tornar código declarativo mais fácil de escrever (e ler) é também uma influência do Haskell. Gosto da filosofia de que o código deve ser lido como uma solução, ao invés de um amontoado de instruções; em outras palavras, enfatizando "o que" em vez de o "como" do programa. O Loop definitivamente segue esta filosofia.

InfoQ: O Loop dá muita ênfase à concorrência, além de fornecer uma abstração nativa de envio de mensagens. Poderia explicar como isso se compara a outras tecnologias populares de concorrência na JVM e além?

Dhanji: Há muito do estado da arte do Erlang nisso. Existem dois métodos principais para se fazer concorrência no Loop. Ambos foram construídos na linguagem, e também são úteis se usados em conjunto:

  • Canais orientados a mensagens: uma abstração orientada a eventos sobre message-passing, filas e pools de threads trabalhadores;
  • Memória transacional em software: um esquema livre de bloqueio, atômico e consistente para compartilhamento de estado mutável.

No primeiro caso, toda a abstração já é tratada para você. Pode-se definir vários "canais" leves, que podem ser configurados para que sejam executados em paralelo, para tratar um grande número de tarefas, ou para processar uma tarefa de cada vez (sequencialmente) em lotes. Isso fornece uma maneira fácil de se criar filas de eventos "fragmentadas", com naturalidade.

Uma vez que os canais são leves, pode-se criar dezenas de milhares deles sem nenhum prejuízo, e usá-los para fragmentar a execução da tarefa, por exemplo, por usuário. Há também uma memória local leve e persistente, disponível para cada canal de serialização, tornando mais fácil implementar o processamento incremental de tarefas. O Loop também garante que as threads de trabalho (worker) sejam balanceadas de forma homogênea nos canais, usando um fator configurável de "justiça" (fairness).

Tudo o que descrevi até agora já está disponível. Além disso, pode-se configurar cada canal para que tenha o seu próprio pool dedicado de threads, e assim por diante.

InfoQ: E a memória transacional?

A memória transacional é uma construção muito mais poderosa, semelhante à "concorrência otimista" utilizada em bancos de dados. A ideia é que nada nunca trava, mesmo durante a escrita. Esse tipo de memória é otimizado para taxas extremamente elevadas de leitura e escrita não-bloqueantes. Isto faz parte da própria gramática da linguagem:

update(person) in @person ->
  this.name: person.name,
  this.age: person.age

Perceba o trecho "in @person", que diz ao Loop para transacionar na célula @person.

Nesse método atualizei a "célula transacional" @pessoa com novos dados a partir do argumento passado em update(). O ponteiro "this" refere-se à célula atual, que está em uma transação ao longo da função. Quando a função for concluída, as alterações em @person serão visíveis atomicamente para todas as demais threads; ou nenhuma das alterações, se a operação falhar (semelhante a um rollback).

Todas as outras threads (mesmo as que não participem de uma transação) continuam a ver uma célula @person consistente até que a transação seja executada. Logo em seguida veem os novos dados de "person" como um todo, sem locks ou espera. O interessante aqui é que as threads de leitura e escrita nunca ficam bloqueadas.

Esse recurso está ainda um pouco alfa, pois estou tentando chegar à semântica ideal; mas sinto que estou próximo a uma API de canais rica, que torne a programação concorrente no Loop elegante, poderosa e fácil de entender.


Você pode contribuir com o Loop no Github.

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT