Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 747
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
13 .8 . Большой пример 297
поэтому понятно, что оператор op в паттерне
BinOp(op,
left,
right)
не может быть оператором деления. Чтобы форматировать такую бинарную операцию, вам нужно сначала отформатировать его операнды left и right
. В качестве параметра уровня приоритета для форматирования левого операнда ис
пользуется opPrec оператора op
, а для правого операнда берется уровень на единицу больше. Вдобавок такая схема обеспечивает правильную ассоциа
тивность, выраженную круглыми скобками.
Например, операция
BinOp("-", Var("a"), BinOp("-", Var("b"), Var("c")))
будет вполне корректно выражена с применением круглых скобок в виде a
-
(b
- c)
. Затем с помощью выстраивания в линию операндов left и right
, разделенных оператором, формируется промежуточный результат oper
. Если уровень приоритета текущего оператора ниже уровня приоритета операто
ра, заключенного в скобки, то oper помещается между круглыми скобками, в противном случае возвращается в исходном виде.
На этом разработка приватной функции format завершена. Остается только пуб личный метод format
, который позволяет программистам, создающим клиентский код, форматировать высокоуровневые выражения, не прибегая к передаче аргумента, содержащего уровень приоритета. Демонстрационная программа, с помощью которой проверяется
ExprFormatter
, показана в ли
стинге 13.22.
Листинг 13.22. Приложение, выполняющее вывод отформатированных выражений import org.stairwaybook.expr.*
object Express:
def main(args: Array[String]): Unit =
val f = new ExprFormatter val e1 = BinOp("*", BinOp("/", Num(1), Num(2)),
BinOp("+", Var("x"), Num(1)))
val e2 = BinOp("+", BinOp("/", Var("x"), Num(2)),
BinOp("/", Num(1.5), Var("x")))
val e3 = BinOp("/", e1, e2)
def show(e: Expr) = println(s"${f.format(e)}\n\n")
for e <- Vector(e1, e2, e3) do show(e)
298 Глава 13 • Сопоставление с образцом
Поскольку этот объект определяет метод main
, он является работоспособным приложением. Запустить программу
Express можно командой scala Express
При этом будет получен следующий вывод:
1
- * (x + 1)
2
x 1.5
- + ---
2 x
1
- * (x + 1)
2
----------- x 1.5
- + ---
2 x
Резюме
В этой главе мы представили подробную информацию о case
классах и со
поставлении с образцом. Они позволяют получить преимущества в процессе использования ряда лаконичных идиом, которые обычно недоступны в объ
ектноориентированных языках. Но реализованное в Scala сопоставление с образцом не ограничивается тем, что было показано в данной главе. Если нужно задействовать сопоставление с образцом, но при этом нежелательно открывать доступ к вашим классам, как делается в case
классах, то можно обратиться к экстракторам. В следующей главе мы рассмотрим списки.
14
Работа со списками
Вероятно, наиболее востребованные структуры данных в программах на
Scala — списки. В этой главе мы подробно разъясним, что это такое. В ней мы представим много общих операций, которые могут выполняться над списками. Кроме того, раскроем некоторые наиболее важные принципы проектирования программ, работающих со списками.
14 .1 . Литералы списков
Списки уже попадались в предыдущих главах, следовательно, вам извест
но, что список, содержащий элементы 'a',
'b'
и 'c'
, записывается как
List('a',
'b',
'c')
. А вот другие примеры:
val fruit = List("apples", "oranges", "pears")
val nums = List(1, 2, 3, 4)
val diag3 =
List(
List(1, 0, 0),
List(0, 1, 0),
List(0, 0, 1)
)
val empty = List()
Списки очень похожи на массивы, но имеют два важных отличия. Во
первых, списки являются неизменяемой структурой данных, то есть их элементы нельзя изменить путем присваивания. Вовторых, списки имеют рекурсивную структуру (имеется в виду связанный список), а у массивов она линейная.
300 Глава 14 • Работа со списками
14 .2 . Тип List
Как и массивы, списки однородны: у всех элементов списка один и тот же тип. Тип списка, имеющего элементы типа
T
, записывается как
List[T]
. На
пример, далее показаны те же четыре списка, что и выше, с явным указанием типов:
val fruit: List[String] = List("apples", "oranges", "pears")
val nums: List[Int] = List(1, 2, 3, 4)
val diag3: List[List[Int]] =
List(
List(1, 0, 0),
List(0, 1, 0),
List(0, 0, 1)
)
val empty: List[Nothing] = List()
В Scala тип списка обладает ковариантностью. Этот значит, что для каждой пары типов
S
и
T
, если
S
— подтип
T
, то
List[S]
— подтип
List[T]
. Например,
List[String]
— подтип
List[Object]
. И это вполне естественно, поскольку каждый список строк также может рассматриваться как список объектов
1
Обратите внимание: типом пустого списка является
List[Nothing]
No- thing считается «низшим типом» в иерархии классов Scala, так как он явля
ется подтипом любого другого имеющегося в Scala типа. Списки ковариант
ны, отсюда следует, что
List[Nothing]
— подтип
List[T]
для любого типа
T
Следовательно, пустой списочный объект, имеющий тип
List[Nothing]
, также может рассматриваться в качестве объекта любого другого списочного типа, имеющего вид
List[T]
. Именно поэтому вполне допустимо написать такой код:
// List() также относится к типу List[String]!
val xs: List[String] = List()
14 .3 . Создание списков
Все списки создаются из двух основных строительных блоков:
Nil и
::
(про
износится как «конс», от слова «конструировать»).
Nil представляет собой пустой список. Инфиксный оператор конструирования
::
обозначает рас
ширение списка с начала. То есть запись x
::
xs представляет собой список, первым элементом которого является x
, за которым следует список xs
(его
1
Более подробно ковариантность и другие разновидности вариантности рассмотре
ны в главе 18.
14 .4 . Основные операции над списками 301
элементы). Следовательно, предыдущие списочные значения можно опре
делить так:
val fruit = "apples" :: ("oranges" :: ("pears" :: Nil))
val nums = 1 :: (2 :: (3 :: (4 :: Nil)))
val diag3 = (1 :: (0 :: (0 :: Nil))) ::
(0 :: (1 :: (0 :: Nil))) ::
(0 :: (0 :: (1 :: Nil))) :: Nil val empty = Nil
Фактически предыдущие определения fruit
, nums
, diag3
и empty
, выражен
ные в виде
List(...)
, всего лишь оболочки, которые разворачиваются в эти определения. Например, применение
List(1,
2,
3)
приводит к созданию списка
1
::
(2
::
(3
::
Nil))
То, что операция
::
заканчивается двоеточием, означает ее правую ассоциа
тивность:
A
::
B
::
C
интерпретируется как
A
::
(B
::
C)
. Поэтому круглые скобки в предыдущих определениях можно отбросить. Например val nums = 1 :: 2 :: 3 :: 4 :: Nil будет эквивалентом предыдущего определения nums
14 .4 . Основные операции над списками
Все действия со списками можно свести к трем основным операциям:
z z
head возвращает первый элемент списка;
z z
tail возвращает список, состоящий из всех элементов, за исключением первого;
z z
isEmpty возвращает true
, если список пуст.
Эти операции определены как методы класса
List
. Некоторые примеры их использования показаны в табл. 14.1. Методы head и tail определены только для непустых списков. Будучи примененными к пустому списку, они гене
рируют исключение:
scala> Nil.head java.util.NoSuchElementException: head of empty list
В качестве примера того, как можно обрабатывать список, рассмотрим сор тировку элементов списка чисел в возрастающем порядке. Один из про
стых способов выполнить эту задачу заключается в сортировке вставками,
1 ... 28 29 30 31 32 33 34 35 ... 64
302 Глава 14 • Работа со списками которая работает так: для сортировки непустого списка x
::
xs сортируется остаток xs и первый элемент x
вставляется в нужную позицию результата.
Таблица 14.1. Основные операции над списками
Что используется
Что этот метод делает
empty.isEmpty
Возвращает true fruit.isEmpty
Возвращает false fruit.head
Возвращает "apples"
fruit.tail.head
Возвращает "oranges"
diag3.head
Возвращает
List(1, 0, 0)
Сортировка пустого списка выдает пустой список. Выраженный в виде кода
Scala, алгоритм сортировки вставками выглядит так, как показано в листин
ге 14.1.
Листинг 14.1. Сортировка списка List[Int] с помощью алгоритма сортировки вставками def isort(xs: List[Int]): List[Int] =
if xs.isEmpty then Nil else insert(xs.head, isort(xs.tail))
def insert(x: Int, xs: List[Int]): List[Int] =
if xs.isEmpty || x <= xs.head then x :: xs else xs.head :: insert(x, xs.tail)
14 .5 . Паттерны-списки
Разбирать списки можно и с помощью сопоставления с образцом. Паттерны
списки по порядку следования соответствуют выражениям списков. Исполь
зуя паттерн вида
List(...)
, можно либо сопоставить все элементы списка, либо разобрать список поэлементно, применив паттерны, составленные из оператора
::
и константы
Nil
Пример использования первой разновидности паттерна выглядит следу
ющим образом:
scala> val List(a, b, c) = fruit val a: String = apples val b: String = oranges val c: String = pears
14 .5 . Паттерны-списки 303
Паттерн
List(a,
b,
c)
соответствует спискам длиной три элемента и привя
зывает эти три элемента к паттернампеременным a
, b
и c
. Если количество элементов заранее не известно, то лучше вместо этого сопоставлять с помо
щью оператора
::
. Например, паттерн a
::
b
::
rest соответствует спискам длиной два и более элемента:
scala> val a :: b :: rest = fruit val a: String = apples val b: String = oranges val rest: List[String] = List(pears)
О сопоставлении с образцом объектов класса List
Если провести беглый обзор возможных форм паттернов, рас
смотренных в главе 15, то выяснится, что ничего похожего ни на
List(...)
, ни на
::
в определенных там разновидностях нет. Фак
тически
List(...)
— экземпляр определенного в библиотеке пат
тернаэкстрактора. Конспаттерн x
::
xs
— особый случай паттерна инфиксной операции. В качестве выражения инфиксная операция выступает эквивалентом вызова метода. Для паттернов действуют иные правила: в качестве паттерна такая инфиксная операция, как p
op q
, является эквивалентом op(p,
q)
. То есть инфиксный оператор op рассматривается в качестве паттернаконструктора. В частности, такой конспаттерн, как x
::
xs
, рассматривается как
::(x,
xs)
Это обстоятельство подсказывает, что должен быть класс по имени
::
, соответствующий паттернуконструктору. Разумеется, это существу
ющий одноименный класс, который создает непустые списки. Следо
вательно,
::
в Scala фигурирует дважды: как имя класса и как метод класса
List
. Результатом применения метода
::
является создание экземпляра класса scala.::
Извлечение части списков с помощью паттернов — альтернатива использо
ванию основных методов head
, tail и isEmpty
. Например, в коде ниже снова применена сортировка вставками, на этот раз записанная с сопоставлением с образцом:
def isort(xs: List[Int]): List[Int] =
xs match case List() => List()
case x :: xs1 => insert(x, isort(xs1))
def insert(x: Int, xs: List[Int]): List[Int] =
304 Глава 14 • Работа со списками xs match case List() => List(x)
case y :: ys => if x <= y then x :: xs else y :: insert(x, ys)
Зачастую применение к спискам сопоставления с образцом оказывается более понятным, чем их декомпозиция с помощью методов, поэтому данное сопоставление должно стать частью вашего инструментария для обработки списков.
Вот и все, что нужно знать о списках в Scala, чтобы их правильно применять.
Но существует также множество методов, которые вбирают в себя наиболее распространенные схемы проведения операций со списками. Эти методы делают программы обработки списков более лаконичными и зачастую более понятными. В следующих двух разделах мы представим наиболее важные методы, определенные в классе
List
14 .6 . Методы первого порядка класса List
В этом разделе мы рассмотрим большинство определенных в классе
List методов первого порядка. Метод первого порядка не получает в качестве аргументов никаких функций. Мы также представим несколько рекомен
дованных приемов структурирования программ, работающих со списками, на двух примерах.
Конкатенация двух списков
Операцией, похожей на
::
, является конкатенация списков, записываемая в виде
:::
. В отличие от операции
::
операция
:::
получает в качестве операндов два списка. Результатом выполнения кода xs
:::
ys выступает новый список, содержащий все элементы списка xs
, за которыми следуют все элементы списка ys
Рассмотрим несколько примеров:
List(1, 2) ::: List(3, 4, 5) // List(1, 2, 3, 4, 5)
List() ::: List(1, 2, 3) // List(1, 2, 3)
List(1, 2, 3) ::: List(4) // List(1, 2, 3, 4)
Как и консоператор, конкатенация списков правоассоциативна. Такое вы
ражение, как xs ::: ys ::: zs
14 .6 . Методы первого порядка класса List 305
интерпретируется следующим образом:
xs ::: (ys ::: zs)
Принцип «разделяй и властвуй»
Конкатенация (
:::
) реализована в виде метода класса
List
. Можно было бы также реализовать конкатенацию «вручную», используя сопоставле
ние с образцом для списков. Будет поучительно попробовать сделать это самостоятельно, поскольку таким образом можно проследить общий путь реализации алгоритмов с помощью списков. Сначала мы остановимся на сигнатуре метода конкатенации, который назовем append
. Чтобы не созда
вать большой путаницы, предположим, что append определен за пределами класса
List
, поэтому будет получать в качестве параметров два конкатени
руемых списка. Оба они должны быть согласованы по типу их элементов, но сам тип может быть произвольным. Все это можно обеспечить, задав append параметр типа
1
, представляющего тип элементов двух исходных списков:
def append[T](xs: List[T], ys: List[T]): List[T]
Чтобы спроектировать реализацию append
, имеет смысл вспомнить прин
цип «разделяй и властвуй» для программ, работающих с рекурсивными структурами данных, такими как списки. Многие алгоритмы для работы со списками сначала разбивают исходный список на простые блоки, ис
пользуя сопоставление с образцом. Это часть принципа, которая называ
ется «разделяй». Затем конструируется результат для каждого варианта.
Если результатом является непустой список, то некоторые из его частей можно сконструировать с помощью рекурсивных вызовов того же самого алгоритма. В этом будет заключаться часть принципа, называемая «вла-
ствуй».
Чтобы применить этот принцип к реализации метода append
, сначала стоит ответить на вопрос: какой именно список нужно сопоставлять? При
менительно к append данный вопрос не настолько прост, как для многих других методов, поскольку здесь имеются два варианта. Но следующая далее фаза «властвования» подсказывает: нужно сконструировать список, состоящий из всех элементов обоих исходных списков. Списки констру
ируются из конца в начало, поэтому ys можно оставить нетронутым, а вот xs нужно разобрать и пристроить впереди ys
. Таким образом, имеет смысл
1
Более подробно параметры типов будут рассмотрены в главе 18.