Статьи

Учебник по Scala — Карты, Наборы, groupBy, Параметры, Flatten, FlatMap

Предисловие

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

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

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

  • Ассоциирование английских слов с их немецкими переводами
  • Ассоциирование каждого слова с его количеством в данном тексте
  • Ассоциирование каждого слова с его возможными частями речи

В этом посте вы увидите конкретные примеры каждого из них.

Создание карт и доступ к их элементам

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

1
2
scala> val engToDeu = Map(("dog","Hund"), ("cat","Katze"), ("rhinoceros","Nashorn"))
engToDeu: scala.collection.immutable.Map[java.lang.String,java.lang.String] = Map(dog -> Hund, cat -> Katze, rhinoceros -> Nashorn)

Обратите внимание, что записи карты имеют ключ формы -> значение . Затем мы можем получить немецкий перевод слова « собака », указав ключ « собака » на созданной нами карте.

1
2
scala> engToDeu("dog")
res0: java.lang.String = Hund

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

1
2
3
4
5
scala> val engWords = List("dog","cat","rhinoceros")
engWords: List[java.lang.String] = List(dog, cat, rhinoceros)
 
scala> val deuWords = List("Hund","Katze","Nashorn")
deuWords: List[java.lang.String] = List(Hund, Katze, Nashorn)

Затем, чтобы найти перевод cat , вам нужно найти индекс cat в engWords , а затем найти этот индекс в deuWords .

1
2
3
4
5
scala> engWords.indexOf("cat")
res2: Int = 1
 
scala> deuWords(engWords.indexOf("cat"))
res3: java.lang.String = Katze

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

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

1
2
scala> engWords.zip(deuWords)
res4: List[(java.lang.String, java.lang.String)] = List((dog,Hund), (cat,Katze), (rhinoceros,Nashorn))

Вызвав метод toMap для такого списка пар, мы получим карту.

1
2
scala> engWords.zip(deuWords).toMap
res5: scala.collection.immutable.Map[java.lang.String,java.lang.String] = Map(dog -> Hund, cat -> Katze, rhinoceros -> Nashorn)

Обратите внимание, что, хотя REPL показывает порядок пар ключ-значение, совпадающий с исходным списком, из которого мы построили карту, элементам карты не присущ порядок.

Вы можете добавить элементы на карту, чтобы создать новую карту, используя оператор + и стрелку -> между каждой парой ключ-значение.

1
2
3
4
5
scala> engToDeu + "owl" -> "Eule"
res6: (java.lang.String, java.lang.String) = (Map(dog -> Hund, cat -> Katze, rhinoceros -> Nashorn)owl,Eule)
 
scala> engToDeu + ("owl" -> "Eule", "hippopotamus" -> "Nilpferd")
res7: scala.collection.immutable.Map[java.lang.String,java.lang.String] = Map(rhinoceros -> Nashorn, dog -> Hund, owl -> Eule, hippopotamus -> Nilpferd, cat -> Katze)

Вы можете добавить одну карту к другой, используя оператор ++ .

1
2
3
4
5
scala> val newEntries = Map(("hippopotamus", "Nilpferd"),("owl","Eule"))
newEntries: scala.collection.immutable.Map[java.lang.String,java.lang.String] = Map(hippopotamus -> Nilpferd, owl -> Eule)
 
scala> val expandedEngToDeu = engToDeu ++ newEntries
expandedEngToDeu: scala.collection.immutable.Map[java.lang.String,java.lang.String] = Map(rhinoceros -> Nashorn, dog -> Hund, owl -> Eule, hippopotamus -> Nilpferd, cat -> Katze)

Вы можете сделать то же самое, передав список кортежей оператору ++.

1
2
scala> engToDeu ++ List(("hippopotamus", "Nilpferd"),("owl","Eule"))
res8: scala.collection.immutable.Map[java.lang.String,java.lang.String] = Map(rhinoceros -> Nashorn, dog -> Hund, owl -> Eule, hippopotamus -> Nilpferd, cat -> Katze)

