Введение в Scala

Введение в Scala

1. Вступление

В этом руководстве мы рассмотримScala - один из основных языков, работающих на виртуальной машине Java.

Мы начнем с основных функций языка, таких как значения, переменные, методы и управляющие структуры. Затем мы рассмотрим некоторые расширенные функции, такие как функции высшего порядка, каррирование, классы, объекты и сопоставление с образцом.

Чтобы получить обзор языков JVM, ознакомьтесь с нашимquick guide to the JVM Languages

2. Настройка проекта

В этом руководстве мы будем использовать стандартную установку Scala изhttps://www.scala-lang.org/download/.

Во-первых, давайте добавим зависимость scala-library к нашему pom.xml. Этот артефакт предоставляет стандартную библиотеку для языка:


    org.scala-lang
    scala-library
    2.12.7

Во-вторых, давайте добавимscala-maven-plugin для компиляции, тестирования, запуска и документирования кода:


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

В Maven есть последние артефакты дляscala-lang иscala-maven-plugin.

Наконец, мы будем использовать JUnit для модульного тестирования.

3. Основные характеристики

В этом разделе мы рассмотрим основные языковые функции на примерах. Для этого мы будем использоватьScala interpreter.

3.1. переводчик

Интерпретатор представляет собой интерактивную оболочку для написания программ и выражений.

Давайте с его помощью напечатаем "hello world":

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>

Выше мы запускаем интерпретатор, набирая «scala» в командной строке. Переводчик запускается и отображает приветственное сообщение, за которым следует подсказка.

Затем мы набираем наше выражение в этой подсказке. Интерпретатор читает выражение, оценивает его и печатает результат. Затем он зацикливается и снова отображает подсказку.

Поскольку он обеспечивает немедленную обратную связь, переводчик является самым простым способом начать работу с языком. Поэтому давайте с его помощью исследуем основные особенности языка: выражения и различные определения.

3.2. Выражения

Any computable statement is an expression.

Напишем несколько выражений и посмотрим их результаты:

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

Как мы видим выше,every expression has a value and a type.

If an expression does not have anything to return, it returns a value of type Unit. Этот тип имеет только одно значение:(). Он похож на ключевое словоvoid в Java.

3.3. Определение значения

Ключевое словоval используется для объявления значений.

Мы используем его, чтобы назвать результат выражения:

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

scala> print(pi)
3.14

Это позволяет нам многократно использовать результат.

Values are immutable. Поэтому мы не можем переназначить их:

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

3.4. Определение переменной

Если нам нужно переназначить значение, мы объявим его как переменную.

Ключевое словоvar  используется для объявления переменных:

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

3.5. Определение метода

Мы определяем методы с помощью ключевого словаdef. После ключевого слова мы указываем имя метода, объявления параметров, разделитель (двоеточие) и тип возвращаемого значения. После этого мы указываем разделитель (=), за которым следует тело метода.

В отличие от Java, мы не используем ключевое словоreturn для возврата результата. Метод возвращает значение последнего вычисленного выражения.

Напишем методavg для вычисления среднего двух чисел:

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

Затем вызовем этот метод:

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

Если метод не принимает никаких параметров, мы можем опустить круглые скобки во время определения и вызова. Кроме того, мы можем опустить фигурные скобки, если в теле есть только одно выражение.

Давайте напишем метод без параметровcoinToss w, который случайным образом возвращает «Head» или «Tail»:

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

Затем давайте вызовем этот метод:

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

4. Управляющие структуры

Управляющие структуры позволяют нам изменять поток управления в программе. У нас есть следующие структуры управления:

  • Выражение if-else

  • Пока цикл и сделать пока цикл

  • Для выражения

  • Попробуй выражение

  • Выражение соответствия

В отличие от Java, у нас нет ключевых словcontinue илиbreak. У нас есть ключевое словоreturn. Тем не менее, мы должны избегать его использования.

Вместо оператораswitch у нас есть сопоставление с образцом через выражение сопоставления. Кроме того, мы можем определить наши собственные абстракции управления.

4.1. если еще

Секспрессияif-else аналогична Java. Частьelse не является обязательной. Мы можем вкладывать несколько выражений if-else.

Since it is an expression, it returns a value. Поэтому мы используем его аналогично троичному оператору (? :) в Java. Фактически, языкdoes not have have the ternary operator.

Используя if-else, давайте напишем метод для вычисления наибольшего общего делителя:

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

Затем давайте напишем модульный тест для этого метода:

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

4.2. Пока петля

Цикл while имеет условие и тело. Он многократно оценивает тело в цикле, пока условие истинно - условие оценивается в начале каждой итерации.

Поскольку ничего полезного не возвращается, он возвращаетUnit.

Давайте воспользуемся циклом while, чтобы написать метод вычисления наибольшего общего делителя:

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
}

Затем проверим результат:

assertEquals(3, gcdIter(15, 27))

4.3. Делать во время цикла

