A linguagem de programação Scala

Bem-vindo, este é um livro sobre a linguagem de programação Scala.

Scala é uma linguagem de programação que se diferencia de outras linguagens por incluir orientação a objeto (Object-Oriented) e programação funcional (Functional Programming).

Versão atual

v0.10 2017-06-02

Audiência

Esse livro está sendo escrito para todas as pessoas interessadas sobre a linguagem de programação Scala.

Não é necessário ter experiência em outras linguagens de programação para ler este livro.

O contéudo é voltado para programadores de todos os níveis.

O material mais avançado pode ser ignorado por iniciantes em uma primeira leitura.

Requerimentos básicos

Este livro requer que você tenha instalado em seu computador a linguagem de programação Scala. Além disso, você precisa ter acesso a um terminal e o mínimo de experiência para editar e manipular arquivos texto.

Existem vários tutoriais pela internet que podem te ensinar como instalar scala e scalac em seu computador.

A referência oficial da linguagem pode ser encontrado no website http://scala-lang.org/.

Este livro utiliza a versão 2.12.2 da linguagem.

Observações

Este livro está sendo atualizado constantemente.

Achou algum erro? Tem alguma sugestão? Entre em contato contato via danibberg@gmail.com.

Capítulo 1

Oi Scala!

Neste capítulos vamos introduzir alguns conceitos sobre a linguagem de forma rápida e condensada.

Comecemos rodando um programa clássico.

Digite em seu terminal o comando scala.

Se tudo estiver corretamente instalado em seu computador você deve ver uma mensagem parecida como esta:

Welcome to Scala 2.12.2 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_102).
Type in expressions for evaluation. Or try :help.

scala>

O commando scala, quando executado sem nenhum argumento abre um programa chamado REPL.

REPL significa Read-Eval-Print-Loop. Ou seja, é um ambiente em que você pode experimentar com a linguagem. Dentro do REPL você pode digitar o código diretamente em seu terminal. Seu código será compilado, executado e os resultados impressos na tela de seu computador.

O REPL é uma excelente ferramenta para experimentar com a linguagem e bibliotecas.

Neste capítulo nós vamos utilizar o REPL para fazer uma pequena introdução a linguagem. Nos próximos capítulos não iremos utilizar o REPL e sim salvar o código fonte em arquivos. Vamos mostrar como compilar esses arquivos fonte utilizando o compilador provido pela linguagem e como executar nossos programas.

Agora, o exemplo clássico. Vamos imprimir na tela de seu computador a mensagem Oi, Scala!.

Uma vez dentro do REPL digite o seguinte comando:

print("Oi, Scala!")

Após pressionar a tecla enter, você deve obter o seguinte resultado:

Oi, Scala!

Este é um exemplo bem simples mas que podemos utilizar para introduzir dois conceitos: Standard Library e Aplicação de Funções.

Standard Library

print é uma função que acompanha toda instalação da linguagem. O conjunto de funções, classes, objetos e todo código que acompanha a instalação da linguagem é chamado de Standard Library. Isso significa que a linguagem contém uma biblioteca padrão com um vasto número de funções que podem ser utilizadas para construir o seu programa.

A documentação dessa biblioteca de código está disponível online no endereço http://www.scala-lang.org/api/current/.

Posteriormente vamos aprender como utilizar a documentação provida pela linguagem.

Aplicação de Funções

Em nosso exemplo, após digitar print nós colocamos entre parênteses o argumento, ou seja, o valor que queremos passar para a função print. Neste caso passamos o valor "Oi, Scala!". Note que a mensagem está entre aspas duplas e isso determina que nossa mensagem é do tipo String.

O método print é versátil e aceita diferentes tipos. Por exemplo, podemos imprimir valores de tipo numérico:

print(42)

Neste caso, o tipo do argumento não é uma String e sim um número natural, 42. Scala define esse tipo natural como Int.

Se isso não fizer sentido neste momento não se preocupe. Em capítulos posteriores vamos explicar não só como esses tipos funcionam mas como a mesma função print pode receber diferentes tipos.

O importante nesse momento é entender que quando digitamos o nome de uma função seguido de parantêses nós estamos descrevendo a informação necessária para executar esta mesma função. O valor que passamos dentro do par de parênteses é o argumento que a função espera receber.