И вы можете удалить ключ с карты с помощью оператора -.

1
2
scala> engToDeu - "dog"
res9: scala.collection.immutable.Map[java.lang.String,java.lang.String] = Map(cat -> Katze, rhinoceros -> Nashorn)

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

Если мы запрашиваем значение, связанное с ключом, которого нет на карте, мы получаем ошибку.

1
2
3
4
scala> engToDeu("bird")
java.util.NoSuchElementException: key not found: bird
at scala.collection.MapLike$class.default(MapLike.scala:224)
(etc.)

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

1
2
3
4
5
scala> engToDeu.contains("bird")
res10: Boolean = false
 
scala> engToDeu.contains("dog")
res11: Boolean = true

Допустим, у вас есть список английских слов и вы хотите найти соответствующие им немецкие слова, и вы хотите защитить себя от исключения NoSuchElementException . Один из способов сделать это — отфильтровать слова с помощью содержимого , а затем отобразить оставшиеся слова через engToDeu .

1
2
3
4
5
scala> val wordsToTranslate = List("dog","bird","cat","armadillo")
wordsToTranslate: List[java.lang.String] = List(dog, bird, cat, armadillo)
 
scala> wordsToTranslate.filter(x=>engToDeu.contains(x)).map(x=>engToDeu(x))
res12: List[java.lang.String] = List(Hund, Katze)

Это полезные способы безопасного применения карты к списку предметов. Однако позже мы увидим лучший способ справиться с пропущенными значениями, используя Параметры.

Если у вас есть разумное значение по умолчанию для любого ключа, который вы можете попробовать использовать с вашей картой, вы можете использовать метод getOrElse . Вы задаете ключ в качестве первого аргумента, а затем значение по умолчанию в качестве второго.

1
2
3
4
5
scala> engToDeu.getOrElse("dog","???")
res1: java.lang.String = Hund
 
scala> engToDeu.getOrElse("armadillo","???")
res2: java.lang.String = ???

Весьма распространено использовать getOrElse со значением по умолчанию 0 для Карт, которые содержат статистику, такую ​​как количество слов (см. Ниже), где отсутствие ключа естественно указывает на то, что он имеет, например, счетчик нуля.

Если у вас есть согласованное значение по умолчанию для любых ключей, которых нет на карте, вы можете установить его с помощью метода withDefault.

1
2
3
4
5
scala> val engToDeu = Map(("dog","Hund"), ("cat","Katze"), ("rhinoceros","Nashorn")).withDefault(x => "???")
engToDeu: scala.collection.immutable.Map[java.lang.String,java.lang.String] = Map(dog -> Hund, cat -> Katze, rhinoceros -> Nashorn)
 
scala> engToDeu("armadillo")
res3: java.lang.String = ???

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

Ключи и значения в Картах

Возможно, вы заметили, что Scala говорит вам больше, чем то, что вы только что создали карту. Как и List, Map является параметризованным типом, что означает, что это общий способ сбора нескольких объектов определенных типов вместе. Выше мы видели экземпляр Map [String, String] (оставив часть java.lang, чтобы сделать ее более понятной). Первая строка указывает, что ключи являются строками, а вторая, что значения являются строками. В принципе, любой тип может использоваться в любой позиции ( предупреждение : вам следует избегать использования изменяемых структур данных в качестве ключей, если вы не знаете, что делаете). Вот несколько примеров (попробуйте игнорировать части scala.collection.immutable и java.lang и просто сфокусируйтесь на сигнатурах Map [X, Y], которые мы получаем).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
scala> Map((10,"ten"), (100,"one hundred"))
res0: scala.collection.immutable.Map[Int,java.lang.String] = Map(10 -> ten, 100 -> one hundred)
 
