Scala: faz barba, cabelo e bigode

July 21, 2009

Aprender Scala está na minha TODO list faz um bom tempo. Finalmente tive algum tempo umas semanas atrás pra dar uma olhada na linguagem. A motivação veio de diversas fontes. No laboratório onde trabalho nós estamos tendendo à usar Scala nos nossos desenvolvimentos futuros, devido a integração de elementos funcionais que facilitam a implementação de algumas partes do código; a minha pesquisa usa uma boa pitada de linguagens específicas de domínio (domain specific languages, DSLs) e Scala parece adequada para o desenvolvimento de embedded DSLs (por exemplo, veja essa DSL para SWT em Scala); e finalmente, tivemos uma palestra sobre tagless interpreters, que haviam sido implementados em Haskell e OCaml, e eu queria ver como implementar um interpretador de cálculo lambda em Scala.

Bem, não importa se você não sabe o que são tagless interpreters ou se não se importa com lambda calculus, o foco desse post é outro. Quando comecei a aprender Scala, eu vi que a linguagem realmente faz jus ao nome (Scala vem de SCAlable LAnguage, e o objetivo era ter uma linguagem que serve pra fazer desde pequenos scripts até grandes aplicações enterprise). Basta olhar a lista de features da linguagem que você percebe que Scala faz barba, cabelo e bigode!

Como não faz sentido discorrer sobre todas as features de Scala (isso merece um livro), eu vou me concentrar em duas pequenas coisas que deixam qualquer programador Java com inveja: Local Type Inference e Traits.

Local Type Inference (Inferência Local de Tipos)

Quantas vezes eu odiei ter que escrever tipos redundantes em Java (que já expliquei em alguns comentários anteriores). Imagine um método que devolve uma lista inicializada com um inteiro (é um exemplo estúpido, mas é só pra mostrar type inference in action). public List<Integer> getList() { List<Integer> list = new ArrayList<Integer>(); list.add(0); return list; }

Scala é staticamente tipada, o que significa que todos os tipos tem que ser verificados pelo compilador. Mas o compilador de Scala tenta ao máximo inferir o tipo que você está indicando. Você só precisa escrever o tipo se o compilador não conseguir determinar qual é o tipo correto. O mesmo código em Scala ficaria mais ou menos assim (Note que sou bem novato em Scala, então me perdoe se existem soluções mais elegantes): def getList() = 0 :: Nil

O código ficou pequeno não somente por inferência de tipos, mas vamos ver como que inferência ajudou (o que segue é como eu imagino o compilador pensando). Primeiro o compilador infere que Nil é uma lista vazia, então pode ter qualquer tipo. O “::” é na verdade uma chamada de método (o add) na lista Nil, passando zero (0) como parâmetro (em Scala, métodos que iniciam com “:” fazem bind à direita, então Nil – que é uma case class de List – é quem recebe o método). O compilador vê que 0 é um Int (o equivalente de Integer e int em Scala), então o resultado de chamar add em Nil é uma lista de inteiros (ou List[Int]). Finalmente, o compilador vê que essa lista é retornada pelo método, então o tipo do método é List[Int].

Com isso, ao invés de escrever o tipo de lista em 3 lugares diferentes, você não precisou escrever em lugar algum, pois o tipo foi inferido do conteúdo e essa informação foi propagada. Note que se a lista estivesse vazia, você teria que escrever pelo menos 1 vez qual é o tipo (pois não tem como inferir do conteúdo), mas é melhor que 3 vezes!

Traits

Traits são basicamente interfaces com implementação. Já é sabido faz um bom tempo que herança múltipla (multiple inheritance, MI) causa dores de cabeça enormes (o Google, por exemplo, proíbe uso de MI no desenvolvimento interno de aplicações C++). A resposta de Java foi o conceito de interfaces, que te dão um poder parecido com MI (porque uma classe pode estender diversas interfaces, então pode ser usada polimorficamente no lugar de qualquer uma delas), mas muito mais restrito (porque a classe tem que implementar os métodos de todas as interfaces).

Em Scala, as Traits são como interfaces, mas elas podem ser parcialmente implementadas, normalmente com código default. Isso resolve alguns problemas de interfaces. Em Java existem várias interfaces pra receber eventos de GUIs que contém diversos métodos (um pra cada tipo de evento, veja MouseListener por exemplo). Quando você só tá interessado em um tipo de evento (por exemplo, Mouse Pressed), você tem que implementar todos os métodos (normalmente deixando em branco). Se você tiver sorte e sua classe não estende nenhuma outra, você pode usar um dos adapters com implementações em branco (como MouseAdapter), mas não é sempre o caso. Outro caso típico é quando um dos métodos pode ser derivado dos outros, como no exemplo direto do tour de Scala: trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }

