Статьи

Вопросы студентов о Scala, часть 1

Предисловие

В настоящее время я преподаю курс прикладного анализа текста и использую Scala в качестве языка программирования, который преподается и используется в курсе. Вместо того, чтобы создавать больше учебников, я решил взять страницу из пьесы Брайана Даннинга в его подкасте « Скептоид» (очень рекомендуется), когда он отвечает на вопросы студентов. Итак, я попросил студентов курса представить вопросы о Скале, которые у них были, основываясь на чтениях и заданиях, которые были до сих пор. Этот пост охватывает более половины из них — остальное будет рассмотрено в последующем посте.
Я начну с некоторых более простых вопросов, и вопросы и / или ответы постепенно переходят к темам более среднего уровня. Предложения и комментарии по улучшению любого из ответов очень приветствуются!

Основные вопросы

Q. Относительно адресации частей переменных: Для адресации отдельных частей списков нумерация элементов равна (Список 0,1,2 и т. Д.), То есть первый элемент называется «0». Похоже, то же самое для массивов и карт, но не для кортежей — чтобы получить первый элемент кортежа, мне нужно использовать Tuple._1. Почему это?
О. Это просто вопрос соглашения — кортежи использовали индекс на основе 1 в других языках, таких как Haskell, и кажется, что Scala приняла ту же конвенцию / традицию. Увидеть:
http://stackoverflow.com/questions/6241464/why-are-the-indexes-of-scala-tuples-1-based

В. Кажется, что Scala не распознает граничный символ «b» как регулярное выражение. Есть ли что-то подобное в Scala?
А. Скала распознает граничные символы. Например, следующий сеанс REPL объявляет регулярное выражение, которое находит «the» с границами, и успешно извлекает три токена «the» в примере предложения.

1
2
3
4
5
6
7
8
scala> val TheRE = """\bthe\b""".r
TheRE: scala.util.matching.Regex = \bthe\b
 
scala> val sentence = "She think the man is a stick-in-the-mud, but the man disagrees."
sentence: java.lang.String = She think the man is a stick-in-the-mud, but the man disagrees.
 
scala> TheRE.findAllIn(sentence).toList
res1: List[String] = List(the, the, the)

В. Почему метод «split» не работает с аргументами? Пример: val arg = args.split (»«). Аргументы правильные, так что split должен работать?
О. Переменная args является массивом, поэтому split на них не работает. По сути, массивы уже разделены.

В. Какова основная разница между foo.mapValues ​​(x => x.length) и foo.map (x => x.length) . В некоторых местах один работает, а другой нет.
О. Функция map работает со всеми типами последовательностей, включая Seqs и Maps (обратите внимание, что Maps можно рассматривать как последовательности Tuple2s). Однако функция mapValues работает только на картах. По сути, это удобная функция. Как пример, давайте начнем с простой карты от Ints до Ints.

1
2
scala> val foo = List((1,2),(3,4)).toMap
foo: scala.collection.immutable.Map[Int,Int] = Map(1 -> 2, 3 -> 4)

Теперь рассмотрим задачу добавления 2 к каждому значению на карте. Это можно сделать с помощью функции map следующим образом.

1
2
scala> foo.map { case(key,value) => (key,value+2) }
res5: scala.collection.immutable.Map[Int,Int] = Map(1 -> 4, 3 -> 6)

Итак, функция map выполняет итерации по парам ключ / значение. Нам нужно сопоставить их обоих, а затем вывести ключ и измененное значение, чтобы создать новую карту. Функция mapValues делает это немного проще.

1
2
scala> foo.mapValues(2+)
res6: scala.collection.immutable.Map[Int,Int] = Map(1 -> 4, 3 -> 6)

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

01
02
03
04
05
06
07
08
09
10
11
scala> val sentence = "here is a sentence with some words".split(" ").toList
sentence: List[java.lang.String] = List(here, is, a, sentence, with, some, words)
 
scala> sentence.map(_.length)
res7: List[Int] = List(4, 2, 1, 8, 4, 4, 5)
 
scala> val firstCharTokens = sentence.groupBy(x=>x(0))
firstCharTokens: scala.collection.immutable.Map[Char,List[java.lang.String]] = Map(s -> List(sentence, some), a -> List(a), i -> List(is), h -> List(here), w -> List(with, words))
 