scala> Map(("a",1),("b",2))
res1: scala.collection.immutable.Map[java.lang.String,Int] = Map(a -> 1, b -> 2)
 
scala> Map((1,3.14), (2,6.28))
res2: scala.collection.immutable.Map[Int,Double] = Map(1 -> 3.14, 2 -> 6.28)
 
scala> Map((("pi",1),3.14), (("tau",2),6.28))
res3: scala.collection.immutable.Map[(java.lang.String, Int),Double] = Map((pi,1) -> 3.14, (tau,2) -> 6.28)
 
scala> Map(("the", List("Determiner")), ("book", List("Verb", "Noun")), ("off", List("Preposition", "Verb")))
res4: scala.collection.immutable.Map[java.lang.String, List[java.lang.String]] = Map(the -> List(Determiner), book -> List(Verb, Noun), off -> List(Preposition, Verb))

В последних двух примерах показаны некоторые очень полезные аспекты типов ключей и значений, которые позволяют использовать более сложные ключи и значения. Первый использует пару (String, Int) в качестве ключа с подписью Map [(String, Int), Double] , а второй использует List [String] в качестве значения с подписью Map [String, List [String] ] . Таким образом, вы можете связать вместе несколько типов с помощью кортежей и использовать параметризованные структуры данных для параметризации другой структуры данных.

Простая задача перевода

Вот мини немецкий / английский словарь в качестве карты.

1
2
scala> val miniDictionary = Map(("befreit", "liberated"), ("baeche", "brooks"), ("eise", "ice"), ("sind", "are"), ("strom", "river"), ("und", "and"), ("vom", "from"))
miniDictionary: scala.collection.immutable.Map[java.lang.String,java.lang.String] = Map(und -> and, eise -> ice, sind -> are, befreit -> liberated, strom -> river, vom -> from, baeche -> brooks)

Мы можем обеспечить (очень плохой) перевод немецкого предложения « vom eise befreit sind strom und baeche », используя этот словарь: мы просто разбиваем немецкое предложение и затем отображаем его элементы, просматривая каждое слово в словаре.

1
2
3
4
5
scala> val example = "vom eise befreit sind strom und baeche"
example: java.lang.String = vom eise befreit sind strom und baeche
 
scala> example.split(" ").map(deuWord => miniDictionary(deuWord)).mkString(" ")
res0: String = from ice liberated are river and brooks

Ладно, не совсем «ото льда, который они освободили, от ручья и ручья», но опять же, это в значительной степени глупейший подход машинного перевода…

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

1
2
3
4
5
scala> val example2 = "vom eise befreit sind strom und schiffe"
example2: java.lang.String = vom eise befreit sind strom und schiffe
 
scala> example2.split(" ").map(deuWord => miniDictionary(deuWord)).mkString(" ")
java.util.NoSuchElementException: key not found: schiffe

Мы вернемся к этому ниже.

Создание карт из списков с помощью groupBy

Мы часто храним данные в определенной структуре данных и хотели бы работать с ней, используя другую структуру данных, которая организует точки данных другим способом. Здесь мы рассмотрим, как преобразовать List в Map с помощью метода groupBy , чтобы выполнить некоторую полезную обработку для работы с частями речи. По пути мы также увидим структуру данных Set .

Мы начнем с очень простого примера того, что делает groupBy . Имея список числовых токенов, мы можем получить карту из числовых типов ко всем токенам каждого числа.

1
2
3
4
5
scala> val numbers = List(1,4,5,1,6,5,2,8,1,9,2,1)
numbers: List[Int] = List(1, 4, 5, 1, 6, 5, 2, 8, 1, 9, 2, 1)
 
scala> numbers.groupBy(x=>x)
res19: scala.collection.immutable.Map[Int,List[Int]] = Map(5 -> List(5, 5), 1 -> List(1, 1, 1, 1), 6 -> List(6), 9 -> List(9), 2 -> List(2, 2), 8 -> List(8), 4 -> List(4))

