BT

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

Contribuir

Tópicos

Escolha a região

Início Artigos Scala ou Java? Explorando mitos, polêmicas e fatos

Scala ou Java? Explorando mitos, polêmicas e fatos

A popularização da linguagem de programação Scala, notável pela abundância de opiniões e críticas em blogs e redes sociais (como esta de Nikita Ivanov da GridGain e o popular caso da empresa Yammer), aumentaram muito a quantidade de informações disponíveis sobre a linguagem. Entretanto, a qualidade destas informações muitas vezes deixa a desejar.

Independente de estas opiniões serem favoráveis ou contrárias ao Scala, muitas vezes elas contêm afirmações desatualizadas, superficiais ou tendenciosas. O objetivo deste artigo é auxiliar àqueles que estão aprendendo ou avaliando a linguagem a chegarem em suas próprias conclusões. Foram selecionadas as questões mais comuns sobre a linguagem e seu ambiente e, em cada uma delas, adicionados esclarecimentos e links, favorecendo a formação de uma opinião mais embasada ou uma avaliação mais correta.

Scala é uma linguagem compilada, projetada para rodar em um ambiente gerenciado como a JVM ou o CLR do .NET e que oferece a união dos paradigmas funcional e orientado a objetos, principalmente na plataforma Java. O suporte à programação funcional e a presença de um compilador moderno permitem que Scala tenha um sistema de tipos verificados em tempo de compilação, como em Java, mas com a expressividade e sintaxe semelhante à de linguagens interpretadas, como Groovy ou Ruby. Entretanto, as funcionalidades avançadas de Scala também podem gerar problemas de desempenho e de complexidade.

Este artigo apresenta os pontos onde esse equilíbrio precisa ser considerado e não requer conhecimento de Scala (mas não é uma introdução ou tutorial da linguagem).

Scala é mais produtivo que Java?

Como a produtividade de desenvolvimento é um assunto muito subjetivo, é necessário decompor esta questão, avaliando as funcionalidades de Scala que normalmente a suportam.

Programação Funcional

O paradigma funcional de programação busca expressar um programa como funções em seu sentido matemático, ou seja, mapeando de um valor para outro (f(x) = y) sem efeitos colaterais, como manutenção de estado de objetos ou entrada e saída de dados. Isso permite que diversas otimizações sejam realizadas em tempo de compilação e evita problemas de concorrência, como será apresentado ao longo deste artigo.

A programação funcional é baseada no Cálculo Lambda, criado em 1930, e há muito tempo faz parte do ambiente acadêmico em linguagens como Lisp, Scheme e Haskell. É uma tendência das linguagens comerciais, como Java e C#, incorporarem funcionalidades destas linguagens, principalmente Closures, mas este esforço é limitado pela compatibilidade com o legado, processos de especificação e conflitos de interesse. Isso abre espaço para que sejam desenvolvidas novas linguagens multiparadigma, buscando o melhor dos dois mundos, como Clojure e Scala.

Funcionalidades como valores imutáveis, coleções, funções de alta ordem e casamento de padrões incentivam os desenvolvedores Scala a programarem no estilo funcional. Isso pode tornar mais produtivos os desenvolvedores familiares com este paradigma, mas também pode alienar aqueles que o desconhecem. Por exemplo, um programa que imprime o milésimo elemento da Sequência de Fibonacci pode ser escrito da seguinte forma em Java:

import java.math.BigInteger;

public class FiboJava {
  private static BigInteger fibo(int x) {
    BigInteger a = BigInteger.ZERO;
    BigInteger b = BigInteger.ONE;
    BigInteger c = BigInteger.ZERO;
    for (int i = 0; i < x; i++) {
      c = a.add(b);
      a = b;
      b = c;
    }
    return a;
  }

  public static void main(String args[]) {
    System.out.println(fibo(1000));
  }
}

Traduzindo este código literalmente pra Scala teriamos:

object FiboScala extends App {
  def fibo(x: Int): BigInt = {
    var a: BigInt = 0
    var b: BigInt = 1
    var c: BigInt = 0
    for (_ <- 1 to x) {
      c = a + b
      a = b
      b = c
    }
    return a
  }

  println(fibo(1000))
}