scala> firstCharTokens.mapValues(_.length)
res9: scala.collection.immutable.Map[Char,Int] = Map(s -> 2, a -> 1, i -> 1, h -> 1, w -> 2)

В. Есть ли какая-либо функция, которая разбивает список на два списка с элементами в чередующихся позициях исходного списка? Например,
MainList = (1,2,3,4,5,6)
List1 = (1,3,5)
List2 = (2,4,6)
О. Учитывая точный основной список, который вы предоставили, можно использовать функцию разбиения и использовать операцию по модулю, чтобы увидеть, делится ли значение равномерно на 2 или нет.

1
2
3
4
5
scala> val mainList = List(1,2,3,4,5,6)
mainList: List[Int] = List(1, 2, 3, 4, 5, 6)
 
scala> mainList.partition(_ % 2 == 0)
res0: (List[Int], List[Int]) = (List(2, 4, 6),List(1, 3, 5))

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
scala> val unordered = List("b","2","a","4","z","8")
unordered: List[java.lang.String] = List(b, 2, a, 4, z, 8)
 
scala> unordered.zipWithIndex
res1: List[(java.lang.String, Int)] = List((b,0), (2,1), (a,2), (4,3), (z,4), (8,5))
 
scala> val (evens, odds) = unordered.zipWithIndex.partition(_._2 % 2 == 0)
evens: List[(java.lang.String, Int)] = List((b,0), (a,2), (z,4))
odds: List[(java.lang.String, Int)] = List((2,1), (4,3), (8,5))
 
scala> evens.map(_._1)
res2: List[java.lang.String] = List(b, a, z)
 
scala> odds.map(_._1)
res3: List[java.lang.String] = List(2, 4, 8)

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

В. Как преобразовать Список в Вектор и наоборот?
A. Используйте toIndexSeq и toList .

01
02
03
04
05
06
07
08
09
10
11
scala> val foo = List(1,2,3,4)
foo: List[Int] = List(1, 2, 3, 4)
 
scala> val bar = foo.toIndexedSeq
bar: scala.collection.immutable.IndexedSeq[Int] = Vector(1, 2, 3, 4)
 
scala> val baz = bar.toList
baz: List[Int] = List(1, 2, 3, 4)
 
scala> foo == baz
res0: Boolean = true

Q. Преимущество вектора над списком — поиск в постоянном времени. В чем преимущество использования списка над вектором?
О. Список немного быстрее для операций в начале (спереди) последовательности, поэтому, если все, что вы делаете, это делает обход (доступ к каждому элементу по порядку, например, при отображении), то списки вполне адекватны и могут быть более эффективный. У них также есть хорошее поведение для сопоставления с образцом для операторов case.
Тем не менее, общепринятым кажется, что вы должны по умолчанию использовать Векторы. Посмотрите хороший ответ Даниэля Спивака на Stackoverflow:
http://stackoverflow.com/questions/6928327/when-should-i-choose-vector-in-scala

В. При разбиении строк holmes.split («\\ s») — \ n и \ t просто требуется один «\» для распознавания его специальной функциональности, но почему два символа «\» требуются для символа пробела?
О. Это потому, что \ n и \ t действительно что-то значат в строке.

1
2
3
4
5
6
7
scala> println("Here is a line with a tab\tor\ttwo, followed by\na new line.")
Here is a line with a tab    or    two, followed by
a new line.
 
scala> println("This will break\s.")
<console>:1: error: invalid escape character
println("This will break\s.")

Итак, вы предоставляете аргумент String для split, и он использует его для создания регулярного выражения. Учитывая, что \ s не является строковым символом, но является метасимволом регулярных выражений, вам нужно избегать его. Конечно, вы можете использовать split («» \ s »») , но это не совсем лучше в этом случае.

В. Я давно программирую на C ++ и Java. Поэтому я бессознательно ставлю точку с запятой в конце строки. Кажется, что стандартный стиль кодирования Scala не рекомендует использовать точки с запятой. Тем не менее, я видел, что в некоторых случаях требуются точки с запятой, как вы показали в прошлом классе. Есть ли какая-то конкретная причина, по которой точка с запятой теряет свою роль в Scala?
О. Основная причина заключается в улучшении читабельности, поскольку точка с запятой редко требуется при написании стандартного кода в редакторах (в отличие от однострочников в REPL). Однако, если вы хотите сделать что-то в одной строке, например, обрабатывать несколько случаев, вам нужны точки с запятой.