Как видно из результата, groupBy взяла анонимную функцию x => x , сгруппировала все элементы List, имеющие одинаковое значение x , а затем создала Map из каждого x в группу, содержащую ее токены. Итак, мы получаем 2 отображения в список, содержащий 2? S, и так далее. Это, вероятно, кажется немного странным, но это невероятно полезно, когда мы рассматриваем списки, в которых есть более интересные элементы. Для этого давайте вернемся к примеру с пометкой части речи из части 4 этих руководств . Скажем, у нас есть предложение, которое помечено частями речи, например, следующий (составленный) пример, который обеспечивает некоторую двусмысленность тегов.

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

Части речи могут быть аннотированы следующим образом (с большим количеством упрощений и извинений за любое нарушение, причиненное чьей-либо языковой чувствительности).

в / Подготовка к / Det темный / Существительное, / Punc a / Det высокий / Прилагательное человек / Псевдоним / Глагол / Det Пила / Существительное, которое / Местоимение он / Местоимение необходимо / Глагол, чтобы / Подготовить человека / Глагол, чтобы / Подготовить вырезать / Глагол / Det dark / Прилагательное дерево / Существительное ./Punc

В части 4 приведено подробное объяснение того, как следующее выражение превращает строку, подобную этой, в список кортежей.

1
2
scala> val tagged = "in/Prep the/Det dark/Noun ,/Punc a/Det tall/Adjective man/Noun saw/Verb the/Det saw/Noun that/Pronoun he/Pronoun needed/Verb to/Prep man/Verb to/Prep cut/Verb the/Det dark/Adjective tree/Noun ./Punc".split(" ").toList.map(x => x.split("/")).map(x => (x(0), x(1)))
tagged: List[(java.lang.String, java.lang.String)] = List((in,Prep), (the,Det), (dark,Noun), (,,Punc), (a,Det), (tall,Adjective), (man,Noun), (saw,Verb), (the,Det), (saw,Noun), (that,Pronoun), (he,Pronoun), (needed,Verb), (to,Prep), (man,Verb), (to,Prep), (cut,Verb), (the,Det), (dark,Adjective), (tree,Noun), (.,Punc))

Теперь давайте по- разному будем использовать groupBy . Первое, что может нас заинтересовать, — это увидеть, с какими частями речи связано каждое слово.

1
2
scala> val groupedTagged = tagged.groupBy(x => x._1)
groupedTagged: scala.collection.immutable.Map[java.lang.String,List[(java.lang.String, java.lang.String)]] = Map(in -> List((in,Prep)), needed -> List((needed,Verb)), . -> List((.,Punc)), cut -> List((cut,Verb)), saw -> List((saw,Verb), (saw,Noun)), a -> List((a,Det)), man -> List((man,Noun), (man,Verb)), that -> List((that,Pronoun)), dark -> List((dark,Noun), (dark,Adjective)), to -> List((to,Prep), (to,Prep)), , -> List((,,Punc)), tall -> List((tall,Adjective)), he -> List((he,Pronoun)), tree -> List((tree,Noun)), the -> List((the,Det), (the,Det), (the,Det)))

Итак, теперь вы видите, что ключи на карте, созданной groupBy, являются словами, а значения — группами исходных элементов. Затем вы можете увидеть, что анонимная функция x => x._1, предоставленная groupBy, выполняет две функции: она определяет часть элементов ввода, которая будет группировать различные элементы, и указывает, что эта часть ввода определяет пространство клавиш.

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

1
2
scala> groupedTagged("saw")
res21: List[(java.lang.String, java.lang.String)] = List((saw,Verb), (saw,Noun))

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

1
2
3
4
5
scala> groupedTagged("saw").map(x=>x._2)
res24: List[java.lang.String] = List(Verb, Noun)
 
scala> groupedTagged("saw").map(x=>x._2).toSet
res25: scala.collection.immutable.Set[java.lang.String] = Set(Verb, Noun)