Se a função espera receber mais de um argumento nós listamos os valores separados por vírgula. Por exemplo:

foo("Oi", 42)

Nesse exemplo estamos executando a função foo que está recebendo dois argumentos. O primeiro argumento, de tipo String, tem o valor Oi. O segundo argumento que a função recebe tem o tipo Int e o valor 42.

O método foo não está definido na Standard Library. Utilizamos este nome apenas como um exemplo da sintaxe da linguagem. É uma prática comum utilizar nomes como foo, bar e baz em exemplos que incluem código fonte que não se espera ser executado.

Aliás, tente executar o comando foo() e confira como o REPL trata erros de programação.

bar()

Nesse caso, nós estamos executando a função bar e a mesma não recebe argumentos.

Comentários

É uma prática comum adicionar comentários no código fonte de programas. Comentários são ignorados pelo compilador, scalac.

Existem duas maneiras de adicionar comentários em Scala. O primeiro é utilizando os caracteres //. Por exemplo:

// Olá, isso é um comentário que será ignorado pelo compilador.

Essa forma de comentário pode conter apenas uma linha de texto. Tudo o que for digitado a partir dos caracteres // será ignorado pelo compilador.

Se você pretende adicionar comentários que incluam várias linhas de texto você deve utilizar a segunda forma:

/*

Olá, isso é um comentário que será ignorado pelo compilador.

Esta forma de comentário pode ocupar várias linhas de texto.

*/

De forma geral aconselhamos que você inclua comentários em seu código fonte para o explicar o porque o código está escrito de certa forma.

Variáveis

A idéia de uma variável em linguagens de programação é dar um nome a um certo valor. É uma forma de abstração. Nós referimos a um nome, ou seja, um símbolo e não ao valor em si.

Existem duas formas de definir variáveis em Scala. A primeira é utilizando a palavra-chave val.

val mensagem = "Oi, Scala!"

A linha de código anterior define a variável mensagem, que aponta para o valor "Oi, Scala!". Note que essa conexão entre o nome da variável e o valor é feito através do símbolo de atribuição =.

Após definar a variável mensagem nós podemos utilizar a variável como se estivéssemos nos referindo ao valor que essa variável representa. Isso fica mais claro utilizando um exemplo:

val mensagem = "Oi, Scala!"
print(mensagem)

Repare que agora estamos executando a função print e estamos passando como argumento a variável mensagem. O resultado é o mesmo de nosso primeiro exemplo, ou seja, imprimimos "Oi, Scala!" na tela do computador.

A segunda forma de declarar uma variável é utilizando a palavra-chave var.

var mensagem = "Oi, Scala!"

val e var

Qual a diferença entra val e var? A diferença é que val define o valor de uma variável e não é permitido alterar esse valor posteriormente em seu programa. Isso quer dizer que a variável criada com val é imutável. No exemplo anterior, a variável mensagem, definida através da palavra chave val, não pode ser alterada para apontar para um valor diferente.

Diferentemente de val, variáveis definidas através da palavra-chave var podem ser alteradas pelo seu programa para apontar para diferentes valores desde que os novos valores possuam o mesmo tipo.

No exemplo abaixo definimos a variável idade que aponta para o valor 30. Na linha seguinte tentamos mudar o valor da variável para 40 mas isso não é possível. O REPL gera uma mensagem de erro e seu programa não é executado.

val idade = 30
idade = 40 // Isso vai gerar um erro de compilação!

Se utilizarmos var para definir uma variável torna-se possível alterar o valor da variável posteriormente.

var ano = 1900
ano = 2000

A escolhe entre val ou var tem uma implicação muito grande em seu programa. Isso vai se tornar mais claro quando introduzirmos conceitos de Functional Programming (FP) e Concurrency. No momento, é importante mencionar apenas que é sempre melhor utilizar valores imutáveis. Em suma, prefira utilizar val sempre que possível.

Primeiros passos em Tipos

No nosso primeiro exemplo, dissemos que a mensagem entre aspas duplas representa um tipo String. Logo adiante, vimos um exemplo em que o valor passado para a função print tinha um tipo integral que em Scala é chamado de Int. O que isso quer dizer?

Todos os valores na linguagem escala possuem um tipo. Em inglês, type. Esse tipo define, ou melhor, restringe, os valores que uma variável pode assumir.

