Статьи

Scala Tutorial — итерация, для выражений, yield, map, filter, count

Предисловие

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

Этот учебник отличается от самого начального характера предыдущих трех, так что он может быть более интересным для читателей, которые уже имеют некоторый опыт программирования на другом языке. (Хотя также, смотрите раздел об использовании сопоставления в Scala в Части 3. )

Итерация, способ (ы) Scala

До сих пор мы (в основном) обращались к отдельным элементам в списке, используя их индексы. Но одна из самых естественных вещей, которые нужно сделать со списком, — это повторить какое-то действие для каждого элемента в списке, например: «Для каждого слова в данном списке слов: напечатать его». Вот как это сказать в Scala.

1
2
3
4
5
6
7
8
scala> val animals = List("newt", "armadillo", "cat", "guppy")
animals: List[java.lang.String] = List(newt, armadillo, cat, guppy)
  
scala> animals.foreach(println)
newt
armadillo
cat
guppy

Это говорит о том, чтобы взять каждый элемент списка (обозначенный foreach ) и применить к нему функцию (в данном случае println ) по порядку. Существует некоторая недостаточная спецификация, заключающаяся в том, что мы не предоставляем переменную для именования элементов. Это работает в некоторых случаях, например, выше, но не всегда возможно. Вот как это выглядит полностью, с переменной, именующей элемент.

1
2
3
4
5
scala> animals.foreach(animal => println(animal))
newt
armadillo
cat
guppy

Это полезно, когда вам нужно сделать немного больше, например объединить элемент String с другой строкой.

1
2
3
4
5
scala> animals.foreach(animal => println("She turned me into a " + animal))
She turned me into a newt
She turned me into a armadillo
She turned me into a cat
She turned me into a guppy

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

1
2
3
4
5
scala> animals.foreach(animal => println(animal.length))
4
9
3
5

Мы можем получить тот же результат, что и foreach, используя выражение for .

1
2
3
4
5
scala> for (animal <- animals) println(animal.length)
4
9
3
5

С тем, что мы делали до сих пор, эти два способа выражения шаблона итерации по элементам списка эквивалентны. Однако они различны: выражение for возвращает значение, тогда как foreach просто выполняет некоторую функцию для каждого элемента списка. Этот последний вид использования называется побочным эффектом : печатая каждый элемент, мы не создаем новые значения, мы просто выполняем действие над каждым элементом. С помощью выражений мы можем выдавать значения, которые создают преобразованные списки. Например, сопоставьте использование println со следующим.

1
2
scala> val lengths = for (animal <- animals) yield animal.length
lengths: List[Int] = List(4, 9, 3, 5)

Результатом является новый список, который содержит длины (количество символов) каждого из элементов списка животных . (Конечно, теперь вы можете распечатать его содержимое, используя lengths.foreach (println) , но обычно мы хотим делать другие, обычно более интересные вещи, с такими значениями.)

То, что мы только что сделали, это отобразили значения животных в новый набор значений один-к-одному, используя функцию length . В списках есть еще одна функция map, которая делает это напрямую.

1
2
scala> val lengthsMapped = animals.map(animal => animal.length)
lengthsMapped: List[Int] = List(4, 9, 3, 5)

Таким образом, выражение for-yield и метод map достигают одинакового результата, и во многих случаях они в значительной степени эквивалентны. Использование карты , однако, часто более удобно, потому что вы можете легко объединить последовательность операций. Например, предположим, что вы хотите добавить 1 к списку чисел, а затем получить квадрат этого значения, превращая List (1,2,3) в List (2,3,4) в List (4,9,16). ). Вы можете сделать это довольно легко, используя карту.

1
nums.map(x=>x+1).map(x=>x*x)

Некоторые читатели будут озадачены тем, что только что сделали. Здесь это более явно, используя промежуточную переменную nums2 для хранения списка add-one.

1
2
3
4
5
scala> val nums2 = nums.map(x=>x+1)
nums2: List[Int] = List(2, 3, 4)
  
