Scala для эффективности понимания?
-
25-09-2019 - |
Вопрос
В книге «Программирование на Scala», глава 23, автор приводит такой пример:
case class Book(title: String, authors: String*)
val books: List[Book] = // list of books, omitted here
// find all authors who have published at least two books
for (b1 <- books; b2 <- books if b1 != b2;
a1 <- b1.authors; a2 <- b2.authors if a1 == a2)
yield a1
Автор сказал, что это будет переведено на:
books flatMap (b1 =>
books filter (b2 => b1 != b2) flatMap (b2 =>
b1.authors flatMap (a1 =>
b2.authors filter (a2 => a1 == a2) map (a2 =>
a1))))
Но если вы посмотрите на определение метода карты и плоской карты (TraversableLike.scala), вы можете обнаружить, что они определены как циклы for:
def map[B, That](f: A => B)(implicit bf: CanBuildFrom[Repr, B, That]): That = {
val b = bf(repr)
b.sizeHint(this)
for (x <- this) b += f(x)
b.result
}
def flatMap[B, That](f: A => Traversable[B])(implicit bf: CanBuildFrom[Repr, B, That]): That = {
val b = bf(repr)
for (x <- this) b ++= f(x)
b.result
}
Ну, я предполагаю, что это for будет постоянно транслироваться в foreach, а затем транслироваться в оператор while, который является конструкцией, а не выражением. В Scala нет конструкции for, потому что она хочет, чтобы for всегда что-то выдавало.
Итак, я хочу обсудить с вами, почему Scala делает это «Для перевода»?В примере автора используются 4 генератора, которые в конце будут преобразованы в 4-уровневый вложенный цикл for. Я думаю, что производительность будет ужасной, когда books
большой.
Scala поощряет людей использовать этот вид «синтаксического сахара», вы всегда можете увидеть коды, которые активно используют фильтры, карты и плоские карты, что кажется, что программисты забывают, что они на самом деле делают, это вкладывают один цикл в другой, и то, что достигается, это всего лишь чтобы коды выглядели немного короче.какая у тебя идея?
Решение
Для пониманий являются синтаксический сахар для монадической трансформации, и, как таковые, полезны во всех видах мест. При этом они гораздо более многослойные в Scala, чем эквивалентная конструкция Haskell (конечно, Haskell не строго по умолчанию, поэтому нельзя говорить о производительности конструкции, как в Scala).
Также важно, эта конструкция сохраняет то, что делается ясно, и избегает быстро усаживания вдавливания или ненужного личного метода вложенности.
Что касается окончательного рассмотрения, ли это скрывает сложность или нет, я поставим это:
for {
b1 <- books
b2 <- books
if b1 != b2
a1 <- b1.authors
a2 <- b2.authors
if a1 == a2
} yield a1
Очень легко увидеть, что делается, и сложность ясна: B ^ 2 * A ^ 2 (фильтр не изменит сложность), для количества книг и количества авторов. Теперь напишите тот же код в Java, либо с глубокими углублениями, либо с частными методами, и попытайтесь выяснить, в быстром виде, какая сложность кода.
Итак, ИМХО, это не скрывает сложность, но, наоборот, делает его понятно.
Для map
/flatMap
/filter
Определения, которые вы упоминаете, они не принадлежат List
или любой другой класс, поэтому они не будут применены. В принципе,
for(x <- List(1, 2, 3)) yield x * 2
переводится в
List(1, 2, 3) map (x => x * 2)
и это не то же самое, что
map(List(1, 2, 3), ((x: Int) => x * 2)))
Вот как прошло определение, будет называться. Для записи фактическая реализация map
на List
является:
def map[B, That](f: A => B)(implicit bf: CanBuildFrom[Repr, B, That]): That = {
val b = bf(repr)
b.sizeHint(this)
for (x <- this) b += f(x)
b.result
}
Другие советы
Я пишу код так, чтобы его было легко понять и поддерживать.Затем я профилирую.Если есть узкое место, я обращаю на это свое внимание.Если это похоже на то, что вы описали, я подойду к проблеме по-другому.До тех пор я люблю «сахар». Это спасает меня проблему выписать что -то или задуматься об этом.
Есть на самом деле 6 петлей. Один цикл для каждого фильтра / с плоской картой
Пары фильтра-> карты могут быть выполнены в одном цикле с помощью ленивых видов коллекций (метод итератора)
В целом, TT работает 2 вложенных петель для книг, чтобы найти все книжные пары, а затем два вложенных петель, чтобы найти, если автор одной книги находится в списке авторов другого.
Используя простые структуры данных, вы будете делать то же самое при явном порядке.
И, конечно же, пример здесь состоит в том, чтобы показать комплекс «для» петли, не писать наиболее эффективный код. Например, вместо последовательности авторов можно использовать набор, а затем найти, если пересечение не пустое:
for (b1 <- books; b2 <- books; a <- (b1.authors & b2.authors)) yield a
Обратите внимание, что в 2.8, filter
звонок был изменен на withFilter
который ленивый и позволяет избежать построения промежуточной структуры. Видеть Руководство по переходу из фильтра в сфильтр?.
Я считаю причину того, что for
переводится в map
, flatMap
а также withFilter
(а также определения стоимости, если присутствуют) состоит в том, чтобы сделать использование монадских проще.
Как правило, я думаю, если вычисления вы делаете, включают в себя цикл 4 раза, это нормально, используя for
петля. Если вычисление может быть сделано более эффективно, и производительность важно, то вы должны использовать более эффективный алгоритм.
Один след за пределами @ Ittayd ответа на эффективность алгоритма. Стоит отметить, что алгоритм в оригинальном посте (и в книге) является Вложенная петля Присоединяется. Отказ На практике это не эффективный алгоритм для больших наборов данных, а большинство баз данных будут использовать Хеш совокупность здесь вместо этого. В Scala, агрегат хеша будет выглядеть что-то вроде:
(for (book <- books;
author <- book.authors) yield (book, author)
).groupBy(_._2).filter(_._2.size > 1).keys