1
2
3
4
5
scala> val foo = List("a",1,"b",2)
foo: List[Any] = List(a, 1, b, 2)
 
scala> foo.map { case(x: String) => x; case(x: Int) => x.toString }
res5: List[String] = List(a, 1, b, 2)

Но, в общем, лучше всего разбить эти случаи на несколько строк в любом реальном коде.

В. Нет ли способа использовать _ в map-подобных методах для коллекций, состоящих из пар? Например, List ((1,1), (2,2)). Map (e => e._1 + e._2) работает, но List ((1,1), (2,2)). Map (_._ 1 + _._ 2) не работает.
A. Область, в которой _ остается не однозначным, заканчивается после первого вызова, поэтому вы можете использовать его только один раз. В любом случае, лучше использовать оператор case, который проясняет, каковы члены пар.

1
2
scala>  List((1,1),(2,2)).map { case(num1, num2) => num1+num2 }
res6: List[Int] = List(2, 4)

В. Я не уверен в точном значении и разнице между «=>» и «->». Кажется, они оба означают что-то вроде «применить X к Y», и я вижу, что каждый из них используется в определенном контексте, но какая логика стоит за этим?
О. Использование -> просто создает Tuple2, как довольно ясно из следующего фрагмента.

1
2
3
4
5
6
7
8
scala> val foo = (1,2)
foo: (Int, Int) = (1,2)
 
scala> val bar = 1->2
bar: (Int, Int) = (1,2)
 
scala> foo == bar
res11: Boolean = true

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

1
2
3
4
5
scala> Map(("a",1),("b",2))
res9: scala.collection.immutable.Map[java.lang.String,Int] = Map(a -> 1, b -> 2)
 
scala> Map("a"->1,"b"->2)
res10: scala.collection.immutable.Map[java.lang.String,Int] = Map(a -> 1, b -> 2)

Второе мне кажется более читабельным.
Использование => указывает, что вы определяете функцию. Основная форма АРГУМЕНТЫ => РЕЗУЛЬТАТ.

01
02
03
04
05
06
07
08
09
10
11
scala> val addOne = (x: Int) => x+1
addOne: Int => Int = <function1>
 
scala> addOne(2)
res7: Int = 3
 
scala> val addTwoNumbers = (num1: Int, num2: Int) => num1+num2
addTwoNumbers: (Int, Int) => Int = <function2>
 
scala> addTwoNumbers(3,5)
res8: Int = 8

Обычно вы используете его при определении анонимных функций в качестве аргументов таких функций, как map , filter и тому подобное.

В. Есть ли более удобный способ выражения гласных как [AEIOUaeiou] и согласных как [BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz] в RegExes?
О. Вы можете использовать строки при определении регулярных выражений, поэтому у вас может быть переменная для гласных и одна для согласных.

01
02
03
04
05
06
07
08
09
10
11
12
13
scala> val vowel = "[AEIOUaeiou]"
vowel: java.lang.String = [AEIOUaeiou]
 
scala> val consonant = "[BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz]"
consonant: java.lang.String = [BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz]
 
scala> val MyRE = ("("+vowel+")("+consonant+")("+vowel+")").r
MyRE: scala.util.matching.Regex = ([AEIOUaeiou])([BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz])([AEIOUaeiou])
 
scala> val MyRE(x,y,z) = "aJE"
x: String = a
y: String = J
z: String = E

Q. «\ b» в RegExes обозначает границу, верно? Таким образом, он также фиксирует «-». Но если у меня есть одна строка «sdnfeorgn», она НЕ охватывает границы этого, это правильно? И если так, то почему бы и нет?
О. Потому что в этой строке нет границ!

Промежуточные вопросы

В. Функция flatMap берет списки списков и объединяет их в один список. Но в примере:

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)

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

