Programação funcional em Java de um viciado em Scala

Tenho feito bastante desenvolvimento do Scala no trabalho ultimamente. Como muitas organizações que começaram a usar Scala, a nossa tradicionalmente é uma loja Java. As razões pelas quais pudemos usar Scala junto com Java é que ambas as linguagens são executadas na JVM, e Scala tem boa interoperabilidade com Java. Esses dois recursos tornam a venda razoavelmente fácil para gerentes em comparação com outras linguagens semelhantes. Quando você adiciona Scala à sua pilha Java, o trabalho normalmente será dividido entre manter os aplicativos legados existentes escritos em Java, além de trabalhar nas coisas novas e brilhantes escritas em Scala. Embora eu acredite que qualquer bom desenvolvedor deva ter poucos problemas para alternar contextualmente entre as duas linguagens, pode haver uma certa dificuldade entre seus diferentes paradigmas. Java é uma linguagem tradicional orientada a objetos, enquanto Scala combina programação orientada a objetos com programação funcional. Existem também vários recursos de linguagem convenientes em Scala (como inferência de tipo, conversões de tipo implícitas e para compreensões) que podem tornar o código Scala muito mais conciso e expressivo do que Java. É fácil se acostumar a escrever coisas no “modo Scala”, o que pode ser um pouco problemático se você não for disciplinado quando voltar a escrever Java em sistemas legados. Por exemplo, não é incomum para mim escrever algo semelhante ao seguinte em Scala: É fácil se acostumar a escrever coisas no “modo Scala”, o que pode ser um pouco problemático se você não for disciplinado quando voltar a escrever Java em sistemas legados. Por exemplo, não é incomum para mim escrever algo semelhante ao seguinte em Scala: É fácil se acostumar a escrever coisas no “modo Scala”, o que pode ser um pouco problemático se você não for disciplinado quando voltar a escrever Java em sistemas legados. Por exemplo, não é incomum para mim escrever algo semelhante ao seguinte em Scala:

val activeUserIds = userService.getByGroupId(12).filter(_.isActive).map(_.id)

Uma explicação detalhada desse código é que ele obtém uma coleção de usuários para o grupo id = 12, filtra aqueles que estão inativos e atribui seus ids ao valor activeUserIds. Se assumirmos que a propriedade id do usuário é do tipo Int, então o tipo resultante do valor deve ser Seq [Int], uma sequência de inteiros Scala. Em Java, o código geralmente é bem diferente. Em um estilo Java imperativo, você provavelmente está acostumado a escrever algo assim:

Collection<Integer> activeUserIds = new ArrayList<Integer>();

for (User user : userService.getByGroupId(12)) {
if (user.isActive()) {
activeUserIds
.add(user.getId());
}
}

É minha opinião que, apesar de seus instintos, escrever seu código dessa maneira é melhor do que tentar adaptar expressões funcionais em seu código Java. Ao voltar ao Java depois de algumas semanas em Scala, minha reação inicial foi tentar e se apropriar das funções de mapa e filtro às quais eu estava acostumado na programação funcional. A biblioteca Guava do Google faz um ótimo trabalho em fornecer esse tipo de funcionalidade. Os objetos Predicate e Function descrevem os lambdas usados ​​acima para especificar os critérios de mapeamento e filtragem dos valores, e há métodos estáticos de transformação Collections2 # filter e Collections2 # que podem aplicá-los às coleções. Usando Guava, escrever o código Java em um estilo funcional seria algo assim:

Collection<Integer> activeUserIds = Collections2.transform(
Collections2.filter(userService.getByGroupId(12),
new Predicate<User>() {
@Override
public boolean apply(User user) {
return user.isActive();
}
}),
new Function<User, Integer>() {
@Override
public Integer apply(User user) {
return user.getId();
}
});

O código Java é muito mais detalhado do que o código Scala e também mais detalhado do que o código Java imperativo. Isso ocorre principalmente porque a linguagem não tem definições de objeto de primeira classe para expressões lambda. Como resultado, _.isActive em nosso Scala precisa ser expresso como

