Это часть 7 учебных пособий для начинающих программистов, попадающих в Scala. Другие посты находятся в этом блоге, и вы можете получить ссылки на эти и другие ресурсы на странице ссылок курса по компьютерной лингвистике, для которого я их создаю.
Списки (и другие структуры данных последовательности, такие как диапазоны и массивы) позволяют группировать коллекции объектов упорядоченным образом: вы можете обращаться к элементам списка, индексируя их положение в списке, или перебирая элементы списка, один за другим. , используя для выражений и функций последовательности, таких как карта , фильтр , уменьшить и сложить . Другим важным видом структуры данных является ассоциативный массив, который вы узнаете в Scala как карта . (Да, это имеет неприятную двусмысленность с функцией карты , но их использование будет вполне понятно из контекста.) Карты позволяют хранить коллекцию пар ключ-значение и получать доступ к значениям с помощью ключей, связанных с ними, а не через индекс (как со списком).
Примеры случаев, когда вы можете использовать карту:
- Ассоциирование английских слов с их немецкими переводами
- Ассоциирование каждого слова с его количеством в данном тексте
- Ассоциирование каждого слова с его возможными частями речи
В этом посте вы увидите конкретные примеры каждого из них.
Создание карт и доступ к их элементам
Карты довольно интуитивно понятны. Вот пример с несколькими английскими словами и их немецкими переводами. Одним из простых способов создания карты является передача списка пар, где первый элемент каждой пары определяет ключ, а второй — соответствующее значение.
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> 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 .
scala> engWords.indexOf("cat") res2: Int = 1 scala> deuWords(engWords.indexOf("cat")) res3: java.lang.String = Katze
Это на самом деле довольно неэффективно, так же как и другие проблемы. Карты — это то, что нам нужно, и они выполняют работу по извлечению значений для ключей достаточно эффективно.
Оказывается, мы можем взять два списка, которые выровнены таким образом, и очень легко построить карту. Напомним, что объединение двух списков вместе создает один список пар, где каждая пара дает элементы с одинаковым индексом.
scala> engWords.zip(deuWords) res4: List[(java.lang.String, java.lang.String)] = List((dog,Hund), (cat,Katze), (rhinoceros,Nashorn))
Называя toMap метод на такой список пар, мы получим карту.
scala> engWords.zip(deuWords).toMap res5: scala.collection.immutable.Map[java.lang.String,java.lang.String] = Map(dog -> Hund, cat -> Katze, rhinoceros -> Nashorn)
Обратите внимание, что, хотя REPL показывает порядок пар ключ-значение, совпадающий с исходным списком, из которого мы построили карту, элементам карты не присущ порядок.
Вы можете добавить элементы на карту, чтобы создать новую карту, используя оператор + и стрелку -> между каждой парой ключ-значение.
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)
Вы можете добавить одну карту к другой, используя оператор ++ .
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)
Вы можете сделать то же самое, передав список кортежей оператору ++.
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)
И вы можете удалить ключ с карты с помощью оператора -.
scala> engToDeu - "dog" res9: scala.collection.immutable.Map[java.lang.String,java.lang.String] = Map(cat -> Katze, rhinoceros -> Nashorn)
См. Map API для большего количества примеров таких функций. Примечание: на протяжении всего этого поста я придерживаюсь неизменных Карт — если вы просматриваете какие-либо другие учебные пособия и задаетесь вопросом, почему некоторые из этих методов здесь не работают, возможно, они использовали изменяемые Карты, которые мы обсудим позже. ,
Если мы запрашиваем значение, связанное с ключом, которого нет на карте, мы получаем ошибку.
scala> engToDeu("bird") java.util.NoSuchElementException: key not found: bird at scala.collection.MapLike$class.default(MapLike.scala:224) (etc.)
Вы можете проверить, есть ли ключ на карте, используя метод contains .
scala> engToDeu.contains("bird") res10: Boolean = false scala> engToDeu.contains("dog") res11: Boolean = true
Допустим, у вас есть список английских слов и вы хотите найти соответствующие им немецкие слова, и вы хотите защитить себя от исключения NoSuchElementException . Один из способов сделать это , чтобы фильтровать слова , используя содержит , а затем отобразить оставшиеся из них через engToDeu .
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 . Вы задаете ключ в качестве первого аргумента, а затем значение по умолчанию в качестве второго.
scala> engToDeu.getOrElse("dog","???") res1: java.lang.String = Hund scala> engToDeu.getOrElse("armadillo","???") res2: java.lang.String = ???
Весьма распространено использовать getOrElse со значением по умолчанию 0 для Карт, которые содержат статистику, такую как количество слов (см. Ниже), где отсутствие ключа естественно указывает на то, что он имеет, например, счетчик нуля.
Если у вас есть согласованное значение по умолчанию для любых ключей, которых нет на карте, вы можете установить его с помощью метода withDefault.
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] мы получаем).
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] ] . Таким образом, вы можете связать вместе несколько типов с помощью кортежей и использовать параметризованные структуры данных для параметризации другой структуры данных.
Простая задача перевода
Вот мини немецкий / английский словарь в качестве карты.
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 », используя этот словарь: мы просто разбиваем немецкое предложение и затем отображаем его элементы, просматривая каждое слово в словаре.
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
Ладно, не совсем «ото льда, который они освободили, от ручья и ручья», но опять же, это в значительной степени глупейший подход машинного перевода…
Опасность, конечно, в том, что у нас будут слова, которых нет в словаре, что приведет к исключению
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 . Имея список числовых токенов, мы можем получить карту из числовых типов ко всем токенам каждого числа.
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 , сгруппировала все элементы списка с одинаковым значением x , а затем создала Map из каждого x в группу, содержащую ее токены. Итак, мы получаем 2 отображения в список, содержащий 2, и так далее. Это, вероятно, кажется немного странным, но невероятно полезно, когда мы рассматриваем списки, в которых есть более интересные элементы. Для этого давайте вернемся к примеру с пометкой части речи из части 4 этих руководств . Скажем, у нас есть предложение, которое помечено частями речи, например, следующий (составленный) пример, который обеспечивает некоторую двусмысленность тегов.
в темноте высокий мужчина увидел пилу, в которой он нуждался, чтобы срубить темное дерево.
Части речи могут быть аннотированы следующим образом (с большим количеством упрощений и извинений за любое нарушение, причиненное чьей-либо языковой чувствительности).
в / Подготовка к / Det темный / Существительное, / Punc a / Det высокий / Прилагательное человек / Псевдоним / Глагол / Det увидел / Существительное, что / Местоимение он / Местоимение необходимо / Глагол, чтобы / Подготовить человека / Глагол, чтобы / Подготовить вырезать / Глагол / Det dark / Прилагательное дерево / Существительное ./Punc
В части 4 приведено подробное объяснение того, как следующее выражение превращает строку, подобную этой, в список кортежей.
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 . Первое, что может нас заинтересовать, — это увидеть, с какими частями речи связано каждое слово.
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, выполняет две функции: она определяет часть элементов ввода, которая будет группировать различные элементы, и указывает, что эта часть ввода определяет пространство клавиш.
Тем не менее, мы не совсем имеем то, что хотим, то есть набор частей речи, связанных с каждым словом. Вместо этого у нас есть список кортежей, например:
scala> groupedTagged("saw") res21: List[(java.lang.String, java.lang.String)] = List((saw,Verb), (saw,Noun))
Сосредоточившись только на этом, мы можем отобразить это и создать Список с только частями речи, а затем превратить этот Список в Набор с помощью метода toSet , чтобы получить только уникальные части речи.
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)
Преобразование списка в комплект не делать здесь, но считают , который имеет несколько маркеров с одной и той же части-в-речь.
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 на Картах ,
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, чтобы увидеть, имеют ли различные слова различные теги.
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
Это пример того, как структуры данных в структурах данных (здесь «Наборы внутри карты») весьма полезны. ( Упражнение : подумайте, что такое дерево на мгновение и как вы можете реализовать его с помощью списков.)
Есть много вещей, которые вы можете сделать в компьютерной лингвистике с помощью Карт, от слов до их частей речи. Простым примером является вычисление среднего количества тегов на тип слова.
scala> val avgTagsPerType = wordsToTags.values.map(x=>x.size).sum/wordsToTags.size.toDouble avgTagsPerType: Double = 1.2
Если вам не ясно, что здесь происходит, дразните его отдельно в своем собственном REPL!
Мы можем перевернуть наши пары слово / тег по-другому, чтобы узнать, какие слова идут с каждой частью речи. Единственное, что нам нужно сделать, это groupBy на втором элементе каждой пары, а затем сопоставить значения List с их первым элементом и получить Set из них.
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, которую мы сделали выше, дает нам именно то, что нам нужно — на самом деле это даже проще, чем манипуляции со словами / тегами, сделанные выше.
Пример текста, который мы будем использовать, является языком скороговоркой о сурках.
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 .
Учитывая это, вот как мы можем вычислить количество вхождений каждого типа слова. Сначала мы группируем по элементам. Хотя список строк не так интересен, как список кортежей, как у нас со словами и тегами, он все равно дает полезный результат: теперь у нас есть уникальный набор ключей, соответствующий типам элементов, найденных в массиве, и каждому значению соответствует Массив токенов этого типа.
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, немного проще, чем те, что были описаны в предыдущем разделе.
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)
С помощью счетчиков мы теперь можем получить доступ к частотам любого из слов, которые были в тексте.
scala> counts("woodchuck") res5: Int = 4 scala> counts("could") res6: Int = 3
Легко! Конечно, мы обычно хотим рассчитывать количество слов для текстов, которые длиннее и хранятся в файле, а не добавляются явно в код Scala. Следующий урок покажет, как это сделать.
Перебор ключей и значений на карте
Приведенный выше материал показывает некоторые полезные аспекты карт, но, конечно, вы можете сделать с ними гораздо больше, часто требуя перебора пар ключ-значение на карте. Мы будем использовать Карту подсчетов, созданную выше для демонстрации этого.
Вы можете получить доступ только к ключам или только к значениям.
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 .)
Вы можете распечатать все пары ключ -> значение на карте несколькими способами. Одним из них является использование для выражения.
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
И вот другие способы достижения того же результата (вывод опущен, поскольку он одинаков).
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 возвращают значения. Почему ты бы так поступил? Ну, в качестве примера рассмотрим группирование всех слов, которые встречались одинаковое количество раз.
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, отсортированные в порядке убывания по количеству, а затем в алфавитном порядке по слову в каждом отсчете.
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
Options and flatMapping for dealing with missing keys
I pointed out toward the start of this tutorial that we run into trouble if we ask for a key that doesn’t exist in a Map. Let’s go back to the engToDeu Map we began with.
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
There is another way of accessing the elements of a Map, using the get method.
scala> engToDeu.get("dog") res2: Option[java.lang.String] = Some(Hund) scala> engToDeu.get("bird") res3: Option[java.lang.String] = None
Now, the return value is an Option[String]. An Option is either a Some that contains a value or a None, which means there is no value. If you want to get the value out of a Some, you use the get method on Options.
scala> val dogTrans = engToDeu.get("dog") dogTrans: Option[java.lang.String] = Some(Hund) scala> dogTrans.get res4: java.lang.String = Hund
If you just use get on a Map to obtain an Option and then immediately call get on the Option, we get the same behavior we had before.
scala> engToDeu.get("dog").get res6: java.lang.String = Hund scala> engToDeu.get("bird").get java.util.NoSuchElementException: None.get
So, at this point, you are probably thinking that this sounds like a waste of time that is just making things more complex. Wait! It actually is tremendously useful because of pattern matching and the way many methods on sequences work.
First, here is how you can write a protected form of translating the words in a list without getting an exception.
scala> wordsToTranslate.foreach { x => engToDeu.get(x) match { | case Some(y) => println(x + " -> " + y) | case None => | }} dog -> Hund cat -> Katze
I know… this probably still isn’t convincing — it still looks more involved than the conditional we used (far) above to check whether engToDeu contained a given key (at least for this particular example). Hold on… because now we are just about ready for things to get simpler, and learn some useful things about Lists in doing so.
First, you should know about a great method on Lists called flatten. If you have a List of Lists of Strings, you can use flatten to get a single List of Strings. Consider the following example, in which we flatten a List of Lists of Strings and make a single String out of the result with mkString. Notice that the empty List in the third spot of the main List just disappears when we flatten it.
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 .
Flattening in general is pretty useful in its own right. Where it comes to play with Option values is that Options can be thought of a Lists: Somes are like one element Lists and Nones are like empty Lists. So, when you have a List of Options, the flatten method gives you the value in a Some and any Nones just drop away.
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)
This is such a generally useful paradigm that there is a function flatMap which does exactly this.
scala> wordsToTranslate.flatMap(x => engToDeu.get(x)) res14: List[java.lang.String] = List(Hund, Katze)
So, returning to the translation example above, we can now safely skip on by “schiffe” without fuss.
scala> example2.split(" ").flatMap(deuWord => miniDictionary.get(deuWord)).mkString(" ") res15: String = from ice liberated are river and
Whether this is the desired behavior in this particular case is another question (e.g. you really should be doing some special unknown word handling). Nonetheless, you’ll find that flatMap is quite handy in general for this sort of pattern, in which a list of elements is used to retrieve values from a Map that will be missing some of those values.
An example of the further use of Options and flatMap is that you also may create functions that return Options and are thus amenable to flatMapping. Consider a function that squares only odd numbers and throws evens away (note: the % operator is the modulo operator that finds the remainder of division of one number by another — try it in the REPL).
scala> def squareOddNumber (x: Int) = if (x % 2 != 0) Some(x*x) else None squareOddNumber: (x: Int)Option[Int]
If you map over the numbers 1 to 10, you’ll see the Somes and Nones, and if you flatMap it, you get exactly the desired result of the squares of all the odd numbers without any pollution from the evens.
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)
This turns out to be amazingly useful and common, so much so that the expression “just flatMap that shit” has become a common refrain among Scala programmers. Scala programmers even write scripts to remind them to do it. ?
From http://bcomposes.wordpress.com/2011/09/12/first-steps-in-scala-for-beginning-programmers-part-7/