Я попытался запустить код с функцией, возвращающей только число или None. Это показало ошибку. Так есть ли способ использовать flatmap без списков параметров и просто список. Например, List (1, None, 9, None, 25) должен быть возвращен как List (1, 9, 25) .
О. Нет, это не будет работать, потому что List (1, None, 9, None, 25) смешивает параметры с Ints .

1
2
scala> val mixedup = List(1, None, 9, None, 25)
mixedup: List[Any] = List(1, None, 9, None, 25)

Итак, ваша функция должна возвращать Option, что означает возвращение Somes или Nones . Тогда flatMap будет работать счастливо.
Один из способов думать о Options заключается в том, что они похожи на списки с нулем или одним элементом, что можно заметить по параллелям в следующем фрагменте.

01
02
03
04
05
06
07
08
09
10
11
scala> val foo = List(List(1),Nil,List(3),List(6),Nil)
foo: List[List[Int]] = List(List(1), List(), List(3), List(6), List())
 
scala> foo.flatten
res12: List[Int] = List(1, 3, 6)
 
scala> val bar = List(Option(1),None,Option(3),Option(6),None)
bar: List[Option[Int]] = List(Some(1), None, Some(3), Some(6), None)
 
scala> bar.flatten
res13: List[Int] = List(1, 3, 6)

В. Есть ли в scala универсальные шаблоны (например, C ++, Java)? например. в C ++ мы можем использовать vector <int>, vector <string> и т. д. Возможно ли это в scala? Если так, то как?
О. Да, каждый тип коллекции параметризован. Обратите внимание, что каждая из следующих переменных параметризована типом элементов, с которыми они инициализируются.

1
2
3
4
5
6
7
8
scala> val foo = List(1,2,3)
foo: List[Int] = List(1, 2, 3)
 
scala> val bar = List("a","b","c")
bar: List[java.lang.String] = List(a, b, c)
 
scala> val baz = List(true, false, true)
baz: List[Boolean] = List(true, false, true)

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
scala> class Flexible[T] (val data: T)
defined class Flexible
 
scala> val foo = new Flexible(1)
foo: Flexible[Int] = Flexible@7cd0570e
 
scala> val bar = new Flexible("a")
bar: Flexible[java.lang.String] = Flexible@31b6956f
 
scala> val baz = new Flexible(true)
baz: Flexible[Boolean] = Flexible@5b58539f
 
scala> foo.data
res0: Int = 1
 
scala> bar.data
res1: java.lang.String = a
 
scala> baz.data
res2: Boolean = true

В. Как мы можем легко создавать, инициализировать и работать с многомерными массивами (и словарями)?
A. Используйте функцию заполнения объекта Array для их создания.

1
2
3
4
5
6
7
8
scala> Array.fill(2)(1.0)
res8: Array[Double] = Array(1.0, 1.0)
 
scala> Array.fill(2,3)(1.0)
res9: Array[Array[Double]] = Array(Array(1.0, 1.0, 1.0), Array(1.0, 1.0, 1.0))
 
scala> Array.fill(2,3,2)(1.0)
res10: Array[Array[Array[Double]]] = Array(Array(Array(1.0, 1.0), Array(1.0, 1.0), Array(1.0, 1.0)), Array(Array(1.0, 1.0), Array(1.0, 1.0), Array(1.0, 1.0)))

После того, как они у вас есть, вы можете перебирать их как обычно.

1
2
3
4
5
scala> val my2d = Array.fill(2,3)(1.0)
my2d: Array[Array[Double]] = Array(Array(1.0, 1.0, 1.0), Array(1.0, 1.0, 1.0))
 
scala> my2d.map(row => row.map(x=>x+1))
res11: Array[Array[Double]] = Array(Array(2.0, 2.0, 2.0), Array(2.0, 2.0, 2.0))

Для словарей (Карт) вы можете использовать изменяемые HashMaps, чтобы создать пустую Карту, а затем добавить к ней элементы. Для этого смотрите этот пост в блоге:
http://bcomposes.wordpress.com/2011/09/19/first-steps-in-scala-for-beginning-programmers-part-8/

В. Является ли функция apply похожа на конструктор в C ++, Java? Где будет применяться функция apply ? Это для инициализации значений атрибутов?
О. Нет, функция apply похожа на любую другую функцию, за исключением того, что она позволяет вам вызывать ее без выписывания «apply». Рассмотрим следующий класс.