var mensagem = "Bom dia"

No exemplo acima o tipo da variável mensagem é String. O tipo String é uma sequência de caracteres.

Vimos anteriormente, que variáveis definidas através da palavra-chave var podem ser modificadas. Portanto, o código fonte seguinte é válido:

var mensagem = "Bom dia"
mensagem = "Boa noite"

O seguinte código, entretanto, é inválido.

var mensagem = "Bom dia"
mensage = 42 // Erro!

A variável mensagem, uma vez definida com o tipo String, não pode ser alterada para um tipo diferente. Neste caso, não podemos modificar o tipo a variável mensagem de String para Int mesmo que a variável tenha sido inicialmente definida com a palavra-chave var.

Essa propriedade da linguagem é geralmente definida em inglês como strongly typed, ou statically typed.

Esta restrição quanto ao tipo dos valores é garantida pelo compilador. Ou seja, erros de programação como esse são descobertos durante o desenvolvimento e não durante a execução de seu programa.

Inferência de Tipos

Até agora nós falamos sobre dois tipos: String e Int e você deve estar se perguntando como a linguagem sabe que esse é o tipo que você quer que a variável assuma.

O compilador trabalha duro para inferir os tipos de cada variável em seu programa. Esse processo é chamado de inferência de tipos, do inglês, type inference.

Note que o REPL imprimi em seu terminal o tipo da variável no momento em que a mesma é definida.

val mensagem = "Bom dia"

No exemplo acima, após pressionar a tecla ENTER o REPL imprimi a seguinte informação:

mensagem: String = Bom dia

Note que após os dois pontos o REPL mostra o tipo que foi inferido para a variável mensagem. Neste caso o tipo inferido é String.

Inferência de tipos em Scala é opcional na maior parte dos casos. Em algumas situações a linguagem pode inferir um tipo diferente do que esperamos. Vamos nos aprofundar mais neste assunto em capítulos posteriores.

Bem, se a inferência de tipos é opcional, como podemos definir o tipo de uma variável? Simples, seguindo o exemplo do REPL, basta adicionarmos após o nome da variável dois pontos, :, seguido do tipo. Por exemplo:

val mensagem: String = "Bom dia"
val ano: Int = 42

A sintaxe da linguagem, definida por sua grámatica, contém uma certa simetria. Definir o tipo de um valor utilizando a fórmula : Tipo será utilizada novamente quando aprendermos sobre funções.

Funções

Em nosso primeiro exemplo nós usamos a função print para imprimir uma mensagem na tela do computador. Aprendemos também sobre a sintaxe para aplicação de funções, ou seja, como executar uma função. Logo após, nós aprendemos que a função print faz parte da biblioteca padrão da linguagem. Nessa seção vamos aprender como definir nossas próprias funções.

Implementar uma função requer as seguintes informações:

A medida que avançarmos no livro vamos aprender que nem sempre todas essas informações são necessárias.

Uma função bem simples

Vamos criar uma função que soma dois números naturais. Digite a seguinte linha de código no REPL:

def somar(a: Int, b: Int): Int = a + b

Primeiramente, nós definimos o nome da função usando a palavra-chave def seguido do nome de nossa função somar. O princípio é o mesmo para variáveis. Variáveis são definidas usando as palavras-chaves val ou var seguido do nome da variável.

A segunda parte da função são os argumentos que nossa função vai receber. A função somar espera receber dois argumentos. Damos o do primeiro argumento de a e seu tipo é Int. Ao segundo argumento damos o nome de b e definimos seu tipo também como Int. Note que quando definimos os argumentos da função não existe inferência automática de tipos e somos obrigados a listar os tipos de cada argumento que a função recebe.

A terceira informação para definirmos a função é seu valor de retorno. O retorno de nossa função é do tipo Int, afinal, estamos somando dois números naturais.

O tipo de retorno de uma função não é obrigatório e o compilador pode inferir esse valor para nós. Apesar disso, sugerimos evitar esta prática. Definir o tipo de retorno manualmente não apenas torna o código mais fácil de se entender como também se evita algumas situações em que o compilador pode inferir um tipo diferente do que esperamos.

