04 October 2011

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>



  • - Rafael de F. Ferreira - Sat, 15 Oct 2011 17:49:53 -0700
    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ê.