Introdução ao Scala

Introdução ao Scala

1. Introdução

Neste tutorial, vamos dar uma olhada emScala - uma das principais linguagens que são executadas na Java Virtual Machine.

Começaremos com os recursos básicos da linguagem, como valores, variáveis, métodos e estruturas de controle. Em seguida, exploraremos alguns recursos avançados, como funções de ordem superior, currying, classes, objetos e correspondência de padrões.

Para obter uma visão geral das linguagens JVM, verifique nossoquick guide to the JVM Languages

2. Configuração do Projeto

Neste tutorial, usaremos a instalação padrão do Scala dehttps://www.scala-lang.org/download/.

Em primeiro lugar, vamos adicionar a dependência da biblioteca scala ao nosso pom.xml. Este artefato fornece a biblioteca padrão para o idioma:


    org.scala-lang
    scala-library
    2.12.7

Em segundo lugar, vamos adicionar oscala-maven-plugin para compilar, testar, executar e documentar o código:


    net.alchim31.maven
    scala-maven-plugin
    3.3.2
    
        
            
                compile
                testCompile
            
        
    

O Maven possui os artefatos mais recentes parascala-langescala-maven-plugin.

Por fim, usaremos o JUnit para testes de unidade.

3. Funcionalidades básicas

Nesta seção, examinaremos os recursos básicos da linguagem por meio de exemplos. UsaremosScala interpreter para esse propósito.

3.1. Intérprete

O intérprete é um shell interativo para escrever programas e expressões.

Vamos imprimir “hello world” usando:

C:\>scala
Welcome to Scala 2.12.6 (Java HotSpot(TM)
 64-Bit Server VM, Java 1.8.0_92).
Type in expressions for evaluation.
Or try :help.

scala> print("Hello World!")
Hello World!
scala>

Acima, iniciamos o intérprete digitando 'scala' na linha de comando. O intérprete inicia e exibe uma mensagem de boas-vindas seguida por um prompt.

Em seguida, digitamos nossa expressão nesse prompt. O intérprete lê a expressão, avalia-a e imprime o resultado. Em seguida, ele faz um loop e exibe o prompt novamente.

Como fornece feedback imediato, o intérprete é a maneira mais fácil de começar o idioma. Portanto, vamos usá-lo para explorar os recursos básicos da linguagem: expressões e várias definições.

3.2. Expressões

Any computable statement is an expression.

Vamos escrever algumas expressões e ver seus resultados:

scala> 123 + 321
res0: Int = 444

scala> 7 * 6
res1: Int = 42

scala> "Hello, " + "World"
res2: String = Hello, World

scala> "zipZAP" * 3
res3: String = zipZAPzipZAPzipZAP

scala> if (11 % 2 == 0) "even" else "odd"
res4: String = odd

Como podemos ver acima,every expression has a value and a type.

If an expression does not have anything to return, it returns a value of type Unit. Este tipo possui apenas um valor:(). É semelhante à palavra-chavevoid em Java.

3.3. Definição de Valor

A palavra-chaveval é usada para declarar valores.

Nós o usamos para nomear o resultado de uma expressão:

scala> val pi:Double = 3.14
pi: Double = 3.14

scala> print(pi)
3.14

Isso nos permite reutilizar o resultado várias vezes.

Values are immutable. Portanto, não podemos reatribuí-los:

scala> pi = 3.1415
:12: error: reassignment to val
       pi = 3.1415
         ^

3.4. Definição de variável

Se precisarmos reatribuir um valor, declararemos como uma variável.

A palavra-chavevar  é usada para declarar variáveis:

scala> var radius:Int=3
radius: Int = 3

3.5. Definição do Método

Definimos métodos usando a palavra-chavedef. Após a palavra-chave, especificamos o nome do método, declarações de parâmetro, um separador (dois pontos) e o tipo de retorno. Depois disso, especificamos um separador (=) seguido pelo corpo do método.

Em contraste com Java, não usamos a palavra-chavereturn para retornar o resultado. Um método retorna o valor da última expressão avaliada.

Vamos escrever um métodoavg para calcular a média de dois números:

scala> def avg(x:Double, y:Double):Double = {
  (x + y) / 2
}
avg: (x: Double, y: Double)Double