Преобразование Списка в Набор здесь мало что дало, но рассмотрим, у которого есть несколько токенов с одной и той же частью речи.

1
2
3
4
5
6
7
8
scala> groupedTagged("the")
res26: List[(java.lang.String, java.lang.String)] = List((the,Det), (the,Det), (the,Det))
 
scala> groupedTagged("the").map(x=>x._2)
res27: List[java.lang.String] = List(Det, Det, Det)
 
scala> groupedTagged("the").map(x=>x._2).toSet
res28: scala.collection.immutable.Set[java.lang.String] = Set(Det)

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

Теперь вернемся к переходу от пар слово / тег к отображению слов к возможным тегам для каждого слова. Ключи, которые мы получили из tagged.groupBy (x => x._1), — это то, что нам нужно, но мы хотим преобразовать значения из списков токенов слов / тегов в наборы тегов, что мы можем сделать с помощью метода mapValues на Картах ,

1
2
scala> val wordsToTags = tagged.groupBy(x => x._1).mapValues(listOfWordTagPairs => listOfWordTagPairs.map(wordTagPair => wordTagPair._2).toSet)
wordsToTags: scala.collection.immutable.Map[java.lang.String,scala.collection.immutable.Set[java.lang.String]] = Map(in -> Set(Prep), needed -> Set(Verb), . -> Set(Punc), cut -> Set(Verb), saw -> Set(Verb, Noun), a -> Set(Det), man -> Set(Noun, Verb), that -> Set(Pronoun), dark -> Set(Noun, Adjective), to -> Set(Prep), , -> Set(Punc), tall -> Set(Adjective), he -> Set(Pronoun), tree -> Set(Noun), the -> Set(Det))

Кусочек внутри части mapValues ​​(…) заставит некоторых читателей зажмуриться, но вам просто нужно взглянуть на строку, где мы получили Res28 выше: если вы поняли это, то вам просто нужно понять, что мы делаем точно так же вещь, но теперь в контексте сопоставления значений, а не с одним значением. Теперь вы знаете, как отображать значения, которые вы отображаете.

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

01
02
03
04
05
06
07
08
09
10
11
scala> wordsToTags("man")("Noun")
res8: Boolean = true
 
scala> wordsToTags("man")("Det")
res9: Boolean = false
 
scala> wordsToTags("man")("Verb")
res10: Boolean = true
 
scala> wordsToTags("saw")("Verb")
res11: Boolean = true

Это пример того, как структуры данных в структурах данных (здесь «Наборы внутри карты») весьма полезны. ( Упражнение : подумайте, что такое дерево на мгновение и как вы можете реализовать его с помощью списков.)

Есть много вещей, которые вы можете сделать в компьютерной лингвистике с помощью Карт, от слов до их частей речи. Простым примером является вычисление среднего количества тегов на тип слова.

1
2
scala> val avgTagsPerType = wordsToTags.values.map(x=>x.size).sum/wordsToTags.size.toDouble
avgTagsPerType: Double = 1.2

Если вам не ясно, что здесь происходит, дразните его отдельно в своем собственном REPL!

Мы можем перевернуть наши пары слово / тег по-другому, чтобы узнать, какие слова идут с каждой частью речи. Единственное, что нам нужно сделать, это groupBy на втором элементе каждой пары, а затем сопоставить значения List с их первым элементом и получить Set из них.

1
2
scala> val tagsToWords = tagged.groupBy(x => x._2).mapValues(listOfWordTagPairs => listOfWordTagPairs.map(wordTagPair => wordTagPair._1).toSet)
tagsToWords: scala.collection.immutable.Map[java.lang.String,scala.collection.immutable.Set[java.lang.String]] = Map(Prep -> Set(in, to), Det -> Set(the, a), Noun -> Set(dark, man, saw, tree), Pronoun -> Set(that, he), Verb -> Set(saw, needed, man, cut), Punc -> Set(,, .), Adjective -> Set(tall, dark))

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