Nesse caso, classes precisam somente implementar isSimilar pois elas já ganham de graça o isNotSimilar. Note que traits, como interfaces, não podem ser instanciadas, e têm que ser adicionadas a classes ou objetos.

E agora?

Eu dei aqui só uma mostra do que Scala tem, e na verdade não cheguei nem perto da parte que eu considero mais importante, que é a junção (bem elegante por sinal) dos paradigmas funcional e orientado a objetos. Inclusive, umas semanas atrás eu fui à uma palestra do Simon Peyton-Jones, que é um dos caras por trás de Haskell. Depois da apresentação eu conversei (por ótimos 2 minutos) com ele sobre Scala e ele se disse feliz que Scala está trazendo linguagens funcionais pra desenvolvedores que estariam presos em OO (tanto por instrução – o que se aprende nas universidades em geral – quanto pelo mercado de trabalho). Eu acho que ele ainda prefere Haskell, mas Scala introduz “desenvolvedores comuns” ao paradigma funcional, sem abdicar de ser prática (não que Haskell não seja, mas o fato de se poder chamar código Java de Scala faz com que a adoção seja bem mais fácil).

O que me pergunto é se “desenvolvedores comuns” vão conseguir entender o que Scala oferece e usar de forma eficiente (isso depois que surgirem “Scala Design Patterns”). Sempre ouço que na indústria o average Joe mal entende Java direito, imagina então Scala. Estaríamos chegando em uma era em que uns poucos desenvolvedores excepcionais, usando linguagens e ferramentas ultra eficientes, seriam capazes de desenvolver “qualquer” sistema e suprir a demanda do mercado? Ou seja, o mercado encolheria em termos de número de programadores, mas os que restarem teriam uma produtividade muito superior?

Não sei. Mas sei que Scala faz barba, cabelo e bigode. E se bobear, tem algum jeito daquele pelinho no fundo da orelha ir junto!

posted in Desenvolvimento, Ferramentas, Formação by Thiago Bartolomei

