BT

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

Contribuir

Tópicos

Escolha a região

Início Artigos Explorando LISP na JVM

Explorando LISP na JVM

Uma das coisas mais excitantes do Java atualmente é o trabalho para fazer outras linguagens de programação rodar na JVM. Há muito falatório sobre JRuby, Groovy, Scala e sobre o engine JavaScript Rhino. Mas porque parar aí? Se você quer dar um passo fora do comum e mergulhar em algo bem diferente de Java, Lisp é uma ótima opção. E há muitas implementações open-source da linguagem de programação Lisp para a JVM por aí, prontas para serem exploradas.

Então o que faz de Lisp algo interessante? Uma coisa, esta linguagem de 50 anos de idade tem sido catalisadora de muitas idéias que nós usamos hoje. A construção if-then-else veio originalmente do Lisp, assim como tentativas precoces de orientação a objetos e gerenciamento automático de memória com garbage collector. Clusores lexicas -- um tópico quente para programadores Java atualmente -- foram primeiramente exploradas em Lisp lá pelos anos 70. Além disso, Lisp ainda tem muitas outras funcionalidades únicas que outras linguagens estão para adotar, o tipo de boas idéias que estão destinadas a reaparecer no futuro.

Este artigo é destinado aos desenvolvedores Java curiosos sobre Lisp. Vamos discutir os diferentes dialetos de Lisp que estão disponíveis na JVM hoje e apresentar um mini-curso sobre como a programação em Lisp funciona e o que a torna única. Finalmente nós vamos olhar para como o código Lisp pode ser integrado em um sistema Java.

Há muitos sistemas feitos tanto em Lisp comercial quanto em Lisp open-ource disponíveis para muitas plataformas diferentes. Para um usuário Java que quer começar a explorar Lisp, ficar na JVM é uma boa primeira escolha. É fácil começar e tem o benefício de ser capaz de usar todas as APIs Java existentes e ferramentas com as quais você já está familiarizado.

COMMON LISP e SCHEME

Há dois dialetos principais de Lisp: Common Lisp e Scheme. Eles são baseados em muitas das mesmas idéias, mas ainda assim é diferente o suficiente para levantar debates intensos sobre qual é a melhor opção.

Common Lisp é um padrão ANSI que foi finalizado em 1991, consolidando idéia de muitas Lisps anteriores. É um grande ambiente desenhado para muitos tipos de desenvolvimento de aplicações, mais famosos para inteligência artificial. Scheme por outro lado vem de um mundo mais acadêmico, é deliberadamente mais minimalista e se tornou uma boa linguagem para ensinar ciência da computação e para scripts embutidos. Algumas outras Lisps bem conhecidas você pode rodar em pequenas DSLs de aplicações específicas, como Emacs Lisp ou AutoCAD AutoLISP.

Na JVM há implementações de ambos os dialetos, mas os Schemes são mais maduros. Armed Bear Common Lisp (www.armedbear.org/abcl.html) é uma implementação razoavelmente completa do padrão Common Lisp, mas sofre porque a distribuição depende de outro sistema Common Lisp que deve ser instalado, o que pode ser um problema para iniciantes.

Do lado do Scheme, os dois principais players são Kawa (www.gnu.org/software/kawa) e SISC (www.sisc-scheme.org -- the Second Interpreter of Scheme Code). Para os exemplos deste artigo, nós vamos usar o Kawa. Kawa é atualmente um framework para criar novas linguagens compiladas para bytecode Java, com Scheme sendo uma das implementações. Incidentalmente, seu criador Per Bothner está agora trabalhando para a Sun no compilador para o projeto JavaFX.

Outro candidato que vale a pena conferir é Clojure (clojure.sourceforge.net). Ele é uma nova linguagem, com seu próprio dialeto Lisp que fica em algum lugar entre Scheme e Common Lisp. Sendo desenhado diretamente para a JVM ele tem a integração mais pura com Java de todas as Lisps mencionadas até agora, assim como implementa algumas outras idéias excitantes, como suporte built-in para memória concorrente e transacional. Clojure está ainda em estágio beta exploratório, então talvez seja um pouco cedo para construir algo sério, mas este é definitivamente um projeto para acompanhar bem de perto.