Então, vamos invocar este método:

scala> avg(10,20)
res0: Double = 12.5

Se um método não aceita nenhum parâmetro, podemos omitir os parênteses durante a definição e a invocação. Além disso, podemos omitir as chaves se o corpo tiver apenas uma expressão.

Vamos escrever um método sem parâmetroscoinToss que retorna aleatoriamente "Cabeça" ou "Cauda":

scala> def coinToss =  if (Math.random > 0.5) "Head" else "Tail"
coinToss: String

A seguir, vamos invocar este método:

scala> println(coinToss)
Tail
scala> println(coinToss)
Head

4. Estruturas de controle

As estruturas de controle nos permitem alterar o fluxo de controle em um programa. Temos as seguintes estruturas de controle:

  • Expressão If-else

  • Enquanto loop e loop while

  • Para expressão

  • Tente expressão

  • Corresponder expressão

Ao contrário do Java, não temos palavras-chavecontinue oubreak. Temos a palavra-chavereturn. No entanto, devemos evitar usá-lo.

Em vez da instruçãoswitch, temos a correspondência de padrões por meio da expressão de correspondência. Além disso, podemos definir nossas próprias abstrações de controle.

4.1. se-mais

A sexpressãoif-else é semelhante a Java. A parteelse é opcional. Podemos aninhar várias expressões if-else.

Since it is an expression, it returns a value. Portanto, usamos semelhante ao operador ternário (? :) em Java. Na verdade, o idiomadoes not have have the ternary operator.

Usando if-else, vamos escrever um método para calcular o maior divisor comum:

def gcd(x: Int, y: Int): Int = {
  if (y == 0) x else gcd(y, x % y)
}

Então, vamos escrever um teste de unidade para este método:

@Test
def whenGcdCalledWith15and27_then3 = {
  assertEquals(3, gcd(15, 27))
}

4.2. Enquanto Loop

O loop while tem uma condição e um corpo. Ele avalia repetidamente o corpo em um loop enquanto a condição é verdadeira - a condição é avaliada no início de cada iteração.

Como não há nada útil para retornar, ele retornaUnit.

Vamos usar o loop while para escrever um método para calcular o maior divisor comum:

def gcdIter(x: Int, y: Int): Int = {
  var a = x
  var b = y
  while (b > 0) {
    a = a % b
    val t = a
    a = b
    b = t
  }
  a
}

Então, vamos verificar o resultado:

assertEquals(3, gcdIter(15, 27))

4.3. Fazer loop enquanto

O loop do while é semelhante ao loop while, exceto que a condição do loop é avaliada no final do loop.

Usando o loop do-while, vamos escrever um método para calcular o fatorial:

def factorial(a: Int): Int = {
  var result = 1
  var i = 1
  do {
    result *= i
    i = i + 1
  } while (i <= a)
  result
}

A seguir, vamos verificar o resultado:

assertEquals(720, factorial(6))

4.4. Para Expressão

A expressão for é muito mais versátil do que o loop for em Java.

Ele pode iterar em uma ou várias coleções. Além disso, pode filtrar elementos, bem como produzir novas coleções.

Usando a expressão for, vamos escrever um método para somar um intervalo de inteiros:

def rangeSum(a: Int, b: Int) = {
  var sum = 0
  for (i <- a to b) {
    sum += i
  }
  sum
}

Aqui,a to b é uma expressão geradora. Ele gera uma série de valores dea ab.

i ← a to b  é um gerador. Ele defineasvale atribui a série de valores produzidos pela expressão geradora.

O corpo é executado para cada valor da série.

A seguir, vamos verificar o resultado:

assertEquals(55, rangeSum(1, 10))

5. Funções

Scala é uma linguagem funcional. As funções são valores de primeira classe aqui - podemos usá-las como qualquer outro tipo de valor.

Nesta seção, veremos alguns conceitos avançados relacionados a funções - funções locais, funções de ordem superior, funções anônimas e currying.

5.1. Funções locais

We can define functions inside functions. Eles são referidos como funções aninhadas ou funções locais. Semelhante às variáveis ​​locais,they are visible only within the function they are defined in.

