lambda

 

Expressões Lambda viraram o mundo da programação de cabeça pra baixo nos últimos anos. A maioria das linguagens modernas as adotaram como parte fundamental da programação funcional. Linguagens baseadas em JVM como Scala, Groovy e Clojure as integraram como parte essencial da linguagem. E agora, Java 8 está (finalmente) entrando na brincadeira.

O interessante das expressões Lambda é que da perspectiva do JVM elas são completamente invisíveis. Ele não tem noção algum sobre o que é uma função anônima ou uma expressão Lambda. Ele só conhece o bytecode, o que é uma especificação estritamente de OO. Fica a cargo dos responsáveis pela linguagem e o seu compilador trabalhar dentro dessas limitações para criar novos e mais avançados elementos da linguagem.

Passamos por isso pela primeira vez quando estávamos trabalhando na implementação de suporte a Scala no Takipi e tivemos que estudar o compilador de Scala a fundo. Com Java 8 logo ali, eu pensei que seria interessante ver como os compiladores de Scala e Java implementam expressões Lambda. Os resultados foram bastante surpreendentes.

Pra começar, eu peguei uma expressão Lambda simples que converte uma lista de Strings para uma lista dos seus tamanhos.

Em Java –


List names = Arrays.asList("1", "2", "3");
Stream lengths = names.stream().map(name -> name.length());

Em Scala –

val names = List("1", "2", "3")
val lengths = names.map(name => name.length)

Não se engane pela sua simplicidade – várias coisas complexes estão acontecendo nos bastidores.

Vamos começar com Scala

SCalaLam (1)

O Código

Eu usei o javap para visualizar o conteúdo do bytecode do arquivo .class produzido pelo compilador do Scala. Vamos dar uma olhada no bytecode resultante (isso é o que o JVM vai executar de fato).


// isso carrega a variável names na pilha (o JVM reconhece
// nela como variável #2).
// Ela vai continuar lá por um tempo até ser usada
// pela função <em>.map</em>

aload_2

Depois, as coisas ficam mais interessantes – uma nova instância de uma classe sintética gerada pelo compilador é criada e inicializada. Esse é o objeto que da perspectiva do JVM possui o método Lambda. É engraçado que enquanto o Lambda é definido como uma parte integral do nosso método, na realidade ele fica completamente fora da nossa classe.


dup // coloca ele na pilha de novo

// finalmente, invoca o c’tor. Lembre-se – é apenas um objeto simples
// da perspectiva do JVM.
invokespecial myLambdas/Lambda1$$anonfun$1/()V

// essas duas (longas) linhas carregam o factory immutable.List CanBuildFrom
// que criará a nova lista. Esse padrão factory faz parte da
// arquitetura de coleções do Scala
getstatic scala/collection/immutable/List$/MODULE$
Lscala/collection/immutable/List$;
invokevirtual scala/collection/immutable/List$/canBuildFrom()
Lscala/collection/generic/CanBuildFrom;

// Agora temos na pilha o objeto Lambda e o factory.
// A próxima fase é chamar a função .<em>map</em>().
// Se você se lembra, nós carregamos a variável <em>names</em> para
// a pilha no início. Agora ela vai ser usada como o
// this para a chamada .<em>map</em>(), que também vai
// aceitar o objeto Lambda e o factory para produzir a
// nova lista de tamanhos.

invokevirtual scala/collection/immutable/List/map(Lscala/Function1;
Lscala/collection/generic/CanBuildFrom;)Ljava/lang/Object;

Mas espere – o que está acontecendo dentro desse objeto Lambda?

O objeto Lambda

A classe Lambda é derivada de scala.runtime.AbstractFunction1. Através disso a função map() pode polimorficamente invocar o apply() sobrescrito, cujo código está abaixo –


// esse código carrega isso e o objeto alvo em questão,
// verifica que é uma String, e então chama outra sobrecarga de apply
// para fazer o trabalho de fato e grava o seu valor de retorno.
aload_0 // carrega isso
aload_1 // carrega o argumento String
checkcast java/lang/String // garante que é uma String – temos um Objeto

// chama outro método apply() na classe sintética
invokevirtual myLambdas/Lambda1$$anonfun$1/apply(Ljava/lang/String;)I

// grava o resultado
invokestatic scala/runtime/BoxesRunTime/boxToInteger(I)Ljava/lang/Integer
areturn

O método que de fato executa a operação .length() está aninhado naquele método apply adicional que simplesmente retorna o tamanho da String como esperávamos.
Ufa.. foi um longo caminho pra chegar até aqui.


