Эта серия статей предназначена для занятых программистов, которые хотят выучить Scala быстро, за 2 часа или меньше. Эти статьи представляют собой письменную версию мини-курса Rock the JVM Scala на Light Speed, которую вы можете найти бесплатно на YouTube или на веб-сайте Rock the JVM в виде видео.
Это третья статья серии, в которой речь пойдет о Scala как о функциональном языке программирования. Вы можете посмотреть его в виде видео здесь или во встроенном видео ниже.
Итак, мы рассмотрели:
- Как начать работу с Scala
- Основы: значения, выражения, типы
- Объектная ориентация: классы, экземпляры, синглтоны, методы и базовые дженерики
Вам также может понравиться: Scala со скоростью света, часть 1: основы и часть 2: ориентация на объекты
Что такое функция?
Из предыдущей части вы помните, что в Scala есть специальный apply
метод, который позволяет вызывать экземпляры классов (или одноэлементные объекты) как функции:
Scala
1
class Person(name: String) {
2
def apply(age: Int) = println(s"I have aged $age years")
3
}
4
val bob = new Person("Bob")
6
bob.apply(43)
7
bob(43) // INVOKING bob as a function === bob.apply(43)
Кроме того, давайте вспомним несколько моментов:
- Скала работает на вершине JVM.
- JVM была построена для Java, который является языком OO.
- Scala - это функциональный язык программирования, что означает, что мы работаем с функциями, как и с любыми другими значениями: мы передаем их, составляем их, возвращаем как результаты.
Для реализации идеала функционального программирования Scala обладает следующими характеристиками (упрощенно):
Scala
xxxxxxxxxx
1
trait Function1[T, R] {
2
def apply(argument: T): R
3
}
4
trait Function2[T1, T2, R] {
6
def apply(arg1: T1, arg2: T2): R
7
}
8
//... all the way to Function22
Поскольку apply позволяет нам вызывать экземпляры, подобные функциям, мы можем легко написать:
Scala
xxxxxxxxxx
1
val simpleIncrementer = new Function1[Int, Int] {
2
override def apply(arg: Int): Int = arg + 1
3
}
4
val newValue = simpleIncrementer(23) // same as simpleIncrementer.apply(23)
Мы только что определили функцию!
Значения функций Scala являются экземплярами Function1, Function2, ... Function22 , которые представляют собой не что иное, как простые черты apply
метода.
Выглядит как функциональное программирование
В функциональном программировании мы много работаем со значениями функций. Создавать их как анонимный класс каждый раз было бы больно. В Scala есть синтаксический сахар для создания значений функций:
Scala
xxxxxxxxxx
1
val doublerFunction = (x: Int) => x * 2
2
/*
3
same as:
4
5
val doublerFunction = new Function1[Int, Int] {
6
override def apply(x: Int) = x * 2
7
}
8
*/
Тип функции, определенный выше Function1[Int, Int]
, или Int => Int
для краткости (в этом отношении похож на Haskell).
Для тех, кто испытывает реальное беспокойство при наборе текста, у Scala есть еще более сокращенная запись:
Scala
xxxxxxxxxx
1
val shortIncrementer: Int => Int = _ + 1 // same as (x: Int) => x + 1
2
val multiplier: (Int, Int) => Int = _ * _ // same as (x: Int, y: Int) => x * y
Пока компилятор знает тип функции (как я явно объявил), вы можете использовать подчеркивания для каждого другого аргумента функции. Эта сокращенная запись особенно полезна для длинных цепочек методов с аргументами функции.
Hofs
Методы или функции, принимающие другие функции в качестве аргументов или возвращающие функции в качестве результатов, называются функциями высшего порядка (HOF).
Некоторые классические Hofs мы используем много с коллекциями map
, flatMap
и filter
. Давайте рассмотрим их по очереди в качестве примеров.
map
Метод коллекции примет функцию в качестве аргумента и вернет новую коллекцию, содержащую приложения функции для каждого элемента:
Scala
xxxxxxxxxx
1
val aMappedList: List[Int] = List(1,2,3).map(x => x + 1) // or .map(_ + 1), same thing
2
// List(2,3,4)
flatMap
Метод коллекции переносит функцию из элемента в другую коллекцию . Приложения функции для каждого элемента будут возвращать множество небольших коллекций, которые будут окончательно объединены в возвращенную коллекцию.
Scala
xxxxxxxxxx
1
val aFlatMappedList = List(1,2,3).flatMap { x =>
2
List(x, 2 * x)
3
}
4
// alternative syntax, same as .map(x => List(x, 2 * x))
5
// returns List(1,2,2,4,3,6) as the concatenation of [1,2], [2,4] and [4,6]
filter
Метод коллекции берет функцию из элемента в Boolean и возвращает новую коллекцию, содержащую только те элементы, для которых функция возвращает true.
Scala
xxxxxxxxxx
1
val aFilteredList = List(1,2,3,4,5).filter(_ <= 3)
2
// List(1,2,3)
Объединение map
, flatMap
и filter
является мощной техникой для манипулирования коллекциями. Пример: все пары объединяют число из списка [1,2,3] с символом из списка [a, b, c]:
Scala
xxxxxxxxxx
1
val allPairs = List(1,2,3)
2
.flatMap(number => List('a', 'b', 'c').map(letter => s"$number-$letter"))
3
Итак, для каждого числа мы берем список символов, а для каждого мы объединяем число и букву.
Это стиль мышления, который мы используем в Scala и функциональном программировании . Вместо того, чтобы перебирать коллекцию и вручную создавать значения, мы манипулируем существующими коллекциями с помощью их функций высшего порядка. Конечно, «манипулирование» - это неправильный термин, потому что мы работаем с неизменяемыми структурами данных, поэтому мы всегда возвращаем новую коллекцию.
Эти цепочки карт / плоских карт / фильтров на практике могут быть очень длинными и, следовательно, их становится все труднее читать. Scala предлагает еще один синтаксический сахар для этих больших цепочек карт / flatMap / фильтра в форме для понимания :
xxxxxxxxxx
1
val alternativePairs = for {
2
number <- List(1,2,3)
3
letter <- List('a','b','c')
4
} yield s"$number-$letter"
5
// equivalent to the map/flatMap chain above
Неправильное восприятие, чтобы развеять сразу же: это не цикл. Это выражение, потому что оно сводится к значению. Значение вычисляется цепочкой map / flatMap / filter, которую компилятор восстанавливает для вас, когда вы пишете для понимания. Если структура данных не предоставляет соответствующие map
/ flatMap
/ withFilter
метод, он не будет иметь права на получении в обмен на постижения и компилятор скажет вам об этом.
Краткий обзор коллекций
Безусловно, самая распространенная структура данных - это список . В стандартной библиотеке это реализовано в виде односвязного списка. Основными методами являются head
(первый элемент), tail
(оставшаяся часть списка) и ::
(добавление элемента).
Scala
xxxxxxxxxx
1
val aList = List(1,2,3,4,5)
2
val firstElement = aList.head
3
val rest = aList.tail
4
val aPrependedList = 0 :: aList // List(0,1,2,3,4,5)
5
val anExtendedList = 0 +: aList :+ 6 // List(0,1,2,3,4,5,6)
Супертипом для всех упорядоченных наборов элементов является Seq
(для последовательности ). Фундаментальный метод - это apply
метод, который обращается к элементу по заданному индексу в последовательности.
Scala
xxxxxxxxxx
1
// sequences
2
val aSequence: Seq[Int] = Seq(1,2,3) // Seq.apply(1,2,3)
3
val accessedElement = aSequence(1) // the element at index 1: 2
Для больших последовательных данных в памяти, Vector
это быстрая реализация Seq (реализована в виде полного 32-разрядного дерева). Он имеет тот же API, что и Seq.
Scala
xxxxxxxxxx
1
// vectors: fast Seq implementation
2
val aVector = Vector(1,2,3,4,5)
Sets
также полезны, потому что они не содержат дубликатов. Set
это просто черта, поэтому Set
метод apply компаньона возвращает некоторую реализацию Set
(обычно некоторый хэш-набор). Основными методами являются contains
(проверьте, есть ли элемент в наборе), +
и -
(для добавления и удаления элементов).
Scala
xxxxxxxxxx
1
// sets = no duplicates
2
val aSet = Set(1,2,3,4,1,2,3) // Set(1,2,3,4)
3
val setHas5 = aSet.contains(5) // false
4
val anAddedSet = aSet + 5 // Set(1,2,3,4,5)
5
val aRemovedSet = aSet - 3 // Set(1,2,4)
Диапазоны полезны для генерации чисел. Они имеют тот же API, что и Seq, но не обязательно должны содержать все числа.
Scala
xxxxxxxxxx
1
// ranges
2
val aRange = 1 to 1000
3
val twoByTwo = aRange.map(x => 2 * x).toList // List(2,4,6,8..., 2000)
Не спрашивайте об to
операторе выше (пока). Обратите внимание , что вместо этого мы можем конвертировать между различными коллекциями с toList
, toSet
, и toSeq
т.д.
Наконец, мультитиповые коллекции. Начнем с того tuples
, что это группы значений с одинаковым значением (ребята из Python должны быть знакомы):
Scala
xxxxxxxxxx
1
// tuples = groups of values under the same value
2
val aTuple = ("Bon Jovi", "Rock", 1982)
А для ассоциаций ключ-значение мы приводим maps
их основные методы, приведенные ниже.
Scala
xxxxxxxxxx
1
// maps
2
val aPhonebook: Map[String, Int] = Map(
3
("Daniel", 6437812),
4
"Jane" -> 327285 // same as ("Jane", 327285)
5
)
6
val aNewPhonebook = aPhonebook + ("Mary" -> 5731) // add/replace association
8
val removedDaniel = aPhonebook - "Daniel" // remove key
9
val hasJane = aPhoneBook.contains("Jane")
10
val danielsNumber = aPhonebook("Daniel") // beware this can throw if key not present
В следующей части мы поговорим об одной из самых мощных функций Scala: сопоставлении с образцом.