Agora, vamos escrever um método para calcular a potência usando uma função aninhada:

def power(x: Int, y:Int): Int = {
  def powNested(i: Int,
                accumulator: Int): Int = {
    if (i <= 0) accumulator
    else powNested(i - 1, x * accumulator)
  }
  powNested(y, 1)
}

A seguir, vamos verificar o resultado:

assertEquals(8, power(2, 3))

5.2. Funções de ordem superior

Como funções são valores, podemos passá-los como parâmetros para outra função. Também podemos ter uma função retornando outra função.

We refer to functions which operate on functions as higher-order functions. Eles nos permitem trabalhar em um nível mais abstrato. Usando-os, podemos reduzir a duplicação de código escrevendo algoritmos generalizados.

Agora, vamos escrever uma função de ordem superior para executar um mapa e reduzir a operação em um intervalo de inteiros:

def mapReduce(r: (Int, Int) => Int,
              i: Int,
              m: Int => Int,
              a: Int, b: Int) = {
  def iter(a: Int, result: Int): Int = {
    if (a > b) {
      result
    } else {
      iter(a + 1, r(m(a), result))
    }
  }
  iter(a, i)
}

Aqui,rem são parâmetros do tipoFunction. Ao passar diferentes funções, podemos resolver uma série de problemas, como a soma de quadrados ou cubos e o fatorial.

A seguir, vamos usar esta função para escrever outra funçãosumSquares que soma os quadrados de inteiros:

@Test
def whenCalledWithSumAndSquare_thenCorrectValue = {
  def square(x: Int) = x * x
  def sum(x: Int, y: Int) = x + y

  def sumSquares(a: Int, b: Int) =
    mapReduce(sum, 0, square, a, b)

  assertEquals(385, sumSquares(1, 10))
}

Acima, podemos ver quehigher-order functions tend to create many small single-use functions. We can avoid naming them by using anonymous functions.

5.3. Funções anônimas

Uma função anônima é uma expressão que é avaliada como uma função. É semelhante à expressão lambda em Java.

Vamos reescrever o exemplo anterior usando funções anônimas:

@Test
def whenCalledWithAnonymousFunctions_thenCorrectValue = {
  def sumSquares(a: Int, b: Int) =
    mapReduce((x, y) => x + y, 0, x => x * x, a, b)
  assertEquals(385, sumSquares(1, 10))
}

Neste exemplo,mapReduce recebe duas funções anônimas:(x, y) ⇒ x + yex ⇒ x * x.

Scala can deduce the parameter types from context. Portanto, estamos omitindo o tipo de parâmetros nessas funções.

Isso resulta em um código mais conciso em comparação com o exemplo anterior.

5.4. Funções de curry

A curried function takes multiple argument lists, such as def f(x: Int) (y: Int). É aplicado passando várias listas de argumentos, como em f (5) (6).

It is evaluated as an invocation of a chain of functions. Estas funções intermediárias recebem um único argumento e retornam uma função.

Também podemos especificar listas de argumentos parcialmente, comof(5).

Agora, vamos entender isso com um exemplo:

@Test
def whenSumModCalledWith6And10_then10 = {
  // a curried function
  def sum(f : Int => Int)(a : Int, b : Int) : Int =
    if (a > b) 0 else f(a) + sum(f)(a + 1, b)

  // another curried function
  def mod(n : Int)(x : Int) = x % n

  // application of a curried function
  assertEquals(1, mod(5)(6))

  // partial application of curried function
  // trailing underscore is required to
  // make function type explicit
  val sumMod5 = sum(mod(5)) _

  assertEquals(10, sumMod5(6, 10))
}

Acima,sumemod cada um leva duas listas de argumentos. Passamos as duas listas de argumentos comomod(5)(6). Isso é avaliado como duas chamadas de função. Primeiro,mod(5) é avaliado, o que retorna uma função. Isso é, por sua vez, invocado com o argumento6.  Temos 1 como resultado.

É possível aplicar parcialmente os parâmetros como emmod(5). . Obtemos uma função como resultado.

Da mesma forma, na expressãosum(mod(5)) _, estamos passando apenas o primeiro argumento para a funçãosum. Portanto,sumMod5 é uma função.

