Статьи

Scala Tutorial – блоки кода, стиль кодирования, замыкания, проект документации scala

Предисловие

Это 12 часть учебных пособий для начинающих программистов, попадающих в Scala. Другие посты находятся в этом блоге, и вы можете получить ссылки на эти и другие ресурсы на странице ссылок курса по компьютерной лингвистике, для которого я их создаю. Кроме того, вы можете найти эту и другие серии учебников на странице JCG Java Tutorials .

Этот пост не столько учебник, сколько комментарий к стилю кодирования с несколькими указателями на то, как работают блоки кода в Scala. Это было спровоцировано шаблонами, которые я отмечал в коде моих студентов; а именно то, что они упаковывали все в однострочники с картой за картой с картой за картой и т. д. Эти последовательности операторов map-over-mapValues-over-map могут быть практически несовместимы, как для какого-то другого человека, читающего код, так и даже для человека, пишущего код. Я признаю, что использую такие последовательности операций в классных лекциях и даже в некоторых из этих учебных пособий довольно много вины. Это хорошо работает в REPL и когда у вас есть много текста, чтобы объяснить, что происходит вокруг фрагмента кода, о котором идет речь, но, похоже, он дал плохую модель для написания реального кода. К сожалению!

Поэтому, делая шаг назад, важно немного разбить последовательность операций, но для новичков не всегда очевидно, как это можно сделать. Кроме того, некоторые студенты указали, что у них сложилось впечатление, что нужно постараться упаковать все в одну строку, если это возможно, и что разбиение было как-то менее продвинутым или менее похожим на Scala. Это вряд ли так. На самом деле многое наоборот: очень важно использовать стратегии, которые позволяют читателям вашего кода видеть логику ваших утверждений. Это не только для других — вы, вероятно, будете читателем своего собственного кода, часто спустя месяцы после того, как вы его изначально написали, и вы хотите быть добрыми к себе в будущем.

Простой пример

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

1
2
scala> val letters = "abcdefghijklmnopqrstuvwxyz".split("").toList.tail
letters: List[java.lang.String] = List(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z)

Хорошо, теперь вот наша (бессмысленная) задача: мы хотим создать карту из каждой буквы (от «а» до «х») в список, содержащий эту букву и две буквы, следующие за ней, в обратном алфавитном порядке. (Я упоминал, что это было бессмысленной задачей само по себе?) Вот один, который может это сделать.

1
2
scala> letters.zip((1 to 26).toList.sliding(3).toList).toMap.mapValues(_.map(x => letters(x-1)).sorted.reverse)
res0: scala.collection.immutable.Map[java.lang.String,List[java.lang.String]] = Map(e -> List(g, f, e), s -> List(u, t, s), x -> List(z, y, x), n -> List(p, o, n), j -> List(l, k, j), t -> List(v, u, t), u -> List(w, v, u), f -> List(h, g, f), a -> List(c, b, a), m -> List(o, n, m), i -> List(k, j, i), v -> List(x, w, v), q -> List(s, r, q), b -> List(d, c, b), g -> List(i, h, g), l -> List(n, m, l), p -> List(r, q, p), c -> List(e, d, c), h -> List(j, i, h), r -> List(t, s, r), w -> List(y, x, w), k -> List(m, l, k), o -> List(q, p, o), d -> List(f, e, d))

Это было сделано, но эта строка не совсем понятна, поэтому мы должны немного разобраться. Кроме того, что такое «_» и что такое «x»? (Под этим я подразумеваю, каковы они с точки зрения логики программы? Мы знаем, что это способы обращения к отображаемым элементам, но они не помогают человеку, читающему код, понять, что происходит.)

Начнем с создания скользящего списка диапазонов номеров.

1
2
scala> val ranges = (1 to 26).toList.sliding(3).toList
ranges: List[List[Int]] = List(List(1, 2, 3), List(2, 3, 4), List(3, 4, 5), List(4, 5, 6), List(5, 6, 7), List(6, 7, 8), List(7, 8, 9), List(8, 9, 10), List(9, 10, 11), List(10, 11, 12), List(11, 12, 13), List(12, 13, 14), List(13, 14, 15), List(14, 15, 16), List(15, 16, 17), List(16, 17, 18), List(17, 18, 19), List(18, 19, 20), List(19, 20, 21), List(20, 21, 22), List(21, 22, 23), List(22, 23, 24), List(23, 24, 25), List(24, 25, 26))

