Предисловие
Это четвертая часть руководств для начинающих программистов, попадающих в 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 nums 2 = nums.map(x = >x+ 1 ) nums 2 : List[Int] = List( 2 , 3 , 4 ) scala> nums 2 .map(x = >x*x) res 9 : 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) res 12 : 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( " " ) res 19 : String = the/DT program/NN halted/VB |
Наконец, здесь все это в один шаг.
1
2
|
scala> ( for ((token, tag) <- tokens.zip(tags)) yield token + "/" + tag).mkString( " " ) res 23 : 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 = > Tuple 2 (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( " " ) res 0 : 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 = Tuple 2 (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( "/" )) res 0 : 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 = > Tuple 2 (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) res 1 : 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 = > Tuple 2 (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 +) res 11 : List[Int] = List( 2 , 3 , 4 , 5 , 6 ) scala> numbers.map( _ + 1 ) res 12 : List[Int] = List( 2 , 3 , 4 , 5 , 6 ) scala> numbers.map(x = >x+ 1 ) res 13 : List[Int] = List( 2 , 3 , 4 , 5 , 6 ) scala> numbers.map((x : Int) = > x+ 1 ) res 14 : 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 ) res 15 : 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 res 8 : Int = 9 |
Тем не менее, поскольку фильтр принимает только логическое значение, мы, конечно, можем использовать логическое соединение и дизъюнкцию для упрощения вещей. И нам не нужно сохранять промежуточные переменные. Вот один лайнер.
1
2
|
scala> courseTags.filter(x = > x ! = "Noun" && x ! = "Adjective" ).length res 9 : Int = 9 |
Если все, что нам нужно, это количество элементов, мы можем вместо этого просто использовать count с тем же предикатом.
1
2
|
scala> courseTags.count(x = > x ! = "Noun" && x ! = "Adjective" ) res 10 : Int = 9 |
В качестве упражнения попробуйте выполнить однострочную строку, которая начинается с sentRaw и предоставляет значение « resX: Int = 9 » (где X — это то, что вы получаете в своем Scala REPL).
В следующем уроке мы увидим, как использовать уменьшение и свертывание для вычисления агрегированных результатов из списка.
Ссылка: Первые шаги в Scala для начинающих программистов, часть 4 от нашего партнера JCG Джейсона Болдриджа в блоге Bcomposes .
Статьи по Теме :
- Scala Tutorial — Scala REPL, выражения, переменные, основные типы, простые функции, сохранение и запуск программ, комментарии
- Scala Tutorial — кортежи, списки, методы для списков и строк
- Scala Tutorial — условное исполнение с блоками if-else и соответствием
- Scala Tutorial — регулярные выражения, сопоставление
- Scala Tutorial — регулярные выражения, сопоставления и замены с помощью API scala.util.matching
- Учебник по Scala — Карты, Наборы, groupBy, Параметры, Flatten, FlatMap
- Scala Tutorial — scala.io.Source, доступ к файлам, flatMap, изменяемые Карты
- Scala Tutorial — объекты, классы, наследование, черты, списки с несколькими связанными типами, применение
- Scala Tutorial — скриптинг, компиляция, основные методы, возвращаемые значения функций
- Scala Tutorial — SBT, scalabha, пакеты, системы сборки
- Scala Tutorial — блоки кода, стиль кодирования, замыкания, проект документации scala
- Веселье с функцией композиции в Scala
- Как Scala изменил мой взгляд на мой Java-код
- Какие функции Java были исключены в Scala?
- Тестирование с помощью Scala
- Вещи, которые должен знать каждый программист