READ-EVAL-PRINT-LOOP

Vamos começar instalando o Kawa. A distribuição, um único arquivo jar, pode ser feito o download em ftp://ftp.gnu.org/pub/gnu/kawa/kawa-1.9.1.jar. Uma vez obtido este arquivo, adicione isso ao seu classpath. Com isso feito você está pronto para começar com o REPL rodando este comando:

  java kawa.repl
#|kawa:1|#

Ele vai iniciar o Kawa e mostrar um prompt. Mas o que é isso? REPL significa READ-EVAL-PRINT-LOOP (Leia-Interprete-Imprima-Repita) e é uma forma de interface com um sistema Lisp executando -- ele lê seu input (READ), Interpreta ele (EVALuate), imprime o resultado (PRINT) e então repete o processo voltando ao prompt. O modo que você desenvolve um programa Lisp é freqüentemente diferente do ciclo "Escrever código, compilar, executar" normal quando programando em Java. Programadores Lisp tipicamente iniciam seus sistemas Lisp e os mantém rodando, mantendo uma linha entre compile e runtime. Dentro do REPL, funções e variáveis podem ser modificadas on the fly. Código é compilado e interpretado dinamicamente.

Primeiro, algo realmente simples: adicionar 2 números.

  #|kawa:1|# (+ 1 2)
3

Esta é a estrutura típica de uma expressão em Lisp, ou sintaxe "form". A sintaxe é muito consistente: as expressões são sempre envoltas por parênteses e usam uma notação de prefixo, assim o + vai antes dos dois termos. Para realizar uma construção mais avançada, você aninha os muitos forms para construir uma estrutura de árvore:

  #|kawa:2|# (* (+ 1 2) (- 3 4))
-3

Funções built-in do Scheme trabalham da mesma forma:

  #|kawa:3|# (if (> (string-length "Hello world") 5)
(display "Longer than 5 characters"))
Longer than 5 characters

Aqui você tem uma declaração-if checando se uma string específica é mais longa do que cinco caracteres. Se isto ser verdadeiro, a próxima expressão é executada causando uma mensagem impressa. Note que a identação aqui é somente para legibilidade de propósitos. Você pode escrever isso tudo inteira em uma linha se você quisesse.

O estilo cheio de parênteses usado para o código Lisp é conhecido pela "s-expression". Isto também duplica como uma forma genérica para definir a estrutura de dados, muito como XML. Lisp tem muitas funções built-in que deixa você manipular dados em formato s-expression muito facilmente, e isto de fato conduz para uma de suas forças. Desde que a sintaxe é bem simples, escrevendo programas para gerar ou modificar códigos isto é mais simples do que em outras linguagens. Nós veremos mais sobre isso quando chegarmos nos exemplos de macros.

FUNCTIONS

Scheme é normalmente considerado parte da família das linguagens de programação funcional. Diferente do mundo orientado a objetos, o principal meio de abstração são as funções e os dados que elas processam -- que não são classes e objetos. Tudo que você faz em Scheme é chamar funções que recebem alguns argumentos e retornam um resultado. Para criar uma função você usa a palavra chave define:

  #|kawa:4|# (define (add a b) (+ a b)) 

O código acima define a função add, que recebe dois argumentos, a e b. O corpo da função simplesmente executa +, e automaticamente retorna o resultado. Note que não há declarações de tipo estáticas. Toda checagem de tipo é feita em tempo de execução, da mesma forma que em outras linguagens dinâmicas.

Com a função definida, você pode simplesmente chamá-la a partir do REPL:

  #|kawa:5|# (add 1 2)   3 

Funções são cidadãos de primeira classe no mundo do Scheme e podem ser passadas para outras funções de forma parecida com objetos em Java. Isso abre muitas possibilidades interessantes. Vamos começar criando uma função que recebe um argumento e o duplica:

  #|kawa:6|# (define (double a) (* a 2)) 

e então definir uma lista de 3 números, chamando a função list:

  #|kawa:7|# (define numbers (list 1 2 3)) 

Agora a parte excitante:

  #|kawa:8|# (map double numbers)   (2 4 6) 