1
2
3
4
class AddX (x: Int) {
  def apply(y: Int) = x+y
  override def toString = "My number is " + x
}

Вот как мы можем это использовать.

01
02
03
04
05
06
07
08
09
10
11
scala> val add1 = new AddX(1)
add1: AddX = My number is 1
 
scala> add1(4)
res0: Int = 5
 
scala> add1.apply(4)
res1: Int = 5
 
scala> add1.toString
res2: java.lang.String = My number is 1

Таким образом, метод apply является просто (очень удобным) синтаксическим сахаром, который позволяет вам указать одну функцию как основную для класса, который вы разработали (на самом деле, вы можете иметь несколько методов apply, если каждый из них имеет уникальный список параметров). Например, для списков метод apply возвращает значение по указанному индексу, а для Maps — значение, связанное с данным ключом.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
scala> val foo = List(1,2,3)
foo: List[Int] = List(1, 2, 3)
 
scala> foo(2)
res3: Int = 3
 
scala> foo.apply(2)
res4: Int = 3
 
scala> val bar = Map(1->2,3->4)
bar: scala.collection.immutable.Map[Int,Int] = Map(1 -> 2, 3 -> 4)
 
scala> bar(1)
res5: Int = 2
 
scala> bar.apply(1)
res6: Int = 2

В. В учебном пособии SBT вы обсуждаете «Узел» и «Значение» как классы дел. Какая альтернатива кейсу?
А. Нормальный класс. Классы случая — особый случай. Они делают две вещи (и больше) для вас. Во-первых, вам не нужно использовать «новый» для создания нового объекта. Рассмотрим следующие идентичные классы.

01
02
03
04
05
06
07
08
09
10
11
scala> class NotACaseClass (val data: Int)
defined class NotACaseClass
 
scala> case class IsACaseClass (val data: Int)
defined class IsACaseClass
 
scala> val foo = new NotACaseClass(4)
foo: NotACaseClass = NotACaseClass@a5c0f8f
 
scala> val bar = IsACaseClass(4)
bar: IsACaseClass = IsACaseClass(4)

Это может показаться незначительным, но может значительно улучшить читаемость кода. Например, рассмотрите возможность создания списков в списках внутри списков, если вам приходилось все время использовать «новый». Это определенно верно для Node и Value , которые используются для построения деревьев.
Классы case также поддерживают сопоставление, как показано ниже.

1
2
scala> val IsACaseClass(x) = bar
x: Int = 4

Нормальный класс не может этого сделать.

1
2
3
4
5
6
7
scala> val NotACaseClass(x) = foo
<console>:13: error: not found: value NotACaseClass
val NotACaseClass(x) = foo
^
<console>:13: error: recursive value x needs type
val NotACaseClass(x) = foo
^

Если вы смешаете класс case в List и отобразите его, вы можете сопоставить его с другими классами, такими как Lists и Ints. Рассмотрим следующий гетерогенный список.

1
2
scala> val stuff = List(IsACaseClass(3), List(2,3), IsACaseClass(5), 4)
stuff: List[Any] = List(IsACaseClass(3), List(2, 3), IsACaseClass(5), 4)

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

1
2
3
4
5
6
7
8
9
scala> stuff.map { case List(x,y) => x; case IsACaseClass(x) => x; case x: Int => x }
<console>:13: warning: match is not exhaustive!
missing combination              *           Nil             *             *
 
stuff.map { case List(x,y) => x; case IsACaseClass(x) => x; case x: Int => x }
^
 
warning: there were 1 unchecked warnings; re-run with -unchecked for details
res10: List[Any] = List(3, 2, 5, 4)

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

1
2
3
scala> stuff.map { case List(x,y) => x; case IsACaseClass(x) => x; case x: Int => x; case _ => throw new MatchError }
warning: there were 1 unchecked warnings; re-run with -unchecked for details
res13: List[Any] = List(3, 2, 5, 4)

А еще лучше, вернуть Options (используя None для несопоставленного случая) и flatMapping вместо этого.

1
2
3
scala> stuff.flatMap { case List(x,y) => Some(x); case IsACaseClass(x) => Some(x); case x: Int => Some(x); case _ => None }
warning: there were 1 unchecked warnings; re-run with -unchecked for details
res14: List[Any] = List(3, 2, 5, 4)