Считать слова

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

Пример текста, который мы будем использовать, является языком скороговоркой о сурках.

1
2
scala> val woodchuck = "how much wood could a woodchuck chuck if a woodchuck could chuck wood ? as much wood as a woodchuck would , if a woodchuck could chuck wood ."
woodchuck: java.lang.String = how much wood could a woodchuck chuck if a woodchuck could chuck wood ? as much wood as a woodchuck would , if a woodchuck could chuck wood .

Учитывая это, вот как мы можем вычислить количество вхождений каждого типа слова. Сначала мы группируем по элементам. Хотя список строк не так интересен, как список кортежей, как у нас со словами и тегами, он все же дает полезный результат: теперь у нас есть уникальный набор ключей, соответствующий типам элементов, найденных в массиве, и каждому значению соответствует Массив токенов этого типа.

1
2
scala> woodchuck.split(" ").groupBy(x=>x)
res29: scala.collection.immutable.Map[java.lang.String,Array[java.lang.String]] = Map(woodchuck -> Array(woodchuck, woodchuck, woodchuck, woodchuck), chuck -> Array(chuck, chuck, chuck), . -> Array(.), would -> Array(would), if -> Array(if, if), a -> Array(a, a, a, a), as -> Array(as, as), , -> Array(,), how -> Array(how), much -> Array(much, much), wood -> Array(wood, wood, wood, wood), ? -> Array(?), could -> Array(could, could, could))

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

1
2
scala> val counts = woodchuck.split(" ").groupBy(x=>x).mapValues(x=>x.length)
counts: scala.collection.immutable.Map[java.lang.String,Int] = Map(woodchuck -> 4, chuck -> 3, . -> 1, would -> 1, if -> 2, a -> 4, as -> 2, , -> 1, how -> 1, much -> 2, wood -> 4, ? -> 1, could -> 3)

С помощью счетчиков мы теперь можем получить доступ к частотам любого из слов, которые были в тексте.

1
2
3
4
5
scala> counts("woodchuck")
res5: Int = 4
 
scala> counts("could")
res6: Int = 3

Легко! Конечно, мы обычно хотим рассчитывать количество слов для текстов, которые длиннее и хранятся в файле, а не добавляются явно в код Scala. Следующий урок покажет, как это сделать.

Перебор ключей и значений на карте

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

Вы можете получить доступ только к ключам или только к значениям.

1
2
3
4
5
scala> counts.keys
res0: Iterable[java.lang.String] = Set(woodchuck, chuck, ., would, if, a, as, ,, how, much, wood, ?, could)
 
scala> counts.values
res1: Iterable[Int] = MapLike(4, 3, 1, 1, 2, 4, 2, 1, 1, 2, 4, 1, 3)

Обратите внимание, что это обе структуры данных Iterable, поэтому мы можем выполнять все обычные отображения, фильтрацию и т. Д., Которые мы уже сделали со списками. ( Конечно , вы можете преобразовать их в списки, если хотите использовать toList .)

Вы можете распечатать все пары ключ -> значение на карте несколькими способами. Одним из них является использование для выражения.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
scala> for ((k,v) <- counts) println(k + " -> " + v)
woodchuck -> 4
chuck -> 3
. -> 1
would -> 1
if -> 2
a -> 4
as -> 2
, -> 1
how -> 1
much -> 2
wood -> 4
? -> 1
could -> 3

И вот другие способы достижения того же результата (вывод опущен, поскольку он одинаков).

1
2
3
4
5
for (k <- counts.keys) println(k + " -> " + counts(k))
counts.map(kvPair => kvPair._1 + " -> " + kvPair._2).foreach(println)
counts.keys.map(k => k + " -> " + counts(k)).foreach(println)
counts.foreach { case(k,v) => println(k + " -> " + v) }
counts.foreach(kvPair => println(kvPair._1 + " -> " + kvPair._2))