scala> nums2.map(x=>x*x)
res9: List[Int] = List(4, 9, 16)

Так как nums.map (x => x + 1) возвращает List, нам не нужно присваивать ему имя переменной, чтобы использовать его — мы можем просто немедленно использовать его, включая выполнение другой функции map для него. (Конечно, можно выполнить это вычисление за один раз, например, map ((x + 1) * (x + 1)), но часто используется ряд встроенных функций или функций, которые уже определены заранее) ,

Вы можете продолжать отображать содержимое вашего сердца, в том числе отображение Ints в Strings.

1
2
scala> nums.map(x=>x+1).map(x=>x*x).map(x=>x-1).map(x=>x*(-1)).map(x=>"The answer is: " + x)
res12: List[java.lang.String] = List(The answer is: -3, The answer is: -8, The answer is: -15)

Примечание: использование х во всех этих случаях не важно. Они могли быть названы x, y, z и turlingdromes42 — любое допустимое имя переменной.

Итерация по нескольким спискам

Иногда у вас есть два спаренных списка, и вам нужно одновременно что-то делать с элементами из каждого списка. Например, допустим, у вас есть список токенов слов и другой список с их частями речи. (См. Предыдущий учебник для обсуждения частей речи.)

1
2
3
4
5
scala> val tokens = List("the", "program", "halted")
tokens: List[java.lang.String] = List(the, program, halted)
  
scala> val tags = List("DT","NN","VB")
tags: List[java.lang.String] = List(DT, NN, VB)

Теперь предположим, что мы хотим вывести их в виде следующей строки:

/ DT программа / NN остановлено / VB

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

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

01
02
03
04
05
06
07
08
09
10
scala> val tokenTagPairs = tokens.zip(tags)
tokenTagPairs: List[(java.lang.String, java.lang.String)] = List((the,DT), (program,NN), (halted,VB))
  
Zipping two lists together in this way is a common pattern used for iterating over two lists.
  
Now we have a list of token-tag pairs we can use a for expression to turn it into a List of strings.
  
1
scala> val tokenTagSlashStrings = for ((token, tag) <- tokenTagPairs) yield token + "/" + tag
tokenTagSlashStrings: List[java.lang.String] = List(the/DT, program/NN, halted/VB)

Теперь нам нужно просто превратить этот список строк в одну строку, объединив все ее элементы с пробелом между ними. Функция mkString делает это легко.

1
2
scala> tokenTagSlashStrings.mkString(" ")
res19: String = the/DT program/NN halted/VB

Наконец, здесь все это в один шаг.

1
2
scala> (for ((token, tag) <- tokens.zip(tags)) yield token + "/" + tag).mkString(" ")
res23: String = the/DT program/NN halted/VB

Копирование строки в полезную структуру данных

В компьютерной лингвистике часто требуется преобразовывать входные данные в полезные структуры данных. Рассмотрим предложение с тегом «часть речи», упомянутое в предыдущем уроке. Давайте начнем с присвоения его переменной sentRaw.

1
val sentRaw = "The/DT index/NN of/IN the/DT 100/CD largest/JJS Nasdaq/NNP financial/JJ stocks/NNS rose/VBD modestly/RB as/IN well/RB ./."

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

1
2
scala> val tokenTagPairs = sentRaw.split(" ").toList.map(x => x.split("/")).map(x => Tuple2(x(0), x(1)))
tokenTagPairs: List[(java.lang.String, java.lang.String)] = List((The,DT), (index,NN), (of,IN), (the,DT), (100,CD), (largest,JJS), (Nasdaq,NNP), (financial,JJ), (stocks,NNS), (rose,VBD), (modestly,RB), (as,IN), (well,RB), (.,.))

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

1
2
scala> sentRaw.split(" ")
res0: Array[java.lang.String] = Array(The/DT, index/NN, of/IN, the/DT, 100/CD, largest/JJS, Nasdaq/NNP, financial/JJ, stocks/NNS, rose/VBD, modestly/RB, as/IN, well/RB, ./.)