Estas são as implementações iterativas do algoritmo em Java e Scala, notavelmente similares. Entretanto, uma versão mais compacta e funcional utilizando sequências infinitas e tuplas pode ficar muito diferente:

object FiboFunctional extends App {
  val fibs:Stream[BigInt] =
    0 #:: 
    1 #:: 
    (fibs zip fibs.tail).map{ case (a,b) => a+b }
  println(fibs(1000))
}

Esta versão pode ser considerada concisa ou complexa demais, de acordo com suas habilidades e preferências. A programação funcional não é a única fonte de complexidade em Scala, como será mostrado no decorrer deste artigo, mas é relevante na diferença em produtividade.

Para aprender mais sobre programação funcional em Scala, este video de Nathan Hamblen apresenta o paradigma em geral e este outro, de Daniel Spiewak, vai um pouco além, implementando algumas estruturas de dados funcionais. Os tutoriais da documentação oficial e os publicados pela equipe do Twitter também são frequentemente recomendados.

Menos código repetitivo

Algumas funcionalidades de Scala, tais como Inferência de Tipos, Exceções Não-Verificadas, Objetos Opcionais e Conversão Implícita, podem reduzir muito a quantidade de declarações e verificações necessárias para compilar e executar um programa sem modificar seu significado. Além disso, Scala tenta ser mais enxuta que Java, removendo as funcionalidades que podem ser expressas em termos de outras. Por exemplo, em Scala não existem referências static ou tipos primitivos, pois as mesmas funcionalidades podem ser obtidas usando apenas objetos.

Para entender a diferença na quantidade de código repetitivo, considere este programa que imprime os endereços MAC de todas interfaces de rede do sistema. Existem diversas formas de escrever este código, mas essa é a maneira como ele provavelmente seria escrito em cada linguagem.

Em Java:

import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Collections;
import java.util.Enumeration;

public class ListMACsJava {
  public static void main(String[] args) throws SocketException {
    Enumeration<NetworkInterface> nics = 
        NetworkInterface.getNetworkInterfaces();
    for (NetworkInterface nic : Collections.list(nics)) {
      byte[] mac = nic.getHardwareAddress();
      for (int i = 0; mac != null && i < mac.length; i++) {
        System.out.format("%2x", mac[i]);
        System.out.print(i == mac.length - 1 ? '\n' : ':');
      }
    }
  }
}

Em Scala:

import java.net.NetworkInterface
import scala.collection.JavaConversions._

object ListMACsScala {
  def main(args: Array[String]) {
    NetworkInterface
      .getNetworkInterfaces
      .flatMap(nic => Option(nic.getHardwareAddress))
      .map(_ map ("%02x" format _) mkString ":")
      .foreach(println(_))
  }
}

Outra implementação em Scala usando compreensão de sequencias and conversões explicitas seria:

import collection.JavaConverters._
import java.net.NetworkInterface

object ListMACsForScala extends App {
  val nicaddresses = for {
    nic <- NetworkInterface.getNetworkInterfaces.asScala
    addrbytes <- Option(nic.getHardwareAddress)
  } yield {
    addrbytes map { "%02x" format _ } mkString ":"
  }
  nicaddresses foreach println
}

Neste exemplo, as seguintes diferenças são relevantes na eliminação de código:

  • Como métodos em Scala são por padrão public e o método main usa a sintaxe de procedures, que por padrão retorna void, essas declarações podem ser omitidas.
  • Scala não força a verificação de exceções, então o bloco try-catch pode ser omitido.
  • As operações na lista podem ser encadeadas fluentemente, sem necessidade de adaptar a API subjacente ou usar bibliotecas adicionais.
  • A classe Option previne exceções NullPointerException e elimina a verificação por valores null.

Outras funcionalidades, além das apresentadas neste exemplo, podem contribuir com a redução de código, por exemplo a Conversão Implícita e Closures. Apesar destas funcionalidades não existirem em Java, algumas delas podem ser simuladas ou adaptadas, como fazem as APIs fluentes.

Traits

Traits são definições de tipos, semelhante à interfaces, mas que podem conter implementação e são acumuláveis, promovendo o reuso de uma maneira diferente das disponíveis em Java. Um Trait se assemelha à combinação de uma classe abstrata e uma interface em uma única definição. O exemplo a seguir, retirado do livro "Programming in Scala", mostra como implementar uma fila de números inteiros e combinar traits para dobrar, incrementar e filtrar:

import scala.collection.mutable.ArrayBuffer

//Type Definition
abstract class IntQueue {
  def get(): Int
  def put(x: Int)
  def size(): Int
}

//ArrayBuffer implementation
class IntQueueImpl extends IntQueue {
  private val buf = new ArrayBuffer[Int]
  def get = buf remove 0
  def put(x: Int) { buf += x }
  def size = buf length
}

trait Doubling extends IntQueue {
  abstract override def put(x: Int) {
    super.put(2 * x)
  }
}

trait Incrementing extends IntQueue {
  abstract override def put(x: Int) {
    super.put(x + 1)
  }
}

trait Filtering extends IntQueue {
  abstract override def put(x: Int) {
    if (x > 0) super.put(x)
  }
}

Depois de definidos, os traits podem ser "misturados" tanto durante a definição de tipos como na construção de objetos:

//Mixing traits in type definition
class DoublePlusOneQueue extends IntQueueImpl with Incrementing with Doubling

object QueueWithTraits {
  def main(args: Array[String]) {
    val queue1 = new DoublePlusOneQueue
    queue1 put 1
    queue1 put 2
    println(queue1 get)
    println(queue1 get)

    //Mixing traits in object instantiation
    val queue2 = new IntQueueImpl with Filtering
    queue2 put -1
    queue2 put 1
    println(queue2 size)
  }
}

Código Scala é complexo?

As mesmas funcionalidades que podem tornar Scala mais produtiva também podem torná-la ilegível, como por exemplo no uso da programação funcional. Uma funcionalidade que é frequentemente questionada é o uso de simbolos em nomes de métodos, como o método ++ na classe List ou o método /:\ da classe Stream. Estes métodos com nomes de operadores podem ser úteis para representar, por exemplo, concatenação para listas ou adição para números complexos. Entretanto, esta nomeclatura pode ser facilmente abusada. Quando combinada com limites de tipos, variância e funções parciais, temas complexos em qualquer linguagem, as declarações podem se tornar bem difíceis, como esta mencionada no post "Opinião: Scala é o novo EJB 2?":

def + + [B>: A, That] (that: TraversableOnce [B]) (implicit bf: CanBuildFrom [List [A], B, That]): That

Outra dificuldade frequente é que o ScalaDocs, a documentação oficial da biblioteca de classes Scala, é incompleta em muitos pontos. Entretanto, existe um esforço de melhoria na documentação da biblioteca e outros recursos para aprendizado, que estão sendo agrupados em um site de documentação.

Isso não quer dizer que não existe complexidade inerente da linguagem Scala. No artigo "True Scala Complexity", Yang Zhang apresenta um exemplo detalhado de como o sistema de tipos e conversões podem ficar confusos até mesmo para um desenvolvedor Scala experiente. Algumas destas complexidades podem ser evitadas, aliviadas ou eliminadas no futuro, mas outras devem permanecer, como as que resultam da integração de um sistema de tipos expressivo e unificado entre os paradigmas funcional e orientado a objetos.

Scala oferece melhor suporte à concorrência que Java?

Escrever programas concorrentes eficientes e corretos é difícil. Depurar estes programas pode ser ainda mais desafiador e imprevisível. Scala oferece paralelismo em alto nível de abstração, mas o Java também o faz, particularmente com as funcionalidades de concorrência incorporadas no Java 7. Apesar das funcionalidades de concorrência das duas linguagens serem semelhantes em seu propósito, elas são muito diferentes em sua arquitetura e uma comparação completa vai além do escopo deste artigo. Entretanto, esta questão se torna mais clara quando analisadas as funcionalidades de Scala que a suportam. Nas palavras do criador da linguagem, Martin Odersky:

"Infelizmente, programadores têm encontrado dificuldade em construir aplicações robustas com muitas threads usando o modelo de dados compartilhados e travas, particularmente quando tais aplicações crescem em tamanho e complexidade. O problema é que em cada ponto do programa, deve-se considerar quais dados estão sendo modificados ou acessados por outras threads e quais travas estão sendo mantidas."

Programming in Scala, Chapter 32