Follow comments via the RSS Feed | Leave a comment | Trackback URL

  • http://rnaufal.livejournal.com Rafael Naufal

    Thiago,

    legal sua visão geral sobre Scala. Comecei meus estudos nela há pouco tempo tb e estou achando que vale mto a pena, justamente por suportar os paradigmas OO e funcional e proporcionar as vantagens dos dois mundos.

    BTW, o método em Java que inicializa uma lista com um inteiro poderia ficar assim, utilizando uma alternativa do Double Brace Initialization:

    public List getList() { return Arrays.asList(0); }

    Há um tempo atrás utilizei Scala e um pouco de sua inferência de tipos local para atualizar alguns posts no LiveJournal. A experiência foi bem interessante: http://rnaufal.livejournal.com/21132.html

  • Thiago Bartolomei

    É, você tem razão, talvez o exemplo não tenha sido o melhor (eu confesso que não pensei muito sobre ele, simplesmente usei o que me veio à cabeça na hora). Uma pequena correção no seu código é que você teria que colocar o tipo de lista no retorno do método: public List getList() { return Arrays.asList(0); }

    Mas a idéia desse exemplo é quando você tem algum conteúdo pra derivar o tipo da lista. Se não tivesse conteúdo, não teria escapatória, você teria que escrever o tipo 1 vez em Scala e 2 (ou 3, dependendo do que você vai fazer com a lista) em Java.

    E obrigado pelo link pro idioma de double braces, eu realmente não conhecia. Mas como o link mostra, existem vários problemas com esse idioma (equality, iniciar o objeto antes do construtor, final classes, etc).

  • http://bpfurtado.net Bruno Patini Furtado

    Gostei do post! :)

    Só que tem algo que me incomoda muito quando se fala de Scala, que os tais ‘traits’ são interfaces com implementação (!).

    Muitos, mas muitos mesmo, se esquecem que o diferencial da implementação de traits (e por consequência mixins) em Scala é que eles sugerem uma maneira alternativa para o Method Resolution Order. Que em Scala, até onde eu li (mas não experimentei) deve estar explicito no código, a diferença de outras linguagens onde isto é feito automaticamente por algoritmos muitas vezes complexos.

  • Thiago Bartolomei

    Hehe, por isso eu coloquei a expressão “basicamente” interfaces com implementação, pois eu sei que isso não é inteiramente verdade.

    AFAIK, Scala usa o mesmo esquema de CaesarJ, que é Mixin Linearization. A hierarquia de herança é um grafo direto acíclico, então pra decidir em que ordem fazer lookup de um membro você precisa linearizar. Em C existe um algoritmo estranhíssimo que leva em consideração coisas como se o método é declarado virtual ou não. Em Scala e CaesarJ a ordem é definida pela ordem com que você inclui as traits (ou cclasses em CaesarJ). Por exemplo A extends B with C é diferente de A extends C with B.

    Como o texto que você citou diz, traits em Scala são quase tão poderosas quanto multiple inheritance. Mas acho que o fato do algoritmo de linearização ser mais simples faz com que traits sejam mais fáceis de manter e entender do que multiple inheritance como feita em C .

  • http://rnaufal.livejournal.com Rafael Naufal

    Eu queria mesmo é entender a grande vantagem de “traits” em Scala, pois eu poderia transpor o seu exemplo de “trait” da seguinte forma:

        protected final boolean isNotSimilar(Object x)
        {
            return !isSimilar(x);
        }
    
        protected abstract boolean isSimilar(Object x);
    

    Dessa forma eu repasso para as subclasses a implementação de isSimilar(x) e isNotSimilar(x) faz uso da implementação provida pelas subclasses. Além do que classes abstratas também não podem ser instanciadas, da mesma forma que as “traits”. Sei que é um exemplo muito simples e as “traits” devem ser mto mais poderosas que isso mas aí eu pergunto: o grande benefício de traits é permitir a MI, que não existe em Java?

  • Thiago Bartolomei

    No meu entender, existem 2 vantagens básicas em traits:

    1) “interfaces com implementação”: é o caso que você tá mostrando. Em Java você pode fazer essa trait como classe abstrata, mas daí todas as classes que querem ter essa trait tem que estender a classe abstrata. Isso é ruim porque em Java classes só podem estender 1 outra classe. Suponha que exista uma outra trait de “Ordenação” que você também quer adicionar à sua classe… não dá.

    Então nesse caso é realmente pra permitir casos simples de MI. Aquele link que o Bruno mandou traz uma discussão boa sobre isso, e inclusive argumenta que MI em Scala ainda é poderosa demais, o que pode trazer problemas na manutenção de grandes sistemas.

    2) “super classe variável”: em traits você não sabe qual é a super classe de fato na hora da declaração, mas somente na hora da referência. Por exemplo: trait A {} trait B extends A {} trait C extends A {} object D extends B with C {}

    Nesse caso, o objeto D (que pode ser visto mais ou menos como uma classe somente com membros estáticos em Java) tem a seguinte hierarquia: D -> C -> B -> A. Como você pode ver, C antes estendia A, mas agora entrou B no meio (por mixin linearization).

    Qual a vantagem disso? O que acontece é que chamadas usando “super” não são estáticas mais, pois o tipo de “super” não é sabido na declaração da trait. Em java, se você fizer: abstract class C extends A { public void m () { super.m(); } } você sabe com certeza que vai ser chamado o método na classe A. Em traits, poderia ser o método m() da classe B (se a trait for aplicada como no exemplo). A vantagem disso é que você pode colocar algum processamento em C que aplica à qualquer tipo que estenda A (eu acho meio parecido com around advice de AOP). Um exemplo simplificado tirado da referência de Scala: abstract class Table { def get(key: Int): // define o tipo } class ListTable { def get(key: Int): // retorna de uma lista } trait SynchronizedTable { abstract override def get(key: Int) = synchronized { super.get(key) } } object MyTable extends ListTable with SynchronizedTable

    Dessa forma, a synchronized table adiciona sincronização, independente do tipo de tabela.

    Traits também são usadas pra outras coisas mais avançadas, como Function Types e Type Constructors. Confesso que tenho somente uma vaga idéia de como isso funciona em Scala e não sei exatamente quanto é dependente de traits, mas espero descobrir essas coisas em breve, assim que sobrar um tempo ;-)

blog comments powered by Disqus

Switch to our mobile site

 
Powered by Wordpress and MySQL. Theme by Shlomi Noach, openark.org