Что такое массив? Это своего рода последовательность, подобная List, но у нее есть некоторые другие свойства, которые мы обсудим позже. А пока давайте придерживаться списков, что можно сделать с помощью метода toList . Кроме того, давайте присвоим его переменной, чтобы на оставшихся операциях было легче сосредоточиться.

1
2
scala> val tokenTagSlashStrings = sentRaw.split(" ").toList
tokenTagSlashStrings: List[java.lang.String] = List(The/DT, index/NN, of/IN, the/DT, 100/CD, largest/JJS, Nasdaq/NNP, financial/JJ, stocks/NNS, rose/VBD, modestly/RB, as/IN, well/RB, ./.)

Теперь нам нужно превратить каждый элемент в этом списке в пары токена и тега. Давайте сначала рассмотрим один элемент, превращая что-то вроде « The / DT » в пару ( «The», «DT») . Следующие строки показывают, как сделать это один шаг за раз, используя промежуточные переменные.

1
2
3
4
5
6
7
8
scala> val first = "The/DT"
first: java.lang.String = The/DT
  
scala> val firstSplit = first.split("/")
firstSplit: Array[java.lang.String] = Array(The, DT)
  
scala> val firstPair = Tuple2(firstSplit(0), firstSplit(1))
firstPair: (java.lang.String, java.lang.String) = (The,DT)

Итак, firstPair — это кортеж, представляющий информацию, закодированную в строке сначала. Это включало две операции: разбиение, а затем создание кортежа из массива, который возник в результате разбиения. Мы можем сделать это для всех элементов в tokenTagSlashStrings, используя map. Давайте сначала преобразовать строки в массивы.

1
2
scala> val tokenTagArrays = tokenTagSlashStrings.map(x => x.split("/"))
res0: List[Array[java.lang.String]] = List(Array(The, DT), Array(index, NN), Array(of, IN), Array(the, DT), Array(100, CD), Array(largest, JJS), Array(Nasdaq, NNP), Array(financial, JJ), Array(stocks, NNS), Array(rose, VBD), Array(modestly, RB), Array(as, IN), Array(well, RB), Array(., .))

И, наконец, мы превращаем массивы в Tuple2s и получаем результат, полученный ранее с помощью одной строки.

1
2
scala> val tokenTagPairs = tokenTagArrays.map(x => Tuple2(x(0), x(1)))
tokenTagPairs: List[(java.lang.String, java.lang.String)] = List((The,DT), (index,NN), (of,IN), (the,DT), (100,CD), (largest,JJS), (Nasdaq,NNP), (financial,JJ), (stocks,NNS), (rose,VBD), (modestly,RB), (as,IN), (well,RB), (.,.))

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

Одна из очень полезных вещей, связанных со списком пар (Tuple2s), заключается в том, что функция unzip возвращает нам два списка, один со всеми первыми элементами, а другой со всеми вторыми элементами.

1
2
3
scala> val (tokens, tags) = tokenTagPairs.unzip
tokens: List[java.lang.String] = List(The, index, of, the, 100, largest, Nasdaq, financial, stocks, rose, modestly, as, well, .)
tags: List[java.lang.String] = List(DT, NN, IN, DT, CD, JJS, NNP, JJ, NNS, VBD, RB, IN, RB, .)

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

Предоставление функции, которую вы определили для сопоставления

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

01
02
03
04
05
06
07
08
09
10
11
def coursePos (tag: String) = tag match {
  case "NN" | "NNS" | "NNP" | "NNPS"                       => "Noun"
  case "JJ" | "JJR" | "JJS"                                => "Adjective"
  case "VB" | "VBD" | "VBG" | "VBN" | "VBP" | "VBZ" | "MD" => "Verb"
  case "RB" | "RBR" | "RBS" | "WRB" | "EX"                 => "Adverb"
  case "PRP" | "PRP$" | "WP" | "WP$"                       => "Pronoun"
  case "DT" | "PDT" | "WDT"                                => "Article"
  case "CC"                                                => "Conjunction"
  case "IN" | "TO"                                         => "Preposition"
  case _                                                   => "Other"
}

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