A última parte necessária para definirmos nossa função é o corpo da função. Ou seja, uma vez que nossa função seja executada devemos definir o que vamos fazer com os argumentos que a função recebeu e que valor iremos retornar. Nossa função tem um corpo bem simples, nós apenas somamos os argumentos recebidos: a + b.

Repare a simetria que a gramática da linguagem fornece.

A parte def somar define um nome, do mesmo mode que definimos um nome de uma variável val mensage. Os tipos de cada argumento acompanham o nome que criamos : Tipo. Incluindo o tipo da função def somar(...): Int. O valor que atribuímos ao nome vem após o sinal de atribuição =. No caso de uma função esse valor é o corpo da função.

Se você tem experiência em outras linguagens de programação deve ter observado que não utilizamos a palavra chave return para retornamos o valor calculado pela função. A palavra-chave return é opcional em Scala e seu uso não é recomendado. A razão por trás disso é que em Scala não existem statements. A linguagem contém apenas expressions, incluindo estruturas de controle como if-else. No nosso exemplo a expressão a + b é executada e seu valor é o valor retornado pela função.

Se Scala é sua primeira linguagem de programação a distinção entre statement e expression não é importante neste momento.

Bem, até agora nós apenas definimos a função. Vamos executá-la:

somar(10, 35)
somar(42, 88)
somar(-5, 72)

Após digitar os comandos anteriores você provavelmente percebeu que o REPL imprimou na tela de seu computador os seguintes resultados:

res0: Int = 45
res1: Int = 130
res2: Int = 67

Isso significa que o REPL criou uma variável automaticamente para você. Cada variável contém o resultado da execução de nossa função. Essas variáveis podem ser usadas novamente através do REPL. Por exemplo, se você digitar:

res1

O REPL vai imprimir o resultado que a variável res1 contém:

res3: Int = 130

Criando novamente uma nova variável, res3. Ad infinitum.

Ao invés de deixar o REPL criar uma variável para nós, podemos capturar o valor retornado de uma função em uma variável que nós definimos. Por exemplo:

val resultado = somar(10, 35)
print(resultado)

Dessa forma, o valor retornado pela aplicação da função somar(10, 35) é atribuído a variável resultado.

Uma função um pouco mais complicada

Nossa primeira função tinha como corpo uma pequena expressão: a + b. Se o valor que a função calcula requer várias expressões o corpo da função deve ser colocado entre chaves.

def maxOuZero(x: Int, y: Int): Int = {
    val max = if (x > y) x else y
    if (max < 0) 0 else max
}

A função maxOuZero requer o uso de chaves pois a mesma contem mais de uma expressão.

A função maxOuZero introduz a expressão if-else. Posteriormente vamos definir com mais detalhes esta estrutura de controle mas no momento podemos ler o código da seguinte forma: definimos a variável max e seu valor depende das variáveis x e y. Se x for maior do que y o valor atribuido a max será o valor de x. Caso contrário, ou seja, se x for menor do que y o valor atribuido a max é o valor da variável y. Na linha seguinte incluimos outra expressão if-else, mas desta vez nós checamos se o valor de max é menor do que o número 0. Em caso afirmativo retornamos 0 e em caso negativo retornamos o valor max.

maxOuZero(10, 100)
maxOuZero(-5, -27)

Se executarmos a função como nos exemplos acima observamos que o REPL imprimi os seguintes resultados:

res0: Int = 100
res1: Int = 0

Em linguagens de programação como C++ ou Java as estrutura de controle if-else é um statement e não uma expression como em Scala. Em Scala, if-else produz um valor.

Capítulo 2

Tipos

No primeiro capítulo nós tivemos uma breve introdução a tipos e aprendemos sobre dois tipos diferentes: String e Int. Neste capítulo vamos nos aprofundar um pouco mais no assunto. Veremos o que são tipos de primeira ordem (first-order types) abordando os tipos mais comuns que se encontram na biblioteca padrão da linguagem e como definir e usar nossos próprios tipos.

Em capítulos posteriores vamos nos aprofundar ainda mais sobre o conceito de tipos. Por exemplo, explicaremos o que são construtores de tipos (type constructors) e como isso é explorado em programação orientada a object e programação funcional.

Quando discutimos o sistema de tipos (type system) em Scala é comum nos referirmos a duas propriedades que fazem parte da linguagem: Strongly Typed e Statically Typed.

Strongly typed