И так далее. По сути, вы можете пошагово проходить карту по одной паре ключ-значение за раз, или вы можете получить набор ключей, а затем пройти по ним и получить доступ к значениям из карты. Форма, которую вы используете, зависит от того, что вам нужно — например, конструкция foreach не возвращает значение, но выражения for и выражения map возвращают значения. Почему ты бы так поступил? Ну, в качестве примера рассмотрим группирование всех слов, которые встречались одинаковое количество раз.

1
2
scala> val countsToWords = counts.keys.toList.map(k => (counts(k),k)).groupBy(x=>x._1).mapValues(x=>x.map(y=>y._2))
countsToWords: scala.collection.immutable.Map[Int,List[java.lang.String]] = Map(3 -> List(chuck, could), 4 -> List(woodchuck, a, wood), 1 -> List(., would, ,, how, ?), 2 -> List(if, as, much))

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

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

1
2
3
4
5
scala> countsToWords.keys.toList.sorted.reverse.foreach(x => println(x + ": " + countsToWords(x).sorted.mkString(",")))
4: a,wood,woodchuck
3: chuck,could
2: as,if,much
1: ,,.,?,how,would

Опции и flatMapping для работы с недостающими ключами

В начале этого урока я указал, что у нас возникнут проблемы, если мы попросим ключ, которого нет на карте. Давайте вернемся к карте engToDeu, с которой мы начали.

1
2
3
4
5
6
7
8
scala> val engToDeu = Map(("dog","Hund"), ("cat","Katze"), ("rhinoceros","Nashorn"))
engToDeu: scala.collection.immutable.Map[java.lang.String,java.lang.String] = Map(dog -> Hund, cat -> Katze, rhinoceros -> Nashorn)
 
scala> engToDeu("dog")
res0: java.lang.String = Hund
 
scala> engToDeu("bird")
java.util.NoSuchElementException: key not found: bird

Существует другой способ доступа к элементам карты, используя метод get .

1
2
3
4
5
scala> engToDeu.get("dog")
res2: Option[java.lang.String] = Some(Hund)
 
scala> engToDeu.get("bird")
res3: Option[java.lang.String] = None

Теперь возвращаемое значение является Option [String] . Опция — это Some, которая содержит значение, или None , что означает, что значения нет. Если вы хотите получить значение из Some, используйте метод get в Options.

1
2
3
4
5
scala> val dogTrans = engToDeu.get("dog")
dogTrans: Option[java.lang.String] = Some(Hund)
 
scala> dogTrans.get
res4: java.lang.String = Hund

Если вы просто используете get on Map для получения Option, а затем немедленно вызываете get on Option, мы получаем то же поведение, что и раньше.

1
2
3
4
5
scala> engToDeu.get("dog").get
res6: java.lang.String = Hund
 
scala> engToDeu.get("bird").get
java.util.NoSuchElementException: None.get

Итак, на данный момент, вы, вероятно, думаете, что это звучит как пустая трата времени, которая просто усложняет ситуацию. Подождите! Это на самом деле чрезвычайно полезно из-за сопоставления с образцом и работы многих методов на последовательностях.

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

1
2
3
4
5
6
scala> wordsToTranslate.foreach { x => engToDeu.get(x) match {
|   case Some(y) => println(x + " -> " + y)
|   case None =>
| }}
dog -> Hund
cat -> Katze

Я знаю … это, вероятно, все еще не убедительно — это все еще выглядит более сложным , чем условное выражение, которое мы использовали (далеко) выше, чтобы проверить, содержал ли engToDeu данный ключ (по крайней мере, для этого конкретного примера). Держись … потому что теперь мы почти готовы к тому, чтобы все стало проще, и узнаем некоторые полезные вещи о списках при этом.