Scala tenta reduzir estas dificuldades baseando suas APIs nos conceitos de imutabilidade e de atores. Além disso, o uso da programação funcional pelas bibliotecas do Scala facilita a exploração do paralelismo interno ou automatizado pelo compilador ou pelas próprias bibliotecas. A proposta de Closures para o Java 8 tenta incorporar algumas destas características na linguagem Java e suas bibliotecas.

Imutabilidade

Sincronizar o acesso à objetos mutáveis compartilhados pode resultar em bastante complexidade no uso de primitivas de concorrência (travas, semáforos, etc). Apesar desta não ser uma preocupação comum para desenvolvedores de aplicativos, pode ser algo pesado para desenvolvedores de servidores e frameworks. Scala tenta suavizar este problema utilizando objetos imutáveis e funções puras. Se um objeto é imutável, ele pode ser compartilhado ou copiado sem se preocupar com quem o está utilizando, ou seja, valores imutáveis são naturalmente "thread-safe".

Ao contrário de outras linguagens funcionais, Scala não exige que objetos sejam imutáveis. Objetos mutáveis são importantes para implementar diversos requisitos, funcionais e não-funcionais. O que Scala faz é incentivar a distinção entre objetos mutáveis e imutáveis usando pacotes e declarações diferentes para cada caso.

Esta palestra, de Rich Hickey, apresenta as principais idéias por trás da imutabilidade e algumas considerações sobre como programar com ela.

Atores

Os controles de paralelismo de baixo nível, como travas e blocos synchronized, são suficientes para escrever programas concorrentes corretamente - mas esta tarefa pode não ser simples. Para escrever este tipo de programa mais produtivamente e prevenir defeitos, é desejável um controle de concorrência de alto nível, como Fork/Join, Software Transactional Memory ou, como é comum em Scala, o Modelo de Atores. Neste modelo, o paralelismo é expressado como atores respondendo à mensagens, ao invés de travar e liberar threads.

O exemplo a seguir demonstra o modelo de atores para estimar o valor de Pi usando o método de Monte Carlo. Este método estima o valor de Pi gerando pontos aleatórios em um quadrado e verificando a proporção deles que caem dentro do circulo nele inscrito. Neste exemplo, será usado um padrão recorrente no modelo de atores, que é ter um ator "coordenador" gerenciando diversos atores "trabalhadores" que, de maneira isolada ou cooperativa, progridem rumo ao resultado.

O ator "calculador" nesse exemplo poderá receber duas mensagens: "Calcule", à qual responde com uma estimativa de Pi e "Pare", que encerra o ator. Como essas mensagens são simples notificações, podem ser repesentadas por case objects, que seriam semelhantes a constantes em linguagens procedurais:

import scala.util.Random
import Math._
import System._
import scala.actors.Actor
import scala.actors.Actor._

case object Calculate
case object ShutDown

class Calculator extends Actor {
  val rand = new Random
  var pi, in, cnt = 1.0

  def act {
    while (true) {
      receive {
        case Calculate =>
          sender ! estimativeOfPi
        case ShutDown => exit
      }
    }
  }

  def estimativeOfPi: Double = {
    val x = rand.nextDouble - 0.5
    val y = rand.nextDouble - 0.5
    cnt += 1.0
    if (sqrt(x * x + y * y) < 0.5) in += 1
    in / cnt * 4
  }
}

O ator "coordenador" inicia uma lista de atores calculadores e lhes envia mensagens "Calcule" até que algum deles produza uma estimativa precisa o suficiente e, neste caso, encerra o calculo e imprime o valor encontrado e o tempo de execução:

class Coordinator(numOfCalculators: Int) extends Actor {
  def act {
    val startedAt = currentTimeMillis
    var calculators = List.fill(numOfCalculators)(new Calculator)
    calculators.foreach(c => {
      c.start
      c ! Calculate
    })
    while (true) {
      receive {
        case estimative: Double =>
          val error = abs(Pi - estimative)
          if (error > 0.0000001)
            sender ! Calculate
          else {
            val tempo = currentTimeMillis - startedAt
            calculators.foreach(_ ! ShutDown)
            println("Pi found by " + sender + " = " + estimative)
            println("Execution time: " + tempo)
            exit
          }
      }
    }
  }
}

Por fim, um objeto com o método "main" inicializa o ator coordenador com o numero de calculadores a serem usados. Quanto maior o numero de coordenadores, maior a probabilidade que algum deles encontre o valor desejado, reduzindo o tempo total de execução.