Q. В C ++ спецификатор доступа по умолчанию является приватным; в Java нужно указать private или public для каждого члена класса, где, как и в Scala, спецификатор доступа по умолчанию для класса — public. Какова может быть мотивация дизайна, когда одна из целей класса — скрытие данных?
О. Причина в том, что Scala имеет гораздо более совершенную схему спецификации доступа, чем Java, что делает публичный рациональный выбор. Смотрите обсуждение здесь:
http://stackoverflow.com/questions/4656698/default-public-access-in-scala
Другим ключевым аспектом этого является то, что в Scala основной упор делается на использование неизменяемых структур данных, поэтому нет никакой опасности того, что кто-то изменит внутреннее состояние ваших объектов, если вы спроектировали их таким образом. Это, в свою очередь, избавляет от нелепых методов получения и установки, которые размножаются и размножаются в программах Java. См. «Почему добытчики и сеттеры злые» для дальнейшего обсуждения:
http://www.javaworld.com/javaworld/jw-09-2003/jw-0905-toolbox.html
После того, как вы привыкнете к программированию в Scala, все, что так часто встречается в Java-коде для getter / setter, в значительной степени достойно кляпов.
В общем, все еще хорошая идея использовать private [this] в качестве модификатора для методов и переменных, когда они нужны только самому объекту.

В. Как мы определяем перегруженные конструкторы в Scala?
Q. То, как класс определяется в Scala, введенном в руководстве, похоже, имеет только один конструктор. Есть ли способ предоставить несколько конструкторов, таких как Java?
О. Вы можете добавить дополнительные конструкторы с этими объявлениями.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
class SimpleTriple (x: Int, y: Int, z: String) {
  def this (x: Int, z: String) = this(x,0,z)
  def this (x: Int, y: Int) = this(x,y,"a")
  override def toString = x + ":" + y + ":" + z
}
 
scala> val foo = new SimpleTriple(1,2,"hello")
foo: SimpleTriple = 1:2:hello
 
scala> val bar = new SimpleTriple(1,"goodbye")
bar: SimpleTriple = 1:0:goodbye
 
scala> val baz = new SimpleTriple(1,3)
baz: SimpleTriple = 1:3:a

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

1
2
3
4
5
6
7
8
9
class SimpleTripleWithDefaults (x: Int, y: Int = 0, z: String = "a") {
  override def toString = x + ":" + y + ":" + z
}
 
scala> val foo = new SimpleTripleWithDefaults(1)
foo: SimpleTripleWithDefaults = 1:0:a
 
scala> val bar = new SimpleTripleWithDefaults(1,2)
bar: SimpleTripleWithDefaults = 1:2:a

Однако вы не можете опустить средний параметр при указании последнего.

1
2
3
4
5
6
7
scala> val foo = new SimpleTripleWithDefaults(1,"xyz")
<console>:12: error: type mismatch;
found   : java.lang.String("xyz")
required: Int
Error occurred in an application involving default arguments.
val foo = new SimpleTripleWithDefaults(1,"xyz")
^

Но вы можете назвать параметры при инициализации, если хотите это сделать.

1
2
scala> val foo = new SimpleTripleWithDefaults(1,z="xyz")
foo: SimpleTripleWithDefaults = 1:0:xyz

Тогда у вас будет полная свобода изменять параметры.

1
2
scala> val foo = new SimpleTripleWithDefaults(z="xyz",x=42,y=3)
foo: SimpleTripleWithDefaults = 42:3:xyz

Q. Я до сих пор не понимаю разницу между классами и чертами. Думаю, я вижу концептуальную разницу, но я не совсем понимаю, в чем заключается функциональная разница — как создание «черты» отличается от создания класса, возможно, с меньшим количеством методов, связанных с ним?
О. Да, они разные. Во-первых, черты являются абстрактными, что означает, что вы не можете создавать никаких членов. Рассмотрим следующий контраст.

01
02
03
04
05
06
07
08
09
10
11
12
13
scala> class FooClass
defined class FooClass
 
scala> trait FooTrait
defined trait FooTrait
 