new Predicate<User>() {
@Override
public boolean apply(User user) {
return user.isActive();
}
}

em nosso Java correspondente. As informações sintáticas adicionadas fornecem pouco valor e demonstram a falta de suporte da linguagem para idiomas de programação de estilo funcional. Um grande problema com esse código também é que é difícil entender exatamente o que está acontecendo; as declarações de classe anônimas para as funções lambda usadas no mapeamento e na filtragem são todas amontoadas na mesma expressão. Você pode contornar isso criando variáveis ​​de instância locais para mantê-los e, em seguida, colocando as referências nas chamadas para Collections2 # filter e Collections2 # transform:

Predicate<User> activeUserPredicate = new Predicate<User>() {
@Override
public boolean apply(User user) {
return user.isActive();
}
};

Function<User, Integer> userToUserIdFunction = new Function<User, Integer>() {
@Override
public Integer apply(User user) {
return user.getId();
}
};

Collection<Integer> activeUserIds = Collections2.transform(Collections2.filter(userService.getByGroupId(12), activeUserPredicate), userToUserIdFunction);

A leitura é um pouco melhor do que antes, mas ainda não é perfeita. O problema de ter um método estático para aplicar filtros e mapas na classe Collections2 significa que precisamos passar as coleções por meio de chamadas de função aninhadas. Acima, não analisamos naturalmente se o filtro ocorre antes da transformação, porque o código não é lido da esquerda para a direita como faz este fragmento do código Scala:

users.filter(_.isActive).map(_.id)

Aqui é muito mais óbvio que o filtro ocorre antes do mapa. É possível que pudéssemos resolver esse problema incluindo funções de filtro e mapa em uma classe de utilitário Collection:

public class FunctionalCollection<T> {

private Collection<T> collection;

public FunctionalCollection(Collection<T> collection) {
this.collection = collection;
}

public FunctionalCollection<T> filter(Predicate<T> predicate) {
Collection<T> filteredCollection = new ArrayList<T>(collection.size());
for (T element : collection) {
if (predicate.apply(element)) {
filteredCollection
.add(element);
}
}
return new FunctionalCollection<T>(filteredCollection);
}

public <R> FunctionalCollection<R> map(Function<T, R> function) {
Collection<R> mappedCollection = new ArrayList<R>(collection.size());
for (T element : collection) {
mappedCollection
.add(function.apply(element));
}
return new FunctionalCollection<R>(mappedCollection);
}

public Collection<T> getCollection() {
return collection;
}
}

utilizando isso, a sintaxe se torna:

Collection<Integer> activeUserIds =
new FunctionalCollection<User>(userService.getByGroupId(12))
.filter(activeUserPredicate)
.map(userToUserIdFunction)
.getCollection();

A leitura é um pouco melhor, mas ainda temos o problema de que a lógica para as funções de filtro e mapa ainda está oculta nas variáveis ​​de instância Function e Predicate e seu comportamento não é óbvio no código, a menos que escolhamos nomes de variáveis ​​expressivos. Infelizmente, esse não é o único problema. A equipe do Guava realmente recomenda NÃO usar as transformações de mapa e filtro, a menos que você tenha absoluta certeza de que a versão funcional resulta em menos linhas de código em geral e não tem impacto no desempenho (referência: http://code.google.com/ p / goiaba-bibliotecas / wiki / FunctionalExplained )

Então, onde isso nos deixa? Infelizmente, isso significa que estamos de volta à estaca zero. Você não deve tentar impor paradigmas funcionais ao código Java, a menos que tenha ganhos de desempenho demonstráveis. Usar o estilo imperativo também é um pouco melhor porque é Java idiomático, o que significa que deve ser lido por qualquer desenvolvedor Java competente. Infelizmente, Java como linguagem não foi escrito com a programação funcional em mente. Embora você possa querer aplicar impulsivamente práticas de programação funcional ao seu código Java, não é uma boa ideia.