Во-первых, вы должны знать о замечательном методе списков, который называется flatten . Если у вас есть список списков строк, вы можете использовать flatten для получения одного списка строк. Рассмотрим следующий пример, в котором мы сглаживаем список списков строк и создаем одну строку из результата с помощью mkString . Обратите внимание, что пустой список в третьем месте основного списка просто исчезает, когда мы выравниваем его.

1
2
3
4
5
6
7
8
scala> val sentences = List(List("Here","is","sentence","one","."), List("The","third","sentence","is","empty","!"), List(),List("Lastly",",","we","have","a","final","sentence","."))
sentences: List[List[java.lang.String]] = List(List(Here, is, sentence, one, .), List(The, third, sentence, is, empty, !), List(), List(Lastly, ,, we, have, a, final, sentence, .))
 
scala> sentences.flatten
res0: List[java.lang.String] = List(Here, is, sentence, one, ., The, third, sentence, is, empty, !, Lastly, ,, we, have, a, final, sentence, .)
 
scala> sentences.flatten.mkString(" ")
res1: String = Here is sentence one . The third sentence is empty ! Lastly , we have a final sentence .

Сглаживание в целом довольно полезно само по себе. Что касается игры со значениями Option, то можно подумать, что Options — это списки. Некоторые — как один элемент. Списки, а None — как пустые списки. Таким образом, когда у вас есть список опций, метод flatten дает вам значение в Some, и любые None просто исчезают.

1
2
3
4
5
scala> wordsToTranslate.map(x => engToDeu.get(x))
res12: List[Option[java.lang.String]] = List(Some(Hund), None, Some(Katze), None)
 
scala> wordsToTranslate.map(x => engToDeu.get(x)).flatten
res13: List[java.lang.String] = List(Hund, Katze)

Это настолько полезная парадигма, что есть функция flatMap, которая делает именно это.

1
2
scala> wordsToTranslate.flatMap(x => engToDeu.get(x))
res14: List[java.lang.String] = List(Hund, Katze)

Итак, возвращаясь к приведенному выше примеру перевода, теперь мы можем спокойно переходить к « schiffe » без суеты.

1
2
scala> example2.split(" ").flatMap(deuWord => miniDictionary.get(deuWord)).mkString(" ")
res15: String = from ice liberated are river and

Является ли это желаемым поведением в данном конкретном случае — это другой вопрос (например, вы действительно должны делать какую-то особую неизвестную обработку слов). Тем не менее, вы обнаружите, что flatMap в целом довольно удобен для такого рода паттернов, в которых список элементов используется для извлечения значений из Map, в которых будут отсутствовать некоторые из этих значений.

Примером дальнейшего использования Options и flatMap является то, что вы также можете создавать функции, которые возвращают Options и, таким образом, поддаются flatMapping. Рассмотрим функцию, которая возводит в квадрат только нечетные числа и выбрасывает четные числа ( примечание : оператор% — это оператор по модулю, который находит остаток от деления одного числа на другое — попробуйте его в REPL).

1
2
scala> def squareOddNumber (x: Int) = if (x % 2 != 0) Some(x*x) else None
squareOddNumber: (x: Int)Option[Int]

Если вы отобразите числа от 1 до 10, вы увидите Somes и Nones, а если вы их спланируете , вы получите точно желаемый результат квадратов всех нечетных чисел без какого-либо загрязнения от четных чисел.

1
2
3
4
5
scala> (1 to 10).toList.map(x=>squareOddNumber(x))
res16: List[Option[Int]] = List(Some(1), None, Some(9), None, Some(25), None, Some(49), None, Some(81), None)
 
scala> (1 to 10).toList.flatMap(x=>squareOddNumber(x))
res17: List[Int] = List(1, 9, 25, 49, 81)

Это оказывается удивительно полезным и распространенным, настолько, что выражение « просто flatMap, что дерьмо » стало распространенным рефреном среди программистов Scala. Программисты Scala даже пишут сценарии, чтобы напомнить им сделать это. 🙂

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

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