Há alguns meses estava pareando com o Alberto Souza para melhorar o plugin do VRaptor para scala fazendo o VRaptor navegar pelos getters (properties) do Scala.
O componente a ser sobrescrito no VRaptor é o TypeFinder, que é responsável por descobrir os tipos dos caminhos que colocamos nas URI templates do VRaptor. Ex:
@Get("/livros/{livro.id}") //==> o parâmetro livro.id é um Long, por exemplo
public void visualiza(Livro livro) {...}
A implementação padrão é a seguinte (não precisa tentar entender o que acontece ainda, é só pra mostrar o estilo do código):
public Map<String, Class<?>> getParameterTypes(Method method, String[] parameterPaths) {
Map<String,Class<?>> result = new HashMap<String, Class<?>>();
String[] parameterNamesFor = provider.parameterNamesFor(method);
for (String path : parameterPaths) {
for (int i = 0; i < parameterNamesFor.length; i++) {
String name = parameterNamesFor[i];
if (path.startsWith(name + ".") || path.equals(name)) {
String[] items = path.split("\.");
Class<?> type = method.getParameterTypes()[i];
for (int j = 1; j < items.length; j++) {
String item = items[j];
try {
type = new Mirror().on(type).reflect().method("get" + upperFirst(item)).withoutArgs().getReturnType();
} catch (Exception e) {
throw new IllegalArgumentException("Parameters paths are invalid: " + Arrays.toString(parameterPaths) + " for method " + method, e);
}
}
result.put(path, type);
}
}
}
return result;
}
A idéia do código é, basicamente, pegar os caminhos ({livro.id}), partir dos nomes dos parâmetros ([livro]) e procurar o tipo (livro.getId() retorna Long). Mas o principal é perceber que isso é um típico código imperativo em java: pegar todos os dados necessários (parameterPaths, parameterNames, etc) percorrê-los e popular um mapa (result) que é o resultado esperado do método.
Para transformar esse código em funcional precisamos mudar o nosso pensamento. Para deixar o código mais funcional o mais natural é, a partir dos dados, aplicar funções e transformá-los em outros dados. Algo como criar uma função que tem como entrada a lista de paths e a lista de nomes e, como saída, o mapa de caminhos para tipos. Até aí, nada muito diferente da programação imperativa. A grande sacada é que não vamos chegar ao nosso objetivo executando comandos um atrás do outro, mas sim fazer composição de funções (lembra do f ∘ g do colégio?) transformando, aos poucos, a entrada na saída esperada.
A tradução direta do código acima para Scala, feita automaticamente com o menu 'Convert Java file to Scala' do IntelliJ e levemente modificada retirando elementos desnecessários, fica assim:
def getParameterTypes(method: Method, parameterPaths: Array[String]) = {
val result = new HashMap[String, Class[_]]
val parameterNamesFor = provider.parameterNamesFor(method)
for (path <- parameterPaths) {
for (i <- 0 to (parameterNamesFor.length - 1)) {
val name = parameterNamesFor(i)
if (path.startsWith(name + ".") || (path == name)) {
val items = path.split("\.")
var clazz = method.getParameterTypes(i)
for(j <- 1 to (items.length - 1)) {
val item = items(j)
try {
clazz = new Mirror().on(clazz).reflect.method("get" + upperFirst(item)).withoutArgs.getReturnType
} catch {
case e => {
throw new IllegalArgumentException("Parameters paths are invalid: " + Arrays.toString(parameterPaths) + " for method " + method, e)
}
}
}
result.put(path, clazz)
}
}
}
result
}
Esse código não tem quase nada de funcional ainda, a idéia é deixá-lo mais funcional com alguns recursos do Scala.
O segundo for
está passeando pelos índices, já que precisamos usar o mesmo índice para pegar o nome e o tipo dos parâmetros, que estão em listas (arrays) diferentes. Em Scala, podemos trocar:
for (i <- 0 to (parameterNamesFor.length - 1)) {
por:
for (i <- parameterNamesFor.indices) { // indices é o plural de index ;)
No terceiro for
precisamos ignorar o primeiro elemento do caminho: já sabemos o seu tipo e vamos navegar a partir dele. O código com o jeitão java abaixo tem uma tradução bem mais legal em Scala (e em várias outras linguagens).
val items = path.split("\.")
for(j <- 1 to (items.length - 1)) {
val item = items(j)
O que queremos é percorrer a lista de items jogando fora o primeiro elemento:
val items = path.split("\.")
for (item <- items.drop(1))
ou ainda:
for (item <- path.split("\.").drop(1))
Só com os dois detalhes acima (além de renomear variáveis e remover o try..catch, que nunca é obrigatório em Scala) já temos um código bem mais limpo, mas ainda não bom (ou funcional) o suficiente:
def getParameterTypes(method: Method, paths: Array[String]) = {
val result = new HashMap[String, Class[_]]
val names = provider.parameterNamesFor(method)
for (path <- paths) {
for (i <- names.indices) {
val name = names(i)
if (path.startsWith(name + ".") || (path == name)) {
var clazz = method.getParameterTypes(i)
for(item <- path.split("\.").drop(1)) {
clazz = new Mirror().on(clazz).reflect.method("get" + upperFirst(item)).withoutArgs.getReturnType
}
result.put(path, clazz)
}
}
}
result
}
O próximo passo é eliminar os dois primeiros for
's aninhados usando um recurso do Scala chamado for comprehensions
. Os dois for
's são equivalentes a:
for (path <- paths; i <- names.indices) {
Isso é bem mais do que só economizar código, mas a explicação completa sobre o que é isso precisaria de um post inteiro. Para entender melhor o poder de for comprehensions
, vamos fazer o inline da variável name
:
for (path <- paths; i <- names.indices) {
if (path.startsWith(names(i) + ".") || (path == names(i))) {
//resto do código
}
}
Tudo que temos dentro do for
agora é o if
. Nesse caso, podemos transferir a responsabilidade de filtrar os elementos para o próprio for
:
for (path <- paths; i <- names.indices; if path.startsWith(names(i) + ".") || (path == names(i))) {
//resto do código
}
De novo, isso é bem mais do que só economizar código. Transformamos um código procedural (for -> for -> if
) em um código funcional. É como se tivéssemos descrito o domínio da nossa função (voltando ao colégio):
```
D = { path ∈ paths, i ∈ indices tais que path começa com names(i)}
```
E o que está dentro do for
será executado para cada elemento do domínio D
- é uma função do domínio D
numa imagem Im
. Louco, não?
Código até agora:
def getParameterTypes(method: Method, paths: Array[String]) = {
val result = new HashMap[String, Class[_]]
val names = provider.parameterNamesFor(method)
for (path <- paths; i <- names.indices; if path.startsWith(names(i) + ".") || (path == names(i))) {
var clazz = method.getParameterTypes(i)
for(item <- path.split("\.").drop(1)) {
clazz = new Mirror().on(clazz).reflect.method("get" + upperFirst(item)).withoutArgs.getReturnType
}
result.put(path, clazz)
}
result
}
================
BÔNUS:
Ainda dá pra deixar o código acima mais funcional (não necessariamente mais legível, mas isso é outra história).
Veja esse código:
var clazz = method.getParameterTypes(i)
for(item <- path.split("\.").drop(1)) {
clazz = new Mirror().on(clazz).reflect.method("get" + upperFirst(item)).withoutArgs.getReturnType
}
O que estamos fazendo, no fim das contas, é transformar a lista de itens (path.split("\\.").drop(1)
) em uma clazz
a partir de um valor inicial (method.getParameterTypes(i)
). Isso é uma operação bem comum em programação funcional chamada fold. Então podemos usar o foldLeft
e evitar o uso da variável mutável (var) clazz:
val items = path.split("\.").drop(1)
val clazz = items.foldLeft(method.getParameterTypes(i)) { (clazz, item) =>
new Mirror().on(clazz).reflect.method("get" + upperFirst(item)).withoutArgs.getReturnType
}
================
BÔNUS 2:
Outra coisa não muito funcional que ainda estamos fazendo é a geração do mapa de resultado:
val result = new HashMap[String, Class[_]]
for (path <- paths ....) {
val clazz = .....
result.put(path, clazz)
}
result
Em programação funcional não deveríamos usar variáveis mutáveis (o valor do objeto result
muda durante a execução do código). Em algumas linguagens como Erlang e Haskell isso é até uma regra da linguagem. A idéia é gerar esse mapa através apenas de aplicação e composição de funções.
Para fazer isso, precisaremos de outro recurso das for comprehensions
: o yield. O que ele faz é pegar o retorno de cada iteração do for
e gerar uma nova lista. Por exemplo:
val novaLista = for (i <- List(1,2,3)) yield i + 2
println(novaLista) // ==> List(3, 4, 5)
O que vamos fazer aqui é transformar cada item do nosso domínio D
(o que está dentro do for
) em uma tupla (path, clazz)
que representa uma entrada (chave,valor) do nosso mapa:
val entries =
for (path <- paths; i <- names.indices;
if path.startsWith(names(i) + ".") || (path == names(i))) yield {
val items = path.split("\.").drop(1)
val clazz = items.foldLeft(...) { (clazz, item) =>
new Mirror().on(clazz)....getReturnType
}
path -> clazz // jeito de criar a tupla (path, clazz) em scala
}
Agora a variável entries
é a lista de tuplas produzida pela aplicação da função que está dentro do for
nos elementos do domínio D
, definido pela for comprehension
.
E como eu transformo isso num Map
agora? O legal é que o Map
do Scala já tem um construtor que recebe uma lista de tuplas!
val result = Map(entries:_*) // na verdade recebe um varargs, por isso o :_*, que explode a lista
Código final:
def getParameterTypes(method: Method, paths: Array[String]) = {
val names = provider.parameterNamesFor(method)
val entries =
for (path <- paths; i <- names.indices;
if path.startsWith(names(i) + ".") || (path == names(i))) yield {
val items = path.split("\.").drop(1)
val clazz = items.foldLeft(method.getParameterTypes(i)) { (clazz, item) =>
new Mirror().on(clazz).reflect.method("get" + upperFirst(item)).withoutArgs.getReturnType
}
path -> clazz
}
Map(entries:_*)
}
E aí, melhor ou pior que o código java correspondente?</p>
Oi Lucas. Estou com preguiça de tentar, mas acho que se você especificar o tipo de retorno do getParameterTypes, não precisa fazer o Map(entries:_*). A mágica do CanBuildFrom gera o Map para você.