object PiActors extends App {
  new Coordinator(2) start
}

O modelo de Atores não é utilizável apenas para comunicação entre Threads. A plataforma Akka, por exemplo, estende o modelo para suportar atores remotos e acrescenta diversas outras ferramentas para o desenvolvimento de sistemas distribuido com alta escalabilidade e tolerância a falhas.

Apesar de útil em muitos casos, o modelo de atores e sua implementação em Scala também tem suas controvérsias. Grande parte dos benefícios do modelo não está nos atores em si, mas na troca de mensagens, que normalmente são imutáveis e favorecem a ausência de estado mutável compartilhado. A implementação padrão em Scala, demonstrada no exemplo acima, vincula cada ator a uma Thread nativa, o que pode ser problemático, como mostrado no post "Why I Don't Like Scala", de Tony Arcieri. Existem implementações alternativas, usando atores baseados em eventos por exemplo, mas estas sempre vêm à algum custo, no mínimo o de complexidade.

Coleções Paralelas

A versão 2.9 de Scala incorporou uma biblioteca de coleções paralelas, que torna simples a execução paralela de operações comuns em coleções, tais como map(), filter() e foreach(). Basta invocar o metodo par() para obter uma versão paralela de uma coleção, como mostrado neste exemplo:

object ParCol extends App {
  (1 to 5) foreach println
  (1 to 5).par foreach println
}

Note que a ordem de impressão dos elementos no segundo comando é imprevisível, pois ela depende do agendamento de threads pelo sistema operacional. Aleksandar Prokopec mostra detalhes interessantes de como as coleções paralelas foram implementadas em sua apresentação no Scala Days 2010.

Scala é extensível?

Scala permite que seus desenvolvedores personalizem a aparência e comportamento da linguagem, criem novas linguagens e que alterem o processo de compilação. Tais tarefas podem ser desafiadoras ou até tornar o código ilegível, mas estendem muito as possibilidades da linguagem.

Linguagens de Domínio Específico

Usando classes genéricas, tipos abstratos, funções como objetos, métodos nomeados como operadores, parâmetros nomeados e conversões implícitas, o código Scala pode se transformar em uma linguagem de domínio específico (DSL). Isso é muito útil quando a linguagem é exposta à usuários especialistas no negócio, permitindo que eles personalizem o sistema em tempo de execução, como Debasish Ghosh mostra em sua DSL financeira.

Linguagens de domínio específico também podem ser criadas quando houver necessidade de uma linguagem de programação mais abstrata ou declarativa, aumentando a produtividade de desenvolvimento. O Apache Camel, por exemplo, oferece uma DSL em Scala voltada para configuração de rotas de serviços mais concisas e corretas.

O desenvolvimento de linguagens de domínio específico é um assunto tão profundo quanto popular. Para uma introdução mais detalhada, veja esta apresentação de Martin Fowler, que também escreveu este livro sobre DSLs.

Alterando a compilação

Em scala, pode-se levar o desenvolvimento de linguagens um passo adiante usando parser combinators, que permitem a criação de gramáticas totalmente novas, como mostrado neste artigo de Daniel Spiewak. Quando mesmo isso não é suficiente, ainda é possível recorrer à plugins do compilador para alterar o processo de compilação. Tais plugins podem ser escritos para realizar, por exemplo, análise estática de código e avaliar métricas, como faz o PMD ou o FindBugs. Outra possibilidade seria utilizar plugins do compilador para otimizar ou alterar o comportamento de uma biblioteca em tempo de compilação.

Estas funcionalidades podem fazer com que o código Scala tenha uma aparência muito diferente da original, como mostrado por Michael Fogus em sua implementação da linguagem BASIC em Scala. Estas personalizações da linguagem podem ser utilizadas para resolver problemas complexos de maneira elegante, mas também podem ser abusadas, alienando os desenvolvedores não familiarizados com o contexto das modificações.

Scala e Java são interoperáveis?

Apesar de ser possível invocar métodos Java em código Scala e vice-versa, a interação entre as linguagens não é sem complicações.