Понятно, что это сейчас. (Функция скольжения — прекрасная вещь, особенно для задач обработки естественного языка.)

Затем мы сжимаем буквы с диапазонами и создаем карту из пар, используя toMap . Это создает карту из букв в списки из трех чисел. Обратите внимание, что длина двух списков различна: у букв 26 элементов, а у диапазонов 24, что означает, что последние два элемента букв (‘y’ и ‘z’) удаляются в сжатом списке.

1
2
scala> val letter2range = letters.zip(ranges).toMap
letter2range: scala.collection.immutable.Map[java.lang.String,List[Int]] = Map(e -> List(5, 6, 7), s -> List(19, 20, 21), x -> List(24, 25, 26), n -> List(14, 15, 16), j -> List(10, 11, 12), t -> List(20, 21, 22), u -> List(21, 22, 23), f -> List(6, 7, 8), a -> List(1, 2, 3), m -> List(13, 14, 15), i -> List(9, 10, 11), v -> List(22, 23, 24), q -> List(17, 18, 19), b -> List(2, 3, 4), g -> List(7, 8, 9), l -> List(12, 13, 14), p -> List(16, 17, 18), c -> List(3, 4, 5), h -> List(8, 9, 10), r -> List(18, 19, 20), w -> List(23, 24, 25), k -> List(11, 12, 13), o -> List(15, 16, 17), d -> List(4, 5, 6))

Обратите внимание, что мы могли бы разбить это на два этапа: сначала создать сжатый список, а затем вызвать toMap . Тем не менее, совершенно ясно, каково намерение, когда один архивирует два списка (создавая список пар), а затем немедленно использует toMap для него, так что это, безусловно, тот случай, когда имеет смысл поместить несколько операций в одну строку.

На этом этапе мы, конечно, можем обработать карту letter2range, используя одну строку .

1
2
scala> letter2range.mapValues(_.map(x => letters(x-1)).sorted.reverse)
res1: scala.collection.immutable.Map[java.lang.String,List[java.lang.String]] = Map(e -> List(g, f, e), s -> List(u, t, s), x -> List(z, y, x), n -> List(p, o, n), j -> List(l, k, j), t -> List(v, u, t), u -> List(w, v, u), f -> List(h, g, f), a -> List(c, b, a), m -> List(o, n, m), i -> List(k, j, i), v -> List(x, w, v), q -> List(s, r, q), b -> List(d, c, b), g -> List(i, h, g), l -> List(n, m, l), p -> List(r, q, p), c -> List(e, d, c), h -> List(j, i, h), r -> List(t, s, r), w -> List(y, x, w), k -> List(m, l, k), o -> List(q, p, o), d -> List(f, e, d))

Это лучше, чем то, с чего мы начали, потому что мы, по крайней мере, знаем, что такое letter2range , но до сих пор не ясно, что происходит после этого. Чтобы сделать это более понятным, мы можем разбить его на несколько строк и дать более понятные имена переменным. Следующее дает тот же результат, что и выше.

1
2
3
4
5
6
letter2range.mapValues (
  range => {
    val alphavalues = range.map (number => letters(number-1))
    alphavalues.sorted.reverse
  }
)