scala> val fclass = new FooClass
fclass: FooClass = FooClass@1b499616
 
scala> val ftrait = new FooTrait
<console>:8: error: trait FooTrait is abstract; cannot be instantiated
val ftrait = new FooTrait
^

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

1
2
3
4
5
scala> class FooTraitExtender extends FooTrait
defined class FooTraitExtender
 
scala> val ftraitExtender = new FooTraitExtender
ftraitExtender: FooTraitExtender = FooTraitExtender@53d26552

Это становится более интересным, если у черты есть несколько методов, конечно. Вот черта Animal , которая объявляет два абстрактных метода, makeNoise и doBehavior .

1
2
3
4
trait Animal {
  def makeNoise: String
  def doBehavior (other: Animal): String
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
case class Bear (name: String, defaultBehavior: String = "Regard warily...") extends Animal {
  def makeNoise = "ROAR!"
  def doBehavior (other: Animal) = other match {
    case b: Bear => makeNoise + " I'm " + name + "."
    case m: Mouse => "Eat it!"
    case _ => defaultBehavior
  }
  override def toString = name
}
 
case class Mouse (name: String) extends Animal {
  def makeNoise = "Squeak?"
  def doBehavior (other: Animal) = other match {
    case b: Bear => "Run!!!"
    case m: Mouse => makeNoise + " I'm " + name + "."
    case _ => "Hide!"
  }
  override def toString = name
}

Обратите внимание, что Bear и Mouse имеют разные списки параметров, но оба могут быть животными, потому что они полностью реализуют черту Animal. Теперь мы можем начать создавать объекты классов Bear и Mouse и заставить их взаимодействовать. Нам не нужно использовать «new», потому что они являются классами case (и это также позволило использовать их в выражениях match методов doBehavior ).

1
2
3
4
5
6
7
8
val yogi = Bear("Yogi", "Hello!")
val baloo = Bear("Baloo", "Yawn...")
val grizzly = Bear("Grizzly")
val stuart = Mouse("Stuart")
 
println(yogi + ": " + yogi.makeNoise)
println(stuart + ": " + stuart.makeNoise)
println("Grizzly to Stuart: " + grizzly.doBehavior(stuart))

Мы также можем создать одноэлементный объект типа Animal с помощью следующего объявления.

1
2
3
4
5
6
7
8
object John extends Animal {
  def makeNoise = "Hullo!"
  def doBehavior (other: Animal) = other match {
    case b: Bear => "Nice bear... nice bear..."
    case _ => makeNoise
  }
  override def toString = "John"
}

Здесь Джон — это объект, а не класс. Поскольку этот объект реализует черту Animal , он успешно расширяет ее и может действовать как Animal . Это означает, что медведь, подобный балу, может взаимодействовать с Джоном .

1
println("Baloo to John: " + baloo.doBehavior(John))

Вывод приведенного выше кода при запуске в виде сценария следующий.

Йог: Рев!

Стюарт: Писк?

Гризли Стюарту: Ешь!

Балу Джону: зевать …

Более близкое различие между чертами и абстрактными классами. Фактически, все показанное выше могло бы быть сделано с Animal как абстрактный класс, а не как черта. Одно из отличий состоит в том, что абстрактный класс может иметь конструктор, а черты — нет. Другое ключевое отличие между ними заключается в том, что признаки могут использоваться для поддержки ограниченного множественного наследования, как показано в следующем вопросе / ответе.

В. Поддерживает ли Scala множественное наследование?
О. Да, через черты с реализациями некоторых методов. Вот пример с признаком Clickable, который имеет абстрактный (не реализованный) метод getMessage , реализованный метод click и частную переназначаемую переменную numTimesClicked (последние два ясно показывают, что признаки отличаются от интерфейсов Java).

1
2
3
4
5
6
7
8
9
trait Clickable {
  private var numTimesClicked = 0
  def getMessage: String
  def click = {
    val output = numTimesClicked + ": " + getMessage
    numTimesClicked += 1
    output
  }
}

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

1
2
3
class MessageBearer (val message: String) {
  override def toString = message
}

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

1
2
3
class ClickableMessageBearer(message: String) extends MessageBearer(message) with Clickable {
  def getMessage = message
}

ClickableMessageBearer теперь обладает способностями как MessageBearers (которая должна быть в состоянии получить его сообщение), так и Clickables .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
scala> val cmb1 = new ClickableMessageBearer("I'm number one!")
cmb1: ClickableMessageBearer = I'm number one!
 
scala> val cmb2 = new ClickableMessageBearer("I'm number two!")
cmb2: ClickableMessageBearer = I'm number two!
 
scala> cmb1.click
res3: java.lang.String = 0: I'm number one!
 
scala> cmb1.message
res4: String = I'm number one!
 
scala> cmb1.click
res5: java.lang.String = 1: I'm number one!
 
scala> cmb2.click
res6: java.lang.String = 0: I'm number two!
 
scala> cmb1.click
res7: java.lang.String = 2: I'm number one!
 
scala> cmb2.click
res8: java.lang.String = 1: I'm number two!

В. Почему есть функции toString , toInt и toList , но нет функции toTuple ?
О. Это основной вопрос, который ведет непосредственно к более продвинутой теме последствий . Есть несколько причин этого. Для начала важно понять, что существует множество типов кортежей, начиная с кортежа с одним элементом (Tuple1) до 22 элементов (Tuple22). Обратите внимание, что когда вы используете (,) для создания кортежа, он неявно вызывает конструктор для соответствующего TupleN правильной арности.

1
2
3
4
5
6
7
8
scala> val b = (1,2,3)
b: (Int, Int, Int) = (1,2,3)
 
scala> val c = Tuple3(1,2,3)
c: (Int, Int, Int) = (1,2,3)
 
scala> b==c
res4: Boolean = true

Учитывая это, очевидно, не имеет смысла иметь функцию toTuple для Seqs (последовательностей), которая длиннее 22. Это означает, что не существует универсального способа иметь, скажем, List или Array, а затем вызывать toTuple для него и ожидать надежного поведения. случаться.
Однако, если вам нужна эта функциональность (даже если она ограничена приведенным выше ограничением максимум в 22 элемента), Scala позволяет вам «добавлять» методы в существующие классы, используя неявные определения. Вы можете найти множество дискуссий о последствиях, выполнив поиск по «scala implicits». Но вот пример, который показывает, как это работает для этого конкретного случая.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
val foo = List(1,2)
val bar = List(3,4,5)
val baz = List(6,7,8,9)
 
foo.toTuple
 
class TupleAble[X] (elements: Seq[X]) {
  def toTuple = elements match {
    case Seq(a) => Tuple1(a)
    case Seq(a,b) => (a,b)
    case Seq(a,b,c) => (a,b,c)
    case _ => throw new RuntimeException("Sequence too long to be handled by toTuple: " + elements)
  }
}
 
foo.toTuple
 
implicit def seqToTuple[X](x: Seq[X]) = new TupleAble(x)
 
foo.toTuple
bar.toTuple
baz.toTuple

Если вы поместите это в Scala REPL, вы увидите, что при первом вызове foo.toTuple появляется ошибка:

1
2
3
4
scala> foo.toTuple
<console>:9: error: value toTuple is not a member of List[Int]
foo.toTuple
^

Обратите внимание, что класс TupleAble принимает Seq в своем конструкторе, а затем предоставляет метод toTuple , используя этот Seq. Это может сделать это для Seqs с 1, 2 или 3 элементами, и, кроме того, оно выдает исключение. (Конечно, мы могли бы продолжать перечислять больше случаев и использовать до 22 наборов элементов, но это показывает смысл.)
Второй вызов foo.toTuple по- прежнему не работает — и это потому, что foo — это List (разновидность Seq), а для списков нет метода toTuple . Вот тут и появляется неявная функция seqToTuple — как только она объявлена, Scala отмечает, что вы пытаетесь вызвать toTuple для Seq, отмечает, что такой функции для Seqs нет, но видит, что существует неявное преобразование из Seqs в TupleAbles через seqToTuple , и затем он видит, что TupleAble имеет метод toTuple . Основываясь на этом, он компилирует и производит желаемое поведение. Это очень удобная возможность Scala, которая может действительно упростить ваш код, если вы используете его правильно и с осторожностью.

Справка: вопросы студентов о Scala, часть 1 от нашего партнера JCG