Цикл do while похож на цикл while, за исключением того, что условие цикла вычисляется в конце цикла.

Используя цикл do-while, напишем метод вычисления факториала:

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

Далее проверим результат:

assertEquals(720, factorial(6))

4.4. Для выражения

Выражение for гораздо более универсально, чем цикл for в Java.

Он может перебирать одну или несколько коллекций. Более того, он может фильтровать элементы, а также создавать новые коллекции.

Используя выражение for, напишем метод для суммирования диапазона целых чисел:

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

Здесьa to b - выражение генератора. Он генерирует серию значений отa доb.

i ← a to b  - генератор. Он определяетasval и присваивает ему серию значений, созданных выражением генератора.

Тело выполняется для каждого значения в серии.

Далее проверим результат:

assertEquals(55, rangeSum(1, 10))

5. функции

Скала - это функциональный язык. Здесь функции являются первоклассными значениями - мы можем использовать их как любой другой тип значения.

В этом разделе мы рассмотрим некоторые сложные концепции, связанные с функциями - локальные функции, функции высшего порядка, анонимные функции и каррирование.

5.1. Локальные функции

We can define functions inside functions. Они называются вложенными функциями или локальными функциями. Аналогично локальным переменнымthey are visible only within the function they are defined in.

Теперь давайте напишем метод вычисления мощности с помощью вложенной функции:

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)
}

Далее проверим результат:

assertEquals(8, power(2, 3))

5.2. Функции высшего порядка

Поскольку функции являются значениями, мы можем передать их в качестве параметров другой функции. У нас также может быть функция, возвращающая другую функцию.

We refer to functions which operate on functions as higher-order functions. Они позволяют нам работать на более абстрактном уровне. Используя их, мы можем уменьшить дублирование кода, написав обобщенные алгоритмы.

Теперь давайте напишем функцию высшего порядка для выполнения сопоставления и сокращения операций над диапазоном целых чисел:

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)
}

Здесьr иm - параметры типаFunction. Передавая различные функции, мы можем решить ряд задач, таких как сумма квадратов или кубов и факториал.

Затем давайте воспользуемся этой функцией, чтобы написать еще одну функциюsumSquares, которая суммирует квадраты целых чисел:

@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))
}

Выше видно, чтоhigher-order functions tend to create many small single-use functions. We can avoid naming them by using anonymous functions.

5.3. Анонимные функции

Анонимная функция - это выражение, которое оценивает функцию. Это похоже на лямбда-выражение в Java.

Давайте перепишем предыдущий пример, используя анонимные функции:

@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))
}

В этом примереmapReduce получает две анонимные функции:(x, y) ⇒ x + y иx ⇒ x * x.

Scala can deduce the parameter types from context. Поэтому мы опускаем тип параметров в этих функциях.

Это приводит к более краткому коду по сравнению с предыдущим примером.

5.4. Функции карри

A curried function takes multiple argument lists, such as def f(x: Int) (y: Int). Он применяется путем передачи нескольких списков аргументов, как в f (5) (6).

It is evaluated as an invocation of a chain of functions. Эти промежуточные функции принимают единственный аргумент и возвращают функцию.

Мы также можем частично указать списки аргументов, напримерf(5).

Теперь давайте разберемся с этим на примере:

@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))
}

Выше,sum иmod принимают по два списка аргументов. Мы передаем два списка аргументов, напримерmod(5)(6). Это оценивается как два вызова функции. Сначала оцениваетсяmod(5), который возвращает функцию. Это, в свою очередь, вызывается с аргументом6. . В результате мы получаем 1.

Можно частично применить параметры, так как вmod(5).  в результате мы получаем функцию.

Точно так же в выраженииsum(mod(5)) _ мы передаем только первый аргумент функцииsum. Следовательно,sumMod5 - функция.

Подчеркивание используется в качестве заполнителя для неприменяемых аргументов. Поскольку компилятор не может сделать вывод, что тип функции ожидается, мы используем завершающее подчеркивание, чтобы сделать возвращаемый тип функции явным.

5.5. Параметры имени

Функция может применять параметры двумя различными способами - по значению и по имени - она ​​оценивает аргументы по значению только один раз во время вызова. В отличие от этого, он оценивает аргументы по имени каждый раз, когда они ссылаются. Если аргумент по имени не используется, он не оценивается.

Scala по умолчанию использует параметры по значению. Если перед типом параметра стоит стрелка (⇒), он переключается на параметр по имени.

Теперь давайте воспользуемся им для реализации цикла while:

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

Чтобы указанная выше функция работала правильно, оба параметраcondition иbody должны оцениваться каждый раз при обращении к ним. Поэтому мы определяем их как параметры по имени.

6. Определение класса

Мы определяем класс с ключевым словомclass, за которым следует имя класса.

После имениwe can specify primary constructor parameters. Doing so automatically adds members with the same name to the class.

В теле класса мы определяем члены - значения, переменные, методы и т. Д. They are public by default unless modified by the private or protected access modifiers.с

Мы должны использовать ключевое словоoverride, чтобы переопределить метод из суперкласса.

Давайте определим класс 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)"
}

Здесь мы указываем три параметра конструктора -name,salary иannualIncrement.

Поскольку мы объявляемname иsalary с ключевыми словамиval иvar, соответствующие члены являются общедоступными. С другой стороны, мы не используем ключевое словоval илиvar для параметраannualIncrement. Поэтому соответствующий член является частным. Поскольку мы указываем значение по умолчанию для этого параметра, мы можем опустить его при вызове конструктора.

Помимо полей, мы определяем методincrementSalary. Этот метод является публичным.

Затем давайте напишем модульный тест для этого класса:

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

6.1. Абстрактный класс

Мы используем ключевое словоabstract, чтобы сделать класс абстрактным. Это похоже на Java. Он может иметь всех членов, которые могут быть в обычном классе.

Кроме того, он может содержать абстрактные члены. Это члены с простым объявлением и без определения, их определение приведено в подклассе.

Подобно Java, мы не можем создать экземпляр абстрактного класса.

Теперь давайте проиллюстрируем абстрактный класс на примере.

Во-первых, давайте создадим абстрактный классIntSet для представления набора целых чисел:

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
}

Затем давайте создадим конкретный подклассEmptyIntSet для представления пустого набора:

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

Затем другой подклассNonEmptyIntSet представляет непустые множества:

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)
    }
}

Наконец, давайте напишем модульный тест дляNonEmptySet:

@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. Черты

Черты соответствуют интерфейсам Java со следующими отличиями:

  • может расширяться из класса

  • может получить доступ к членам суперкласса

  • может иметь операторы инициализатора

Мы определяем их так же, как и классы, но используя ключевое словоtrait. Кроме того, они могут иметь те же члены, что и абстрактные классы, за исключением параметров конструктора. Кроме того, они предназначены для добавления в какой-то другой класс как миксин.

Теперь давайте проиллюстрируем черты характера на примере.

Во-первых, давайте определим чертуUpperCasePrinter, чтобы методtoString возвращал значение в верхнем регистре:

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

Затем давайте проверим эту черту, добавив ее в классEmployee:

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

Классы, объекты и признаки могут наследовать не более одного класса, но любое количество признаков.

7. Определение объекта

Объекты являются экземплярами класса. Как мы видели в предыдущих примерах, мы создаем объекты из класса, используя ключевое словоnew.

Однако, если у класса может быть только один экземпляр, нам нужно предотвратить создание нескольких экземпляров. В Java мы используем шаблон Singleton для достижения этой цели.

Для таких случаев у нас есть краткий синтаксис, называемый определением объекта - похожий на определение класса с одним отличием. Instead of using the class keyword, we use the object keyword. Это определяет класс и лениво создает его единственный экземпляр.

Мы используем определения объектов для реализации служебных методов и синглтонов.

Давайте определим объектUtils:

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

Здесь мы определяем классUtils, а также создаем его единственный экземпляр.

We refer to this sole instance using its nameUtils. Этот экземпляр создается при первом обращении к нему.

Мы не можем создать еще один экземпляр Utils, используя ключевое словоnew.

Теперь давайте напишем модульный тест для объектаUtils:

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

7.1. Объект-компаньон и класс-компаньон

Если класс и определение объекта имеют одинаковые имена, мы называем их как сопутствующий класс и сопутствующий объект соответственно. Нам нужно определить оба в одном файле. Сопутствующие объекты могут получать доступ к закрытым членам из своего класса-компаньона и наоборот.

В отличие от Java,we do not have static members. Вместо этого мы используем сопутствующие объекты для реализации статических членов.

8. Сопоставление с образцом

Pattern matching matches an expression to a sequence of alternatives.  Каждый из них начинается с ключевого словаcase. За ним следует узор, разделительная стрелка (⇒) и ряд выражений. Выражение оценивается, если шаблон соответствует.

Мы можем построить шаблоны из:

  • конструкторы класса case

  • переменная структура

  • шаблон подстановки _

  • литералы

  • постоянные идентификаторы

Классы Case упрощают сопоставление с образцом для объектов. Мы добавляем ключевое словоcase при определении класса, чтобы сделать его классом case.

Таким образом, сопоставление с образцом намного мощнее, чем оператор switch в Java. По этой причине это широко используемая языковая функция.

Теперь давайте напишем метод Фибоначчи с использованием сопоставления с образцом:

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

Теперь давайте напишем модульный тест для этого метода:

assertEquals(13, fibonacci(6))

9. Заключение

В этом уроке мы представили язык Scala и некоторые его основные функции. Как мы уже видели, он обеспечивает отличную поддержку императивного, функционального и объектно-ориентированного программирования.

Как обычно, полный исходный код можно найтиover on GitHub.