O sublinhado é usado como um espaço reservado para argumentos não aplicados. Como o compilador não pode inferir que um tipo de função é esperado, estamos usando o sublinhado à direita para tornar explícito o tipo de retorno da função.

5.5. Parâmetros por nome

Uma função pode aplicar parâmetros de duas maneiras diferentes - por valor e por nome - avalia argumentos por valor apenas uma vez no momento da chamada. Por outro lado, avalia argumentos por nome sempre que são referidos. Se o argumento por nome não for usado, ele não será avaliado.

Scala usa parâmetros por valor por padrão. Se o tipo de parâmetro for precedido por uma seta (⇒), ele muda para o parâmetro por nome.

Agora, vamos usá-lo para implementar o loop while:

def whileLoop(condition: => Boolean)(body: => Unit): Unit =
  if (condition) {
    body
    whileLoop(condition)(body)
  }

Para que a função acima funcione corretamente, ambos os parâmetrosconditionebody devem ser avaliados sempre que forem referidos. Portanto, estamos definindo-os como parâmetros por nome.

6. Definição de classe

Definimos uma classe com a palavra-chaveclass seguida do nome da classe.

Após o nome,we can specify primary constructor parameters. Doing so automatically adds members with the same name to the class.

No corpo da classe, definimos os membros - valores, variáveis, métodos etc. They are public by default unless modified by the private or protected access modifiers.

Temos que usar a palavra-chaveoverride para substituir um método da superclasse.

Vamos definir uma classe Employee:

class Employee(val name : String, var salary : Int, annualIncrement : Int = 20) {
  def incrementSalary() : Unit = {
    salary += annualIncrement
  }

  override def toString =
    s"Employee(name=$name, salary=$salary)"
}

Aqui, estamos especificando três parâmetros do construtor -name,salary eannualIncrement.

Como estamos declarandoname esalary comvalevar palavras-chave, os membros correspondentes são públicos. Por outro lado, não estamos usando a palavra-chaveval ouvar para o parâmetroannualIncrement. Portanto, o membro correspondente é privado. Como estamos especificando um valor padrão para esse parâmetro, podemos omiti-lo enquanto chamamos o construtor.

Além dos campos, estamos definindo o métodoincrementSalary. Este método é público.

A seguir, vamos escrever um teste de unidade para esta classe:

@Test
def whenSalaryIncremented_thenCorrectSalary = {
  val employee = new Employee("John Doe", 1000)
  employee.incrementSalary()
  assertEquals(1020, employee.salary)
}

6.1. Classe abstrata

Usamos a palavra-chaveabstract para fazer um resumo da classe. É semelhante ao Java. Pode ter todos os membros que uma classe regular pode ter.

Além disso, pode conter membros abstratos. Esses são membros com apenas declaração e sem definição, com sua definição fornecida na subclasse.

Da mesma forma que Java, não podemos criar uma instância de uma classe abstrata.

Agora, vamos ilustrar a classe abstrata com um exemplo.

Primeiro, vamos criar uma classe abstrataIntSet para representar o conjunto de inteiros:

abstract class IntSet {
  // add an element to the set
  def incl(x: Int): IntSet

  // whether an element belongs to the set
  def contains(x: Int): Boolean
}

A seguir, vamos criar uma subclasse concretaEmptyIntSet para representar o conjunto vazio:

class EmptyIntSet extends IntSet {
  def contains(x : Int) = false
  def incl(x : Int) =
  new NonEmptyIntSet(x, this)
}

Então, outra subclasseNonEmptyIntSet representa os conjuntos não vazios:

class NonEmptyIntSet(val head : Int, val tail : IntSet)
  extends IntSet {

  def contains(x : Int) =
    head == x || (tail contains x)

  def incl(x : Int) =
    if (this contains x) {
      this
    } else {
      new NonEmptyIntSet(x, this)
    }
}

Finalmente, vamos escrever um teste de unidade paraNonEmptySet:

@Test
def givenSetOf1To10_whenContains11Called_thenFalse = {
  // Set up a set containing integers 1 to 10.
  val set1To10 = Range(1, 10)
    .foldLeft(new EmptyIntSet() : IntSet) {
        (x, y) => x incl y
    }

  assertFalse(set1To10 contains 11)
}