Заметь:

  • Я назвал это range, а не _, что является лучшим индикатором того, с чем работает mapValues .
  • После => я использую открытую левую скобку {
  • Следующие строки представляют собой блок кода, который я могу использовать как любой блок кода, что означает, что я могу создавать переменные и разбивать их на более мелкие, более понятные шаги. Например, строка, создающая буквенные значения, проясняет, что мы берем диапазон и сопоставляем его с соответствующими индексами в списке букв (например, диапазон 2, 3, 4 становится «b», «c», «d»). Для такого списка мы затем сортируем и переворачиваем его (хорошо, так что он сначала отсортирован, но вы можете себе представить, сколько раз вам нужно выполнить такую ​​сортировку).
  • Последняя строка этого блока — это результат общего mapValue для этого элемента (здесь, обозначенный переменной range ).

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

1
2
def lookupSortAndReverse (range: List[Int], alpha: List[String]) =
  range.map(number => alpha(number-1).sorted.reverse)

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

1
letter2range.mapValues(range => lookupSortAndReverse(range, letters))

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

Затворы

Еще одно последнее замечание. Обратите внимание, что я передал список букв в функцию lookupSortAndReverse , чтобы его значение было связано с внутренней переменной функции alpha . Вы можете спросить, нужно ли мне это включить, или есть ли возможность прямого доступа к списку букв в функции. На самом деле вы можете: при условии, что буквы уже определены , мы можем сделать следующее.

1
2
3
4
def lookupSortAndReverseCapture (range: List[Int]) =
  range.map(number => letters(number-1).sorted.reverse)
 
letter2range.mapValues(range => lookupSortAndReverseCapture(range))

Это называется замыканием , что означает, что в функцию включены свободные переменные (здесь буквы ), которые выходят за пределы своей области видимости. Я обычно не использую эту стратегию с именованными функциями, как это, но есть много естественных ситуаций для использования замыканий. Фактически вы делаете это все время, когда создаете анонимные функции в качестве аргументов для таких функций, как map и mapValue и их родственников. Напомним, что здесь была анонимная функция map-inside-a-mapValue, которую мы определили ранее.

1
2
3
4
5
6
letter2range.mapValues (
  range => {
    val alphavalues = range.map (number => letters(number-1))
    alphavalues.sorted.reverse
  }
)

Переменная букв была «закрыта» в анонимной функции range => {…} , что не очень отличается от того, что мы сделали с функцией lookupSortAndReverse в стиле замыкания .

Весь код в одном месте

Поскольку между разными шагами в этом руководстве есть некоторые зависимости, которые могут все перепутать, здесь весь код в одном месте, так что вы можете легко его запустить.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Get a list of the letters
val letters = "abcdefghijklmnopqrstuvwxyz".split("").toList.tail
 
// Now create a list that maps each letter to a list containing itself
// and the two letters after it, in reverse alphabetical
// order. (Bizarre, but hey, it's a simple example. BTW, we lose y and
// z in the process.)
 
letters.zip((1 to 26).toList.sliding(3).toList).toMap.mapValues(_.map(x => letters(x-1)).sorted.reverse)
 
// Pretty unintelligible. Let's break things up a bit
 
val ranges = (1 to 26).toList.sliding(3).toList
val letter2range = letters.zip(ranges).toMap
letter2range.mapValues(_.map(x => letters(x-1)).sorted.reverse)
 
// Okay, that's better. But it is easier to interpret the latter if we break things up a bit
 
letter2range.mapValues (
  range => {
    val alphavalues = range.map (number => letters(number-1))
    alphavalues.sorted.reverse
  }
)
 
// We can also do the one-liner coherently if we have a helper function.
 
def lookupSortAndReverse (range: List[Int], alpha: List[String]) =
  range.map(number => alpha(number-1).sorted.reverse)
 
letter2range.mapValues(range => lookupSortAndReverse(range, letters))
 
// Note that we can "capture" the letters value, though this makes the
// requires letters to be defined before lookupSortAndReverse in the
// program.
 
def lookupSortAndReverseCapture (range: List[Int]) =
  range.map(number => letters(number-1).sorted.reverse)
 
letter2range.mapValues(range => lookupSortAndReverseCapture(range))

Заворачивать

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

Обратите внимание, что одна простая вещь, которую вы можете сделать, чтобы создать лучший код, это попытаться придерживаться установленных правил кодирования. Например, см. Руководство по кодированию для Scala в проекте документации Scala . Есть также много других очень полезных вещей, включая учебные пособия, и они активно развиваются и растут!

Ссылка: Первые шаги в Scala для начинающих программистов, часть 12 от нашего партнера JCG Джейсона Болдриджа в блоге Bcomposes .

Статьи по Теме :