A tradução literal quer dizer fortemente tipificada. Na prática isto quer dizer que variáveis e funções são definidas com apenas um tipo e esse tipo é imutável em seu programa. Por exemplo, uma variável definida com o tipo String não pode posteriormente assumir um valor de um tipo diferente. A partir dessa propriedade o compilador da linguagem pode conferir que várias operações de seu programa são condizentes. Por exemplo, se uma variável x tem o tipo String e uma função f requer um argumento do tipo Int, o compilador garante que não é possível executar a função f utilizando como argumento a variável x porque os tipos não se alinham.

Essa propriedade gera os seguintes benefícios:

  1. É mais fácil ler o código fonte em uma linguagem fortemente tipificada. Isso se reflete não apenas na leitura de código por programadores mas por ferramentas como IDEs que precisam retirar informações do código fonte sem necessáriamente executar o mesmo.
  2. Uma maior confiança quanto a qualidade do programa uma vez que vários erros podem ser descobertos antes mesmo de o programa ser executado.
  3. O compilador pode fazer maiores optimizações baseado nos tipos que cada valor possue gerando programas mais eficientes.

Statically typed

O termo em português é estaticamente tipificada. Isso quer dizer que a limitação sobre os tipos e sua concordância quando passamos valores entre funções e variáveis são checadas durante a compilação e não durante a execução do programa. Um benefício dessa propriedade é um menor número de testes que precisam ser escritos, executados e mantidos para aumentar a confiança de que o programa que escrevemos está correto.

Um outra forma de enxergar tipos é baseada na teoria de conjuntos. Por exemplo, uma variável do tipo Int pertence ao conjunto dos números naturais. Além disso, esse conjunto é finito e inclui valores entre -2147483648 e 2147483647.

Vamos agora definir os tipos incluidos na biblioteca padrão (standard library) da linguagem. Esses tipos estão presentes na maioria das linguagens de programação.

Tipos Numéricos

A biblioteca padrão oferece 7 tipos numéricos: Byte, Short, Char, Int, Long, Float e Double. Os tipos se diferenciam entre si por três propriedades. Se o número é inteiro, de ponto flutuante e se possuem representação negativa.

Todos os tipos possuem representação positiva e negativa sendo a única exceção o tipo Char que apenas possue representação positiva.

Os tipos Byte, Short, Int e Long representam números inteiros (positivos e negativos) utilizando 8, 16, 32 e 64 bits respectivamente. O tipo Char utiliza 16 bits e não possue números negativos. Os tipos Float e Double são números de ponto flutuante (positivos e negativos) e utilizam 32 e 64 bits respectivamente.

A especificação da linguagem requer que os tipos sejam Value Classes, ou seja, a representação desses valores não devem ser representados por objetos no sistema que hospeda a linguagem.

Boolean

O tipo Boolean possue apenas dois valores: true (verdadeiro) e false (falso). Valores boolean são tão comuns em programação que utilizamos o nome em inglês de seus valores neste livro.

Unit

O tipo Unit possue apenas um valor: (). O tipo Unit pode ser relacionado ao tipo void em outras linguagens de programação. A diferença é que em Scala todas as funções retornam um valor. Se o tipo de retorno é Unit o único valor possível é ().

String

O tipo String define uma sequência de caracteres que por sua vez são do tipo Char. Valores do tipo String são delimitados por aspas duplas. Por exemplo:

val s = "A Linguagem de Programação Scala"

Um valor String definido desta forma não pode conter quebras de linha, ou seja, os valores não Unicode \u000A (LF) ou \u000D (CR) não podem ser utilizados. Aspas duplas também não podem ser utilizadas pois a mesma é utilizada para demarcar o começo e o final da sequência de caracteres.

É possível utilizar aspas duplas e alguns outros caracteres especiais utilizando uma sequência de escape, do inglês escape sequence. Sequências de escape são formadas pela barra inversa (backslash) seguida pelo caracter desejado. Por exemplo, para se utilizar aspas duplas em um valor String utilizamos o sequinte formato:

val mensagem = "Aspas duplas via \"sequência de escape\"."

Executando o código acima em seu REPL é possível perceber que as aspas duplas foram inseridas corretamente:

mensagem: String = Aspas duplas via "sequência de escape".

A lista de sequências de escape incluídas em Scala é a seguinte:

sequência unicode character
\b \u0008 BS
\t \u0009 HT
\n \u000a LF
\f \u000c FF
\r \u000d CR
\" \u0022 "
\' \u0027 '
\\ \u005c \

Valores do tipo também podem ser definidos em mais de uma linha.

Concatenação e InterpolaçãO

EM BREVE.

Tuple

EM BREVE

Array

EM BREVE

Classes

Até o momento lidamos apenas com tipos definidos pela linguagem ou que existem na biblioteca padrão que acompanha a linguagem. Vamos agora aprender como definir nossos própios tipos em scala. Começaremos explicando classes, depois objetos e finalmente traits.

Class

Uma classe é tipo definido pelo usuário que pode ser usado para construir objetos durante a execução do seu programa. Alguns textos utilizam a analogia de que uma classe é como a planta de uma casa e portanto várias casas (ou objetos) podem ser constrúidos a partir desta planta.

Por exemplo, suponha que nosso programa lide com clientes. Podemos abstrair a idéia de um cliente criando a classe Cliente. Isso é traduzido em scala da seguinte forma:

class Cliente

Após a palavra-chave class nós introduzimos o nome da nossa classe, neste caso, Cliente.

Após definirmos a classe Cliente podemos criar diferentes clientes durante a execução de nosso programa.

val maria = new Cliente

O código acima constrói uma instância da classe Cliente na memória do computador. De forma resumida, a criação de instâncias de classes se dá através da palavra-chave new. A construção da instância se dá em duas etapas. Na primeira etapa memória é reservada para conter a instância da classe e na segunda etapa o construtor da classe é executado. O construtor de uma classe é uma função que é executada durante a criação de uma instância. Discutiremos construtores posteriormente.

Em nosso exemplo, além de criamos uma instância de Cliente, criamos também a variável maria. A variável maria é um binding, ou seja, uma ligação entre o nome maria e a instância de Cliente residente na memória do computador.

Nossa classe Cliente é um exemplo simples. No dia-a-dia uma a definição de uma classe como Cliente deve conter maiores informações para se modelar um cliente. Vamos portanto modificar nossa classe de modo que possamos incluir informações sobre clientes.

class Cliente(val nome: String, val email: String)

A nova definição de Cliente inclui duas variáveis nome e email e portanto diferentes instâncias de nossa classe podem agora conter diferentes nomes e emails.

Se você tem experiência em outras linguagens de programação você deve ter notado que Scala é uma linguagem sucinta. Definida dessa forma, as variáveis nome e email são parâmetros do construtor da classe Cliente e ao mesmo tempo variáveis que podem ser acessadas através das instâncias criadas dessa classe. Por exemplo:

val maria = new Cliente("Maria Joaquina", "maria@email.com")
println(maria.nome)
println(maria.email)

No código acima nós primeiros criamos uma instância da classe cliente com o nome Maria Joaquina e email maria@email.com. A variável maria é a ligação, ou binding, com a instância de clientecriada na memória do computador. A sintaxe que nos permite acessar as variáveis que compõe a classe é composta pelo nome da variável, seguida de um ponto, e finalmente o nome da variável. Portanto, maria.nome, retorna o valor "Maria".

Classes podem conter não apenas variáveis mas também funções. Por exemplo, podemos adicionar uma função que envia uma mensagem a nosso cliente.

class Cliente(val nome: String, val email: String) {
  def enviarEmail(mensagem: String): Unit = {
    println(s"Enviando mensagem $mensagem para $nome no endereço $email")
  }
}

Repare como adicionamos a função enviarEmail em nossa classe. Nós definimos a função dentro de chaves. A definição da função em nada difere do que aprendemos anteriormente. A função recebe o argumento mensagem e constrói uma nova string que inclui a mensagem recebida pela função e as variáveis nome e email que a classe contidadas na classe. Nós então apenas imprimimos a mensagem através da função println.

EM BREVE

Capítulo 3

Funções

EM BREVE

Capítulo 4

Programação Orientada a Objetos

EM BREVE

Capítulo 5

Programação Genérica

Capítulo 6

Functional Programming

EM BREVE

Capítulo N

EM BREVE

Bibliografia

Scala Language Specification Version 2.12

http://scala-lang.org/files/archive/spec/2.12/

(c) 2017 daniberg.com