6.2. Traits

Os traços correspondem às interfaces Java com as seguintes diferenças:

  • capaz de estender de uma classe

  • pode acessar membros da superclasse

  • pode ter instruções inicializadoras

Nós os definimos como definimos classes, mas usando a palavra-chavetrait. Além disso, eles podem ter os mesmos membros das classes abstratas, exceto pelos parâmetros do construtor. Além disso, eles devem ser adicionados a alguma outra classe como um mixin.

Agora, vamos ilustrar os traços usando um exemplo.

Primeiro, vamos definir um traçoUpperCasePrinter para garantir que o métodotoString retorne um valor em maiúsculas:

trait UpperCasePrinter {
  override def toString =
    super.toString toUpperCase
}

Então, vamos testar essa característica adicionando-a a uma classeEmployee:

@Test
def givenEmployeeWithTrait_whenToStringCalled_thenUpper = {
  val employee = new Employee("John Doe", 10) with UpperCasePrinter
  assertEquals("EMPLOYEE(NAME=JOHN DOE, SALARY=10)", employee.toString)
}

Classes, objetos e características podem herdar no máximo uma classe, mas qualquer número de características.

7. Definição de Objeto

Objetos são instâncias de uma classe. Como vimos nos exemplos anteriores, criamos objetos de uma classe usando a palavra-chavenew.

No entanto, se uma classe puder ter apenas uma instância, precisamos impedir a criação de várias instâncias. Em Java, usamos o padrão Singleton para conseguir isso.

Para esses casos, temos uma sintaxe concisa chamada definição de objeto - semelhante à definição de classe com uma diferença. Instead of using the class keyword, we use the object keyword. Fazer isso define uma classe e, preguiçosamente, cria sua única instância.

Usamos definições de objeto para implementar métodos de utilitário e singletons.

Vamos definir um objetoUtils:

object Utils {
  def average(x: Double, y: Double) =
    (x + y) / 2
}

Aqui, estamos definindo a classeUtilse também criando sua única instância.

We refer to this sole instance using its nameUtils. Esta instância é criada na primeira vez que é acessada.

Não podemos criar outra instância de Utils usando a palavra-chavenew.

Agora, vamos escrever um teste de unidade para o objetoUtils:

assertEquals(15.0, Utils.average(10, 20), 1e-5)

7.1. Objeto complementar e classe complementar

Se uma classe e uma definição de objeto tiverem o mesmo nome, as chamaremos de classe complementar e objeto complementar, respectivamente. Precisamos definir os dois no mesmo arquivo. Os objetos complementares podem acessar membros particulares de sua classe complementar e vice-versa.

Ao contrário de Java,we do not have static members. Em vez disso, usamos objetos complementares para implementar membros estáticos.

8. Correspondência de padrões

Pattern matching matches an expression to a sequence of alternatives. Cada um deles começa com a palavra-chavecase. Isso é seguido por um padrão, seta separadora (⇒) e várias expressões. A expressão é avaliada se o padrão corresponder.

Podemos criar padrões a partir de:

  • construtores de classe de caso

  • padrão variável

  • o padrão curinga _

  • literais

  • identificadores constantes

As classes de caso facilitam a correspondência de padrões nos objetos. Adicionamos a palavra-chavecase ao definir uma classe para torná-la uma classe de caso.

Portanto, a correspondência de padrões é muito mais poderosa do que a instrução switch em Java. Por esse motivo, é um recurso de linguagem amplamente utilizado.

Agora, vamos escrever o método Fibonacci usando correspondência de padrões:

def fibonacci(n:Int) : Int = n match {
  case 0 | 1 => 1
  case x if x > 1 =>
    fibonacci (x-1) + fibonacci(x-2)
}

A seguir, vamos escrever um teste de unidade para este método:

assertEquals(13, fibonacci(6))

9. Conclusão

Neste tutorial, apresentamos a linguagem Scala e alguns de seus principais recursos. Como vimos, ele fornece excelente suporte para programação imperativa, funcional e orientada a objetos.

Como de costume, o código-fonte completo pode ser encontradoover on GitHub.