1
2
scala> tags.map(coursePos)
res1: List[java.lang.String] = List(Article, Noun, Preposition, Article, Other, Adjective, Noun, Adjective, Noun, Verb, Adverb, Preposition, Adverb, Other)

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

1
2
3
4
5
val sentRaw = "The/DT index/NN of/IN the/DT 100/CD largest/JJS Nasdaq/NNP financial/JJ stocks/NNS rose/VBD modestly/RB as/IN well/RB ./."
  
val (tokens, tags) = sentRaw.split(" ").toList.map(x => x.split("/")).map(x => Tuple2(x(0), x(1))).unzip
  
tokens.zip(tags.map(coursePos)).map(x => x._1+"/"+x._2).mkString(" ")

Еще один момент заключается в том, что когда вы предоставляете выражения, такие как (x => x + 1) для отображения , вы фактически определяете анонимную функцию! Здесь та же операция с картой с различными уровнями спецификации

01
02
03
04
05
06
07
08
09
10
11
12
13
14
scala> val numbers = (1 to 5).toList
numbers: List[Int] = List(1, 2, 3, 4, 5)
  
scala> numbers.map(1+)
res11: List[Int] = List(2, 3, 4, 5, 6)
  
scala> numbers.map(_+1)
res12: List[Int] = List(2, 3, 4, 5, 6)
  
scala> numbers.map(x=>x+1)
res13: List[Int] = List(2, 3, 4, 5, 6)
  
scala> numbers.map((x: Int) => x+1)
res14: List[Int] = List(2, 3, 4, 5, 6)

Итак, все последовательно: передаете ли вы именованную функцию или анонимную функцию, map будет применять ее к каждому элементу в списке.

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

1
2
3
4
5
scala> def addOne = (x: Int) => x + 1
addOne: (Int) => Int
  
scala> addOne(1)
res15: Int = 2

Это похоже на определение функций, как у нас ранее (например, def addOne (x: Int) = x + 1 ), но это более удобно в определенных контекстах, к которым мы вернемся позже. На данный момент нужно понимать, что всякий раз, когда вы наносите на карту, вы используете функцию, которая уже существовала, или создаете ее на лету.

Фильтрация и подсчет

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

1
2
scala> val courseTags = tags.map(coursePos)
courseTags: List[java.lang.String] = List(Article, Noun, Preposition, Article, Other, Adjective, Noun, Adjective, Noun, Verb, Adverb, Preposition, Adverb, Other)

Один из способов сделать это — отфильтровать все существительные и прилагательные, чтобы получить список без них, а затем получить его длину.

1
2
3
4
5
6
7
scala> val noNouns = courseTags.filter(x => x != "Noun")noNouns: List[java.lang.String] = List(Article, Preposition, Article, Other, Adjective, Adjective, Verb, Adverb, Preposition, Adverb, Other)
  
scala> val noNounsOrAdjectives = noNouns.filter(x => x != "Adjective")
noNounsOrAdjectives: List[java.lang.String] = List(Article, Preposition, Article, Other, Verb, Adverb, Preposition, Adverb, Other)
  
scala> noNounsOrAdjectives.length
res8: Int = 9

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

1
2
scala> courseTags.filter(x => x != "Noun" && x != "Adjective").length
res9: Int = 9

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

1
2
scala> courseTags.count(x => x != "Noun" && x != "Adjective")
res10: Int = 9

В качестве упражнения попробуйте выполнить однострочную строку, которая начинается с sentRaw и предоставляет значение « resX: Int = 9 » (где X — это то, что вы получаете в своем Scala REPL).

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

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

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