Nas chamadas de métodos Scala a partir de código Java, o desenvolvedor precisa entender como as funcionalidades que não existem no Java são transformadas em classes e objetos executáveis na JVM. Por exemplo, métodos com nomes não alfabéticos, funções como objetos, tuplas, e closures funcionarão quando utilizadas em código java, mas precisam ser escritas de maneira diferente, que Java suporte, como mostrado neste artigo.

Ao invocar Java a partir de Scala, o problema são as funcionalidades do Java que foram abandonadas ou implementadas de maneira diferente em Scala, como Interfaces, Anotações e métodos static. Este artigo mostra mais detalhes sobre estas diferenças e as alternativas para diversos casos, como a classe JavaConversions para converter coleções da biblioteca de uma linguagem para outra. Apesar destas alternativas, algumas diferenças podem forçar o desenvolvedor a escrever código em duas linguagens. Por exemplo, para escrever uma interface ou uma anotação, é necessário fazê-lo Java, uma vez que é possível usar, mas não declarar, estes elementos em Scala.

Apesar do compilador scalac não compilar código java nem invocar o compilador javac para fazê-lo, os arquivos .java são analisados junto com os .scala e seus tipos são verificados simultaneamente. Isso resolve o problema de dependências circulares entre as linguagens, como explicado neste artigo, mas requer que o código java seja compilado separadamente.

O ferramental para Scala é ruim?

Os quinze anos de historia e a força da comunidade Java certamente se refletem na quantidade e maturidade de suas ferramentas, que hoje são muito completas e estáveis. Apesar de mais recentes, as ferramentas para Scala estão amadurecendo, principalmente pelo esforço da Typesafe (empresa por trás da linguagem e diversos projetos relacionados), mas também com muitas contribuições da comunidade, como as do Twitter.

Reuso de Bibliotecas e Frameworks Java

Um dos benefícios de usar uma linguagem executável na JVM é a reutilização das inúmeras bibliotecas e frameworks disponíveis para Java. Scala não é exceção e, considerando as limitações apresentadas na questão de interoperabilidade, pode reutilizar qualquer biblioteca ou framework disponível para Java. Isso inclui servidores de aplicações Java EE (EJB, JSF, JAX-RS, etc) e bibliotecas populares como Hibernate ou JUnit, uma vez que depois de compilado para bytecode (.class), as classes Java e Scala são praticamente indistinguíveis. Por exemplo, um servlet que imprime os parametros do request HTTP pode ser escrito em Scala da seguinte maneira:

import javax.servlet.http._
import javax.servlet.annotation.WebServlet
import scala.collection.JavaConversions._

@WebServlet(Array("/printParams"))
class PrintParametersServlet extends HttpServlet {
  override def doGet(req: HttpServletRequest, resp: HttpServletResponse) {
    val out = resp.getWriter
    req.getParameterMap
      .map { case (key, value) => key + " = " + value.mkString(",") }
      .foreach(out println _)
  }
}

Bibliotecas e Frameworks Scala

O problema em reutilizar as bibliotecas Java é que estas não são projetadas considerando as melhorias de sintaxe de Scala, onde seu uso provavelmente não será idiomático. Em alguns casos isso é apenas uma inconveniência, mas em outros pode requerer justamente o código repetitivo que Scala tenta evitar.

Por exemplo, a classe CollectionUtils da biblioteca Apache Commons Collections possui métodos para filtrar, transformar e iterar sobre coleções, assim como a biblioteca padrão de Scala. Um programa Scala que utilizasse a Commons Collections para imprimir o dobro de cada elemento positivo em uma lista de inteiros pode ser escrito da seguinte maneira:

import org.apache.commons.collections.CollectionUtils._
import org.apache.commons.collections._
import scala.collection.JavaConversions._

object Colecoes extends App {
  val myList = List.range(-5, 5)

  forAllDo(
    collect(
      select(myList,
        new Predicate {
          def evaluate(obj: Object): Boolean =
            return obj.asInstanceOf[Int] > 0
        }),
      new Transformer {
        def transform(obj: Object): Object = {
          return (2 * obj.asInstanceOf[Int])
            .asInstanceOf[Object]
        }
      }),
    new Closure {
      def execute(obj: Object) {
        println(obj)
      }
    })
}