aload_1
invokevirtual java/lang/String/length()I
ireturn

Pra uma linha tão simples como a que escrevemos acima, é gerado um bocado de bytecode – uma classe adicional e vários novos métodos. É claro que isso não tem a intenção de nos convencer a deixar de usar Lambdas (estamos programando em Scala, não C). Isso serve apenas para mostrar a complexidade por trás dessas estruturas. Pense só na quantidade de código e complexidade que entra na compilação de complexas correntes de expressões Lambda!

Eu estava esperando que Java 8 implementasse isso da mesma forma, mas fiquei bastante surpreso ao ver que eles adotaram uma abordagem completamente diferente.

Java 8 – Uma nova abordagem

JavaLam (1)

O bytecode aqui é um pouco menor mas faz algo bem surpreendente. Ele começa simplesmente carregando variável names e invocando o seu método .stream(), mas então ele faz algo bastante elegante. Ao invés de criar um novo objeto para abrigar a função Lambda, ele use a nova instrução invokeDynamic, que foi adicionada ao Java 7, para fazer um link dinâmico entre o local dessa chamada e a função Lambda de fato.


aload_1 // carrega a variável names

// chama a sua função stream()
invokeinterface java/util/List.stream:()Ljava/util/stream/Stream;

// a mágica do invokeDynamic!
invokedynamic #0:apply:()Ljava/util/function/Function;

// chama a função map()
invokeinterface java/util/stream/Stream.map:
(Ljava/util/function/Function;)Ljava/util/stream/Stream;

A mágica do invokeDynamic. Essa instrução foi adicionada ao Java 7 para tornar o JVM menos rigoroso, e permite que linguagens dinâmicas façam o link de símbolos em tempo de execução, ao invés de fazer todo o link estaticamente quando o código for compilado pelo JVM.

Links Dinâmicos. Se você observar a instrução invokedynamic, você verá que não referência alguma da função Lambda de fato (chamada lambda$0). A resposta está na maneira que a invokeDynamic foi projetada (o que por si só merece um post completo), mas a resposta simples é o nome e a assinatura do Lambda, que no nosso caso é –


// uma função chamada lamda$0 que obtém uma String e retorna um Inteiro
lambdas/Lambda1.lambda$0:(Ljava/lang/String;)Ljava/lang/Integer;

estão armazenados em uma entrada numa tabela separada no arquivo .class a qual o parâmetro #0 passou aos pontos de instrução. Essa nova tabela, na verdade, mudou a estrutura da especificação do bytecode pela primeira vez após alguns anos, exigindo também que nós adaptássemos o mecanismo de análise de erros do Takipi a ela.

O código Lambda

Esse é o código para a expressão Lambda de fato. Ele não tem nada de diferente – simplesmente carrega o parâmetro String, chama length() e guarda o resultado. Perceba que ele foi compilado como uma função estática para evitar ter que passar objeto this a mais para ele como vimos em Scala.


aload_0
invokevirtual java/lang/String.length:()
invokestatic java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
areturn

Essa é outra vantagem da abordagem do invokedynamic, já que ela nos permite invocar o método de uma forma polimórfica da perspectiva da função .map(), mas sem ter que alocar um objeto pra guardar ou invocar um método virtual sobrescrito. Muito legal!

Resumindo. É fascinante ver como Java, a mais “rigorosa” das linguagens modernas está agora usando links dinâmicos para fortalecer as suas novas expressões Lambda. Além disso é também uma abordagem eficiente, já que nenhuma classe adicional precisa ser carregada ou compilada – o método Lambda é simplesmente mais um método privado na nossa classe.
Java 8 fez realmente um trabalho muito elegante ao usar tecnologia nova introduzida no Java 7 para implementar expressões Lambda de uma maneira muito simples. De certa forma é um prazer ver que até uma senhora “venerável” como Java pode ensinar a todos nós alguns truques novos.

O Takipi detecta todas as suas exceções e erros e te informa porque eles aconteceram. Até mesmo através de threads e máquinas múltiplas. É instalado em 1 minuto. Menos de 2% de overhead – Experimente-o de graça.
Desenvolvedores Java – Instalem Takipi agora e ganhem uma camiseta de graça.

Duke-T-shirt

Java/Scala developers – Install Takipi now and get a free T-shirt

Java 8

Java 8 Exceptions have never been so beautiful – Try Takipi for Java 8

email
Tal is the CEO of OverOps. Tal has been designing scalable, real-time Java and C++ applications for the past 15 years. He still enjoys analyzing a good bug though, and instrumenting code. In his free time Tal plays Jazz drums.