Scala для эффективности понимания?

В книге «Программирование на 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), вы можете обнаружить, что они определены как для циклов:

   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 побуждает людей использовать этот вид «синтаксического сахара», вы всегда можете увидеть коды, которые в значительной степени используют фильтры, карты и плоские карты, что, кажется, программисты забывают, что они на самом деле делают, это встраивают один цикл в другой, и то, что достигается, остается только чтобы коды выглядели немного короче. какая у тебя идея?


person Sawyer    schedule 18.11.2010    source источник
comment
Откуда берутся упомянутые вами определения map / flatMap / filter?   -  person Daniel C. Sobral    schedule 19.11.2010
comment
@Daniel Ну, эти определения взяты из главы 23.5 книги «Программирование на Scala» - Мартин Орденски. Да, плохо иллюстрировать эту проблему, я изменил ее на определения TraversableLike.scala.   -  person Sawyer    schedule 19.11.2010
comment
Ссылки 10.4 и 10.5 в scala-lang.org/docu/files/ScalaByExample.pdf для тех, у кого нет программирования на Scala   -  person oluies    schedule 19.11.2010
comment
@oluies, спасибо за внимание.   -  person Sawyer    schedule 19.11.2010
comment
Обратите внимание, что они определены как foreach петли. Этого и следовало ожидать, поскольку это единственный метод, который кому-то нужно реализовать на Traversable.   -  person Daniel C. Sobral    schedule 19.11.2010


Ответы (5)


Ибо понимания - это синтаксический сахар для монадических преобразований и, как таковые, полезны во всех сферах. При этом в 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, либо с глубокими отступами, либо с частными методами, и попытайтесь быстро выяснить, какова сложность кода.

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

Что касается упомянутых вами определений _2 _ / _ 3 _ / _ 4_, они не принадлежат к 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
}
person Daniel C. Sobral    schedule 18.11.2010
comment
ИМХО, почему люди никогда не сомневаются, что это, наверное, проблема Scala? Я помню, что в java есть библиотека под названием Lambda4j, которая может заставить Java включать функцию закрытия и писать коды стиля DSL. Он в значительной степени использует интроспекцию и динамические прокси, вы можете писать с ним код очень высокого уровня, но должны платить компромисс производительности. Что ж, после нескольких месяцев изучения Scala я обнаружил, что Scala похожа на Lambda4j, заставляет меня чувствовать, что я использую не язык, а библиотеку, высокоуровневые характеристики излишне инкапсулированы базовыми конструкциями, а не преимуществом языковой дизайн. - person Sawyer; 19.11.2010
comment
@ZZCat Самоанализ и динамические прокси имеют высокую стоимость в JVM, но в целом Scala не использует их. - person Daniel C. Sobral; 19.11.2010
comment
Насколько я понимаю, Scala просто имитирует некоторые функции языков функционального программирования (Hashell), все переводы аналогичны вызовам методов. вы можете определить методы, названные по имени, и делегировать задачи оператору while в теле, но он не делает этого, потому что хочет, чтобы он был более естественным. - person Sawyer; 19.11.2010
comment
Понятия монад в Haskell также переводятся в вызовы методов. Просто говорю' - person Apocalisp; 19.11.2010
comment
@ZZCat Он ничего не симулирует. Он переводит язык на что-то другое, как и любой другой компилируемый язык. В противном случае мы могли бы сказать, что каждый объектно-ориентированный язык имитирует объектно-ориентированный объект, потому что он переводит это в машинный код. - person Daniel C. Sobral; 19.11.2010

Я пишу код, чтобы его было легко понять и поддерживать. Я тогда профиль. Если есть узкое место, я обращаю на это свое внимание. Если дело в чем-то похожем на то, что вы описали, я решу проблему другим способом. А пока я люблю "сахар". Это избавляет меня от необходимости что-то записывать или много думать об этом.

person wheaties    schedule 18.11.2010

Фактически получается 6 петель. Один цикл для каждого фильтра / flatMap / map

Пары фильтр-> карта могут быть выполнены в одном цикле с использованием ленивых представлений коллекций (метод итератора)

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

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

И, конечно же, приведенный здесь пример предназначен для демонстрации сложного цикла for, а не для написания наиболее эффективного кода. Например, вместо последовательности авторов можно использовать Set, а затем определить, не является ли пересечение пустым:

for (b1 <- books; b2 <- books; a <- (b1.authors & b2.authors)) yield a
person IttayD    schedule 18.11.2010
comment
Вы забыли условие if b1! = B2. - person Landei; 18.11.2010

Обратите внимание, что в 2.8 вызов filter был изменен на withFilter, что является ленивым и позволяет избежать создания промежуточной структуры. См. руководство по переходу от фильтра к withFilter?.

Я считаю, что причина того, что for переводится в map, flatMap и withFilter (а также определения значений, если они есть), заключается в том, чтобы упростить использование монад.

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

person huynhjl    schedule 18.11.2010

Одно продолжение ответа @ IttayD об эффективности алгоритма. Стоит отметить, что алгоритм в исходной публикации (и в книге) представляет собой соединение с вложенным циклом. На практике это неэффективный алгоритм для больших наборов данных, и в большинстве баз данных вместо этого будет использоваться агрегат хеширования. В Scala агрегат хеша будет выглядеть примерно так:

(for (book <- books;
      author <- book.authors) yield (book, author)
).groupBy(_._2).filter(_._2.size > 1).keys
person James_pic    schedule 19.02.2014