Apesar de funcionar, esse código provavelmente seria considerado muito prolixo ou repetitivo para um desenvolvedor Scala. Entretanto, se usarmos a biblioteca de coleções de Scala, podemos escrever o mesmo programa de maneira muito mais natural para a linguagem:

  myList filter(_ > 0) map(_ * 2) foreach println

Sendo assim, para oferecer um uso mais natural aos desenvolvedores, diversas bibliotecas acabam sendo re-desenvolvidas em Scala. É o caso, por exemplo, da biblioteca Scalaz, que traz estruturas de dados mais adequadas ao desenvolvimento de programas no paradigma funcional. O mesmo acontece para frameworks web (Lift, Play), bibliotecas para TDD e BDD (ScalaTest, Specs) e muitas outras. Algumas destas bibliotecas são muito parecidas com suas versões para Java, enquanto outras são radicalmente diferentes e inspiradas em bibliotecas de outras linguagens, como o Scalatra, baseado na biblioteca Sinatra para Ruby, ou o Scalacheck, baseado na QuickCheck para Haskell.

Ferramentas de Desenvolvimento

Apesar das principais IDEs Java possuírem plugins para Scala (Eclipse, NetBeans e IntelliJ IDEA), suas funcionalidades e maturidade variam muito. Todos oferecem coloração de sintaxe e compilação, mas recursos mais avançados como refatoração, autocomplemento e integração com outras ferramentas (automação de compilação, servidores de aplicação, etc) varia muito. Além disso, a falta de estabilidade e a quantidade de defeitos são críticas comuns aos plugins.

A grande diversidade de ferramentas de automação de compilação para Scala pode tanto positiva quanto confusa. O Simple Build Tool (SBT) é muito usado, praticamente um padrão de mercado. Entretanto, para integrar com outras ferramentas (IDEs, servidores, etc) e suportar o processo de desenvolvimento completo (testes, empacotamento, implantação, etc) ferramentas como o maven, ant ou buildr podem ser mais adequadas, pois já suportam Scala e oferecem maior variedade de integrações e plugins.

Assim como as bibliotecas, muitas as ferramentas para Scala são inspiradas em outras linguagens, além de Java. A mais popular é o console interativo (REPL), tradicional em linguagens de script e muito útil para testar e aprender a linguagem e suas bibliotecas. Como essas ferramentas podem ser estranhas ao desenvolvedor familiarizado com o ambiente Java, a migração do ambiente de desenvolvimento precisa ser analisada em detalhes. Nem todas ferramentas terão um paralelo exato, mas a diferença pode ser benéfica e melhorar o ciclo de desenvolvimento em aspectos diferentes dos tradicionais de Java.

Scala roda tanto em Java quanto em .NET?

Apesar de Scala ter sido projetada para ser independente da maquina virtual subjascente, o alvo sempre foi claramente a JVM. No começo do projeto, em 2004, foi desenvolvida uma versão para .NET que nunca obteve muita popularidade e se tornou obsoleta em pouco tempo. Entretanto, em Junho de 2011 um projeto realizado pela École Polytechnique Fédérale de Lausanne obteve resultados significativos na adaptação de Scala para a plataforma .NET, como explicado neste artigo. Apesar da versão para .NET ainda estar em desenvolvimento e possuir diversas limitações, já é possível rodar programas Scala nas duas plataformas. Para mais informações sobre o Scala na plataforma .NET, veja esta esta entrevista com Martin Odersky e esta página do site principal da linguagem.

A portabilidade para outras plataformas, até mesmo além de Java e .NET, é uma caracteristica interessante de Scala. Um projeto inovador neste sentido é o scala-llvm, uma implementação do compilador Scala para LLVM. Entretanto, essa portabilidade ainda é apenas uma possibilidade, por enquanto Java é a única plataforma que suporta todas as funcionalidades, ferramentas e bibliotecas normalmente esperadas em um ambiente de desenvolvimento corporativo e aplicações em produção.

Scala é lento?

Incrementos no nível de abstração são normalmente seguidos pelas críticas de performance e Scala não é exceção. Em geral, sistemas Java e Scala têm desempenho semelhante, em tempo de execução, já que ambos estão sujeitos aos custos e beneficios da JVM. Já em tempo de compilação, têm muito mais trabalho a fazer comparado com o compilador Java.

Desempenho de execução