Aqui você chamou map, que recebe dois argumentos: outra função e uma lista do mesmo tipo. map irá iterar sobre cada um dos elementos na lista e chamar a função fornecida como parâmetro para cada um deles. Os resultados são então organizado em uma nova lista, que é o que você pode ver como retorno no REPL. Essa é a forma "funcional" de fazer o que seria feito com um loop for em Java.

LAMBDAS

Ainda mais conveniente é usar a palavra chave lambda para definir uma função anônima, que funciona de forma similar a uma inner class anônima em Java. Refazendo o exemplo acima, você pode pular a definição da função intermediária double e declarar seu map assim:

  #|kawa:9|# (map (lambda (a) (* 2 a)) numbers)
(2 4 6)

Também é possível definir uma função que apenas retorna um lambda. Um exemplo clássico de livros é este:

  #|kawa:10|# (define (make-adder a) (lambda (b) (+ a b)))
#|kawa:11|# (make-adder 2)
#

O que aconteceu aqui? Primeiro você definiu uma função chamada make-adder que recebe um argumento, a. make-adder retorna uma função anônima que recebe outro argumento, "b". Quando chamada, esta função anônima irá calcular a soma de a e b.

Executando o código (make-adder 2) -- ou "me dê uma função que adiciona 2 ao número que eu passar" -- faz com que a REPL imprima que é apenas a função lambda impressa como string. Para usar isso voc6e deverá fazer algo assim:

  #|kawa:12|# (define add-3 (make-adder 3))
#|kawa:13|# (add-3 2)
5

O bom aqui é que lambda funciona como uma closure. Ela se "fecha" e mantém as referências para as variáveis que estavam no escopo quando ela foi criada. A lambda que era o resultado da chamada (make-adder 3) segura o valor de a, e quando (add-3 2) é executado, ela será capaz de calcular 3 + 2 e retornar o 5 tão esperado.

MACROS

As funcionalidade que olhamos até agora são muito similares ao que pode ser encontrada em linguagens dinâmicas mais novas. Ruby, por exemplo, também deixa você usar blocos anônimos para processar coleções de objetos, exatamente o que nós fizemos com a lambda e a função map anteriormente. Então vamos mudar de marcha e olhar algo mais Lisp: macros.

Tanto Scheme quanto Common Lisp possuem um sistema de macros. Quando você vê alguém se referindo a Lisp como "a linguagem de programação programável", é isso que eles querem dizer. Com macros você pode manipular o compilador e redefinir a linguagem em si. Aqui é o ponto em que a sintaxe uniforme de Lisp começa a valer a pena e tudo começa a ficar mais interessante.

Para um exemplo simples, vamos olhar para loops. Originalmente não há loop definidos na linguagem Scheme. A forma clássica de iterar sobre algumas coisas seria usar map ou chamadas recursivas de funções. Graças a um truque de compilador conhecido como otimizações tail-call, recursão pode ser usada sem explodir o stack. Assim, um comando do muito flexível foi adicionado e usá-lo para executar o loop seria como:

(do ((i 0 (+ i 1)))
((= i 5) #t)
(display "Print this "))

Aqui nós definimos uma variável de índice, i, inicializamos isso como zero e definimos para ser incrementado a cada iteração. O loop é interrompido uma vez que a expressão (= i 5) se torne verdadeira e então #t, o valor Scheme equivalente ao boolean true do Java, é retornado. Dentro do loop nós apenas imprimimos uma string.

Este seria um código bala de prata se tudo o que precisamos fosse um simples loop. Em alguns casos ser capaz de fazer alguma coisa mais direta seria melhor, como:

(dotimes 5 (display "Print this")) 

Graças às macros é possível adicionar a sintaxe especial dotimes à linguagem, usando a sintaxe apropriadamente nomeada função define-syntax:

(define-syntax dotimes
(syntax-rules ()
((dotimes count command) ; Padrão a ser reconhecido
(do ((i 0 (+ i 1))) ; Será convertido para
((= i count) #t)
command))))

Executar isso diz ao sistema que qualquer chamada para dotimes precisa ser tratada de forma especial. Scheme irá usar as regras de sintaxe que nós definimos para encontrar um padrão e expandi-lo antes de enviar o resultado para o compilador. Neste caso o padrão é (dotimes count command), que é transformado em um loop do regular.

Execute isso na REPL e você terá:

#|kawa:14|# (dotimes 5 (display "Print this "))
Print this Print this Print this Print this Print this #t

Duas questões devem surgir depois deste exemplo. Primeira, porque precisamos usar uma macro? Isso não poderia ser feito com uma função comum? A resposta é não - uma chamada a função poderia disparar a interpretação de todos os argumentos antes deles serem enviados e isso não funcionaria nesse caso. Como você manipularia (do-times 0 (format #t "Never print this")), por exemplo? A interpretação precisa ser deferida e isso só pode ser feito com uma macro.

Segundo, nós estamos usando uma variável i dentro da macro. Não haveria colisões se o código na expressão command tiver outra variável com o mesmo nome de variável? Não precisa se preocupar - as macros do Scheme são conhecidas como "higiênicas". O compilador automaticamente detecta e sabe como tratar colisões de nomes como essa, de forma completamente transparente para o programador.

Agora que você viu isso, imagine tentar adicionar sua própria construção de loop ao Java. É quase impossível. Bem, não tão impossível assim -- o compilador é open source, portanto você é livre para baixá-lo e por a mão na massa, mas essa não é uma opção realista. Em outras linguagens dinâmicas, closures podem te levar um pouco mais longe para fazer a linguagem se parecer com o que você quer, mas ainda há casos onde estas construções não são poderosas ou flexíveis o bastante para deixar você enfeitar a linguagem completamente.

Esta força é o porquê de Lisp freqüentemente aparecer como uma linguagem para ser vencida quando o assunto é meta-programação ou domain specific languages. Programadores Lisp têm sido por muito tempo os campeões da "programação bottom-up", quando a linguagem por si só é ajustada para se encaixar perfeitamente no problema de seu domínio, ao invés de achar outra forma "por fora".

CHAMANDO SCHEME A PARTIR DO JAVA

Um dos principais benefícios de executar outra linguagem na JVM é como o código escrito pode ser integrado com as aplicações existentes. É fácil imaginar o uso de Scheme para modelar algumas lógicas complexas de negócio que tendem a mudar freqüentemente e então embutir isso em uma framework java mais estável. O engine de regras Jess (www.jessrules.com) é um exemplo de uma idéia indo nessa direção, executando na JVM mas usando sua própria linguagen Lisp-like para declarar as regras.

Mas ganhar interoperabilidade entre linguagens diferentes para que funcionem de forma clara exige alguns truques, especialmente quando a diferença entre as duas é tão grande quanto no caso de Java e Lisp. Não há padrões existentes para fazer a integração e todos os dialetos que vivem na JVM abordam o problema de forma diferente. Kawa tem um suporte relativamente bom para integração com Java e nós vamos continuar a usando para investigar como definir uma GUI Swing usando código Scheme.

Executar código Kawa dentro de um programa Java é simples:

import java.io.InputStream;
import java.io.InputStreamReader;

import kawa.standard.Scheme;

public class SwingExample {

public void run() {

InputStream is = getClass().getResourceAsStream("/swing-app.scm");
InputStreamReader isr = new InputStreamReader(is);

Scheme scm = new Scheme();
try {
scm.eval(isr);
} catch (Throwable schemeException) {
schemeException.printStackTrace();
}
}

public static void main(String arg[]) {
new SwingExample().run();
}
}

Neste exemplo, um arquivo contendo um programa Scheme chamado swing-app.scm deve estar disponível no classpath. Uma nova instância do interprete, kawa.standard.Scheme, é criado, e chamado para interpretar o conteúdo do arquivo.

Kawa não suporta a API de scripts (JSR-223) que foi introduzida no Java 1.6 (javax.scripting.ScriptEngine, etc). Se você precisa de uma Lisp que o faça, sua melhor aposta seria SISC.

CHAMANDO BIBLIOTECAS JAVA A PARTIR DO SCHEME

Antes de avançarmos para escrever um programa Lisp maior, é tempo de buscar um editor melhor, caso contrário, trabalhar com todos aqueles parênteses vai te deixar louco. Uma das mais populares escolhas é claro usar Emacs -- ele é ao final de tudo programável em seu próprio dialeto Lisp -- mas um desenvolvedor Java ficaria mais confortável com Eclipse. Se esse é o caso você deve instalar o plugin gratuito SchemeScript antes de seguir em frente. Ele pode ser encontrado em schemeway.sourceforge.net/schemescript.html. Há também um plugin para Common Lisp chamado Cusp (bitfauna.com/projects/cusp).

Agora, vamos olhar ao conteúdo atual de swing-app.scm, e o que nó precisamos para definir uma GUI simples usando Kawa. Este exemplo irá exibir um pequeno frame com um botão. Após o botão ser clicado uma vez ele será desabilitado.

 (define-namespace JFrame )
(define-namespace JButton )
(define-namespace ActionListener )
(define-namespace ActionEvent )

(define frame (make JFrame))
(define button (make JButton "Click only once"))

(define action-listener
(object (ActionListener)
((action-performed e :: ActionEvent) ::
(*:set-enabled button #f))))

(*:set-default-close-operation frame (JFrame:.EXIT_ON_CLOSE))
(*:add-action-listener button action-listener)
(*:add frame button)
(*:pack frame)
(*:set-visible frame #t)

As primeiras linhas usam o comando define-namespace para definir nomes mais curtos para as classes Java que nós vamos utilizar, similar à declaração import Java.

Nós então definimos o frame e o botão. Objetos Java são criados usando a função make. No caso do botão nós passamos uma string como argumento para o construtor e Kawa é inteligente o suficiente para fazer a conversão para java.lang.String como necessário.

Vamos ignorar a definição de ActionListener por enquanto e olhar para as últimas 5 linhas primeiro. Aqui a notação *: é usada para disparar os métodos em objetos. Por exemplo (*:add frame button) é o equivalente a frame.add(button). Também note como os nomes dos métodos são automaticamente traduzidos do estilo Java camel-case (maiúsculas e minúsculas) para letras minúsculas separadas por traços típico do Scheme. Por tráz das cenas set-default-close-operation se tornará setDefaultCloseOperation, por exemplo. Outro detalhe aqui é como :. é usado para acessar campos estáticos da classe. (JFrame:.EXIT_ON_CLOSE) é o equivalente a JFrame.EXIT_ON_CLOSE.

Agora voltando ao ActionListener. Aqui a função object é usada para criar uma classe anônima que implementa a interface java.awt.event.ActionListener . A função action-performed é usada para chamar setEnabled(false) do botão. Alguma informação sobre o tipo deve ser adicionada aqui, para permitir ao compilador saber que action-performed deve ser uma implementação de void actionPerformed(ActionEvent e) definido na interface ActionListener. Nós dissemos que normalmente em Scheme você não precisa definir tipos, mas nesse caso quando você está interfaceando com Java, o compilador precisa de uma informação extra.

Uma vez que você tenha os dois arquivos, compile SwingExample.java e certifique-se de colocar as duas classes resultantes e swing-app.scm em algum lugar do classpath. Agora, execute java SwingExample para ver a GUI. Você também pode usar a REPL para interpretar o código do arquivo através da função load: (load "swing-app.scm"). Isso abre uma porta para manipular componentes GUI dinamicamente. Por exemplo, você pode mudar o texto do botão executando (*:set-text button "Novo texto") no prompt da REPL e ver as mudanças acontecerem imediatamente.

É claro que, este exemplo foi apenas para mostrar como chamar Java a partir de Kawa. 'Nem de longe este seria o código Scheme mais elegante que você pode imaginar. Se você quer definir uma GUI Swing maior em Scheme, você deve provavelmente utilizar uma nível de abstração maior e esconder o código "bagunçado" de integração por trás de umas funções e macros bem definidas.

RECURSOS

Espero que ao ler este artigo tenha lhe despertado algum interesse em Lisp. Acredite quando eu digo que ainda há muitas coisas para serem exploradas. Aqui estão alguns recursos onde você pode aprender mais:

SOBRE O AUTOR

Per Jacobsson é um arquiteto de software na eHarmony.com em Los Angeles. Ele trabalha com Java há 10 anos e tem programado em Lisp como hobby. Você pode encontrá-lo em pjacobsson.com.

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT