Подготовка к тестовому в DINS Scala School
Тестовое задание ты будешь писать уже на Scala. Для этого достаточно как следует вникнуть в основы, о которых мы расскажем на этой странице.
Самое главное
Типы
Int
Int
— целое число, хранится в ячейке памяти длиной 32 бита.
1 2 3 4 5 |
2 + 2 // 4 5 - 3 // 2 7 * 8 // 56 17 / 3 // 5 11 % 3 // 2 |
String
String
— строка любой длины. Записывается в двойных кавычках.
1 2 3 |
"Hello " + "world!" // "Hello world!" "" // пустая строка "hello".length // 5 |
Boolean
Boolean
— логическое значение. Записывается как true
или false
, обязательно маленькими буквами.
1 2 3 |
!false // = true, операция отрицания true || false // = true, операция "или" true && false // = false, операция "и" |
Любые значения одного типа можно проверить на равенство (==
) или неравенство (!=
).
1 2 3 4 |
2 == 5 // false "а" == "а" // true true == false // false "а" != "б" // true |
Попробовать вживую
Если хочется запустить код из примеров или написать свой, воспользуйся Scastie, он работает в браузере, скачивать ничего не нужно. Убедись, что настройка Worksheet в панели над текстовой областью включена, и напиши что-нибудь. Чтобы запустить, нажми кнопку Save.
После запуска напротив каждой строчки появится результат её выполнения. Если вызван println()
, вывод из него будет в консоли внизу. Например, println(10)
выведет в консоль 10.
Переменные
Переменные можно объявлять с помощью ключевого слова var
. Значения таких переменных можно изменять в любой момент.
1 2 3 |
var x = 1 + 1 x = 3 x * x // 9 |
Типы переменных могут быть выведены автоматически, но можно и явно указать тип, как показано ниже:
1 |
var x: Int = 1 + 1 |
Обрати внимание, что объявление типа Int
происходит после идентификатора x, следующим за двоеточием.
Значения
Результаты выражений можно присваивать именам с помощью ключевого слова val
.
Такие переменные называются значениями. Вызов значения не приводит к его повторному вычислению.
В отличие от обычных переменных, значения не изменяемы и не могут быть переназначены. Например, следующий фрагмент кода не получится собрать и запустить:
1 2 3 |
val x = 1 + 1 x // 2 x = 3 // Не компилируется. |
Как и в случае с переменными, можно явно указать тип:
1 |
val x: Int = 1 + 1 |
Методы
Методы задаются ключевым словом def
. За def
следует имя, список параметров, возвращаемый тип и тело.
1 2 |
def add(x: Int, y: Int): Int = x + y add(1, 2) // 3 |
Обрати внимание, как объявлен возвращаемый тип сразу после списка параметров и двоеточия : Int
.
Методы могут принимать несколько списков параметров.
1 2 |
def addThenMultiply(x: Int, y: Int)(multiplier: Int): Int = (x + y) * multiplier addThenMultiply(1, 2)(3) // 9 |
Или вообще ни одного списка параметров.
1 2 |
def name: String = "username" "Hello, " + name + "!" |
Методы также могут иметь многострочные выражения.
1 2 3 4 5 |
def getSquarePlusOne(input: Int): Int = { val square = input * input square + 1 } getSquarePlusOne(2) // 5 |
Последнее выражение в теле становится возвращаемым значением метода.
Когда не имеет смысла что-либо возвращать, используется тип Unit
(аналогично void
в Java и C). Поскольку каждое выражение Scala должно иметь какое-то значение, то при отсутствии возвращающегося значения вместо него используется экземпляр типа Unit. Явным образом его можно задать как ()
, он не несет какой-либо информации.
1 2 |
def greet(prefix: String, name: String, suffix: String): Unit = println(prefix + name + suffix) |
List
List
— это односвязный неизменяемый список. Каждый элемент списка содержит значение и ссылку на следующий элемент списка. Последний элемент содержит ссылку на пустой список, который не содержит никаких значений. Для List
пустой список называется Nil
. Значения и их порядок нельзя изменить, как мы бы это сделали с массивом, но можно создать новый список, с другими элементами.
Первый элемент списка называют головой списка (head
), а всю остальную его часть — хвостом (tail
).
List
можно создать так:
1 2 3 |
val ints = 1 :: 2 :: 3 :: 4 :: 5 :: Nil val names = "Аркадий" :: "Анатолий" :: "Анжела" :: Nil val alsoInts = List(1, 2, 3, 4, 5) // такая запись тоже возможна, список в данном случае будет равен списку ints |
Также можно указать тип списка: тип элементов внутри указывается в квадратных скобках после слова List
. Это может быть полезно при объявлении входных и выходных параметров методов и при создании пустого List
: компилятору негде взять информацию о типе элементов внутри.
1 2 3 |
val ints: List[Int] = 1 :: 2 :: 3 :: 4 :: 5 :: Nil val names: List[String] = "Аркадий" :: "Анатолий" :: "Анжела" :: Nil val noNumbers: List[Int] = Nil |
Чтобы создать список определенного размера с одинаковыми элементами, можно использовать List.fill
:
1 2 3 4 5 |
val zeroes: List[Int] = List.fill(5)(0) // List(0, 0, 0, 0, 0) val ivans: List[String] = List.fill(4)("Иван") // List("Иван", "Иван", "Иван", "Иван") |
Можно добавить элемент в начало списка или склеить несколько списков подряд. Поскольку список односвязный, добавление элемента в начало — это достаточно быстрая операция: можно создать головной элемент, а в качестве ссылки на остальную часть списка указать уже существующий список.
Обрати внимание, что поскольку список неизменяемый, все обновления записываются в новые значения val
.
1 2 3 4 5 6 7 8 |
val names: List[String] = "Аркадий" :: "Анатолий" :: "Анжела" :: Nil val ivans: List[String] = List.fill(4)("Иван") val namesExtended = "Анфиса" :: names // List("Анфиса", "Аркадий", "Анатолий", "Анжела") val namesExtended2 = names ::: ivans // List("Аркадий", "Анатолий", "Анжела", "Иван", "Иван", "Иван", "Иван") |
map
У List
есть метод map
. Он принимает на вход функцию и возвращает последовательность, в которой каждый элемент заменен на результат применения этой функции к элементу изначальной коллекции.
1 2 3 |
val intsList = List(1, 2, 3) val intsListIncreased = intsList.map(x => x + 10) // List(11, 12, 13) |
Функция, которую мы передали в качестве входного параметра, состоит из двух частей: слева от стрелочки (=>
) находится название переменной, в которой во время выполнения map окажется один из элементов последовательности, а справа — блок кода, который использует эту переменную, чтобы вычислить новое значение элемента списка.
Блок кода справа от стрелочки может состоять из нескольких строк, но его придется взять в фигурные скобки:
1 2 3 4 5 6 7 |
val intsList = List(1, 2, 3) val intsListModified = intsList.map(x => { val xSquared = x * x val xDecreased = x - 1 xSquared * xDecreased }) // intsListModified = List(0, 4, 18) |
Название переменной слева от стрелочки можно придумать любое.
flatMap
flatMap
работает примерно так же, как map
: принимает на вход функцию и возвращает последовательность, в которой каждый элемент заменен на результат применения этой функции к элементу изначальной коллекции. Единственное отличие в том, что эта функция возвращает не одно значение, а последовательность значений, и все они будут вставлены на место изначального элемента.
1 2 3 |
val intsList = List(1, 2, 3) val intsListDuplicated = intsList.flatMap(x => List.fill(x)(x)) // List(1, 2, 2, 3, 3, 3) |
Паттерн-матчинг
Паттерн-матчинг используется с той же целью, что и цепочки if-else: чтобы предоставить различное поведение в зависимости от заданных условий, но делает это более структурированно. Например, попробуем вывести строку в зависимости от значения x:
1 2 3 4 5 6 7 |
val x = 2 x match { case 0 => "zero" case 1 => "one" case 2 => "two" case _ => "many" } //"two" |
Последняя ветвь выражения (case _ =>
) не накладывает никаких ограничений, то есть будет выполнена, если никакие из ветвей, объявленных выше, не сработают.
Вместо _
можно объявить значение и использовать его в части после =>
, например:
1 2 3 4 5 6 |
val int = 2 val newInt = int match { case 0 => 1 case other => other * 2 } // newInt = 4 |
Таким образом, если после слова case написано конкретное значение (например, false
, 0
или "десять"
), то будет проверено, соответствует ли переменная этому значению. Если после слова case написать название новой переменной, то эта переменная будет объявлена со значением изначальной переменной (в примере выше значение other совпадает со значением int). Вместо названия новой переменной можно использовать символ _
, тогда новая переменная не создастся.
К выражению case
можно добавить условие:
1 2 3 4 5 6 7 8 9 10 |
val special = 3 def newInt(number: Int) = number match { case 0 => 1 case other if other == special => other case other => other * 2 } // newInt(5) = 10 // newInt(0) = 1 // newInt(3) = 3 |
Паттерн-матчинг выполняется построчно сверху вниз: для каждого case проверяется, выполняются ли условия, описанные в нём. Если да, выполняется часть выражения после =>
, а следующие case игнорируются. Если нет, проверяется следующий case. Если все case проверены, а подходящего так и не нашлось, паттерн-матчинг завершается с ошибкой.
Паттерн-матчинг может использоваться внутри map и flatMap, тогда название переменной и слово match можно пропустить:
1 2 3 4 5 |
List(1, 2, 3).map { case num if num % 2 == 0 => num * 10 case num => num } // List(1, 20, 3) |
Паттерн-матчинг и List
List позволяет разделить себя на голову и хвост прямо в выражении case
, если используется оператор ::
. Слева от этого оператора будет записана голова, а справа — хвост.
::
матчит любой непустой список. Список из одного элемента будет разделен на значение этого элемента и пустой список. Пустой список так разделить не получится, поэтому для него нужна отдельная ветвь: case Nil => ...
Хвост можно таким же образом распаковать с помощью оператора ::
в том же выражении case
.
Например:
1 2 3 4 5 6 7 8 9 10 11 12 |
val intList = List(1, 2, 3) val newList = intList match { case head :: tail => head * 2 :: tail case Nil => Nil } // newList = List(2, 2, 3) val sumOfTwoFirst = intList match { case first :: second :: _ => first + second case _ => 0 } // sumOfTwoFirst = 3 |
Обрати внимание, если мы попробуем разделить список на больше частей, чем он содержит, будет считаться, что это выражение case
не сматчилось, и будет проверен следующий case
:
1 2 3 4 5 6 7 8 9 |
List(1, 2) match { case first :: second :: _ => first + second case _ => 0 } // 3, так как список разделен на части следующим образом: 1 :: 2 :: Nil List(1) match { case first :: second :: _ => first + second case _ => 0 } // 0, так как список из одного элемента нельзя разделить на три части |
Tuple
Tuple позволяет объединить несколько переменных в одной. Эти переменные могут быть одного или разных типов. К переменной внутри tuple можно обратиться по номеру:
1 2 3 4 5 6 7 8 |
val twoInts = (1, 5) twoInts._1 // = 1 twoInts._2 // = 5 val differentTypes = (1, false, "строка") differentTypes._1 // = 1 differentTypes._2 // = false differentTypes._3 // = "строка" |
На практике к элементам tuple чаще не обращаются по номеру, а распаковывают во время паттерн-матчинга:
1 2 3 4 5 6 7 8 9 |
val string = "десять" val int = 10 (string, int) match { case ("пять", 5) => 0 case ("шесть", _) => 6 case (string, i) if i != 10 => string.length case (_, value) => value } // 10 |
У последовательностей есть метод zipWithIndex
, который из каждого элемента последовательности создает tuple, состоящий из номера этого элемента и самого элемента последовательности. Элементы последовательностей, в отличие от элементов tuple, нумеруются с 0.
1 2 3 4 5 |
val intsList = List(10, 20, 30, 40) val intsListDuplicated = intsList.zipWithIndex.flatMap { case (element, index) => List.fill(index)(element) } // List(20, 30, 30, 40, 40, 40) |
Хвостовая рекурсия
Когда мы работаем с неизменяемыми структурами данных, часто удобнее использовать не цикл, а рекурсию. Обычно рекурсия может привести к переполнению стека, но в scala есть встроенный механизм, который автоматически преобразует рекурсию в цикл. Достаточно написать хвостовую рекурсию: это рекурсивная функция, которая либо возвращает значение, либо возвращает результат вызова самой себя.
Можно добавить к функции аннотацию @tailrec
: сборка завершится ошибкой, если функция с такой аннотацией использует нехвостовую рекурсию или не использует рекурсию совсем. Обрати внимание, что для использования этой аннотации в начале файла должна быть строка import scala.annotation.tailrec
, так как этой аннотации нет в стандартной области видимости.
1 2 3 4 5 6 7 8 9 10 11 12 |
import scala.annotation.tailrec @tailrec def twoLastElements(list: List[Int]): List[Int] = { list match { case first :: second :: Nil => list case head :: tail => twoLastElements(tail) case Nil => Nil } } twoLastElements(List(1, 2, 3, 4, 5)) // List(4, 5) |
Хвостовая рекурсия неудобна, если нужно произвести вычисления на каждом этапе, а не только на последнем. В таком случае можно создать другую функцию, добавив в список входных параметров значение, которое будет накапливать в себе результаты всех прошлых итераций.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import scala.annotation.tailrec def increaseAll(list: List[Int]): List[Int] = increaseAll(list, List()) @tailrec def increaseAll(list: List[Int], resultReversed: List[Int]): List[Int] = { list match { case head :: tail => increaseAll(tail, (head + 1) :: resultReversed) case Nil => resultReversed.reverse } } val list = List(1, 2, 3) val newList = increaseAll(list) // newList = List(2, 3, 4) |
Важный момент: поскольку мы снимаем значения с начала списка и добавляем их в начало результирующего списка, порядок будет инвертирован: первый элемент окажется последним, поэтому перед тем, как вернуть результат, мы вызываем метод reverse
у списка, который возвращает список в обратном порядке.
Входной тест
На входном тесте будет предложено несколько задач, которые будут похожи на примеры из этого файла.
Потренироваться можно на задачах из раздела «Working with lists» из Ninety-Nine Scala Problems.
Пример задачи
Напишите метод, который заменяет все вхождения элементов, равных replacing
, на элемент replacement
. Скопируйте объявление метода replace в Scastie и замените ???
на решение задачи
1 |
def replace(list: List[String], replacing: String, replacement: String): List[String] = ??? |
Варианты решения:
Вариант 1, с использованием map:
1 2 3 4 5 |
def replace(list: List[String], replacing: String, replacement: String): List[String] = list.map { case r if r == replacing => replacement case other => other } |
Вариант 2, с использованием рекурсии:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
def replace(list: List[String], replacing: String, replacement: String): List[String] = { @tailrec def replace(list: List[String], replacing: String, replacement: String, reversedAcc: List[String]): List[String] = { list match { case r :: tail if r == replacing => replace(tail, replacing, replacement, replacement :: reversedAcc) case other :: tail => replace(tail, replacing, replacement, other :: reversedAcc) case Nil => reversedAcc.reverse } } replace(list, replacing, replacement, Nil) } |