As diferenças de desempenho normalmente surgem das funcionalidades de Scala que não são suportadas nativamente pela JVM. Algumas delas, como closures, provavelmente serão suportadas em breve, mas muitas outras nunca serão. Por isso, um código enxuto mas de alto nível de abstração escrito em Scala pode ser compilado para uma grande quantidade de bytecode, prejudicando o desempenho do programa, como mostrado nesta apresentação.

Isso pode ser um problema para programas que dependem de alto desempenho, mas existem contornos. O mais comum é analisar o bytecode gerado utilizando o javap para entender o que está acontecendo por trás dos panos e se familiarizar com o desempenho das funcionalidades e bibliotecas mais usadas. Entender bytecode e ajustar o desempenho em baixo nível não é uma tarefa simples, mas em linguagens funcionais tradicionais, como implementações de Haskell e Scheme, é algo muito mais complexo, quase impossível.

Entretanto, é importante observar que a linguagem é um fator relevante, mas não o único, no desempenho de um sistema. Os benefícios obtidos das utilidades de concorrência e outras funcionalidades podem compensar ou até superar as penalidades da abstração ao se medir a latência e vazão finais.

Desempenho de compilação

Algumas funcionalidades de Scala, como a busca por conversões implícitas, podem levar muito tempo para compilar e o compilador ainda precisa verificar tanto o sistema de tipos de Scala quanto o sistema de tipos da plataforma subjacente (JVM, CLR ou outras). Isso torna o processo de compilação mais lento, mas o faz em benefício de flexibilidade e independência de plataforma.

Usando uma IDE, a compilação incremental alivia estes problemas, mas para compilações complexas ou integração contínua, o desempenho do compilador pode se tornar um problema.

O código gerado pelo compilador Scala não é retrocompatível?

Manter compatibilidade binária é uma escolha difícil para os desenvolvedores de uma linguagem de programação. Se forem estabelecidas regras claras para manutenção de compatibilidade, como a especificação da linguagem Java faz no capítulo 13, então os usuários da linguagem podem esperar que códigos novos sejam compatíveis com binários antigos sem precisar recompilar tudo. Entretanto, com o tempo, manter retrocompatibilidade binária pode tornar a linguagem difícil de evoluir. Trabalhar para melhorar a linguagem e ao mesmo tempo manter essa compatibilidade foi um dos motivos dos atrasos no lançamento do Java 7 e 8.

Por outro lado, não se comprometer com a compatibilidade torna a linguagem mais fácil de evoluir. Isso pode ser muito importante no inicio do desenvolvimento da linguagem, para permitir a alteração de decisões de projeto, correções de defeitos e ajustes de funcionalidades. O problema é que as mudanças significativas, como as vistas do Scala 2.8 para 2.9, podem causar incompatibilidade e requerer que bibliotecas e aplicações sejam recompiladas para a mesma versão da linguagem. Esse problema pode ser suavizado usando métodos ponte, ferramentas de migração, boa documentação e processos, mas não existe uma solução definitiva.

Por este motivo, bibliotecas populares em Scala são distribuídas em diferentes versões, compiladas para diferentes versões da linguagem. Uma versão da biblioteca specs para BDD specs, por exemplo, pode ser encontrada como specs_2.9.0 e specs_2.8.1, para estas respectivas versões de Scala. Para bibliotecas menos populares, pode ser necessário recompilar tudo para a mesma versão da linguagem usada pela aplicação. As ferramentas de automação de compilação podem ajudar muito nesta tarefa. O SBT, por exemplo, pode facilmente compilar cruzado e gerar binários para diversas versões de Scala em uma única execução, assim como referenciar bibliotecas corretamente usando tanto a versão da biblioteca quanto a versão da linguagem.

Conclusões

As vantagens e desvantagens da linguagem Scala muitas vezes são afirmadas em opiniões influenciadas pelo entusiasmo ou pela decepção. Para avaliar a tecnologia adequadamente, é importante entender o contexto e os fatos por trás de tais opiniões, assim como as escolhas do projeto.

Todos os exemplos apresentados nesse artigo estão publicados neste repositório do GitHub. Agradecimentos sinceros aos membros da lista de usuários Scala, que além de manter uma comunidade ativa também contribuiram muito para este artigo. Se você é ou foi um desenvolvedor Scala, contribua com sua opinião nos comentários!

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT