Подтвердить что ты не робот

Scala: Может ли быть какая-то причина предпочесть `filter + map` над` collect`?

Может ли быть какая-то причина предпочесть filter+map:

list.filter (i => aCondition(i)).map(i => fun(i))

над collect?:

list.collect(case i if aCondition(i) => fun(i))

Тот, у кого collect (один взгляд), выглядит быстрее и чище для меня. Поэтому я всегда буду искать collect.

4b9b3361

Ответ 1

Большинство коллекций Scala с нетерпением применяют операции и (если вы не используете библиотеку макросов, которая делает это для вас) не будут сдерживать операции. Таким образом, filter, за которым следует map, обычно создаются две коллекции (и даже если вы используете Iterator или somesuch, промежуточная форма будет временно создана, хотя и только элемент за раз), тогда как collect не будет.

С другой стороны, collect использует частичную функцию для реализации совместного теста, а частичные функции медленнее, чем предикаты (A => Boolean) при проверке, находится ли что-то в коллекции.

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

xs.filter(p).map(f)
xs.collect{ case x if p(x) => f(x) }

но если вы поставляете встроенные блокировки, collect обычно выглядит более чистым

xs.filter(x < foo(x, x)).map(x => bar(x, x))
xs.collect{ case x if foo(x, x) => bar(x, x) }

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

Теперь, насколько велика разница в производительности? Это меняется, но если мы рассмотрим такой набор:

val v = Vector.tabulate(10000)(i => ((i%100).toString, (i%7).toString))

и вы хотите выбрать вторую запись, основанную на фильтрации первой (так что операции с фильтром и картой очень просто), тогда мы получим следующую таблицу.

Примечание: можно получить ленивые представления в коллекциях и собирать там операции. Вы не всегда возвращаете исходный тип, но всегда можете использовать to получить правильный тип коллекции. Таким образом, xs.view.filter(p).map(f).toVector, из-за представления, не создаст промежуточного элемента. Это также проверено ниже. Было также высказано предположение, что можно xs.flatMap(x => if (p(x)) Some(f(x)) else None) и что это эффективно. Это не так. Он также тестировался ниже. И можно избежать частичной функции, явно создав конструктор: val vb = Vector.newBuilder[String]; xs.foreach(x => if (p(x)) vb += f(x)); vb.result, а результаты для этого также перечислены ниже.

В приведенной ниже таблице проверены три условия: отфильтровать ничего, отфильтровать половину, отфильтровать все. Время нормализовано для фильтрации/отображения (100% = в то же время, что и фильтр/карта, лучше - ниже). Границы ошибок составляют + - 3%.

Производительность различных альтернатив фильтра/карты

====================== Vector ========================
filter/map   collect  view filt/map  flatMap   builder
   100%        44%          64%        440%      30%    filter out none
   100%        60%          76%        605%      42%    filter out half
   100%       112%         103%       1300%      74%    filter out all

Таким образом, filter/map и collect, как правило, довольно близки (при выигрыше collect, когда вы держите много), flatMap намного медленнее во всех ситуациях, и создание строителя всегда выигрывает. (Это верно для Vector. Другие коллекции могут иметь несколько иные характеристики, но тенденции для большинства будут похожи, потому что различия в операциях аналогичны.) Представления в этом тесте, как правило, выигрывают, но они не всегда работают без проблем (и они не намного лучше, чем collect, за исключением пустого случая).

Итак, нижняя строка: предпочитайте filter, затем map, если она помогает ясности, когда скорость не имеет значения, или предпочитает ее для скорости, когда вы отфильтровываете почти все, но все же хотите сохранить функциональность (так что don Не хотите использовать строитель); и в противном случае используйте collect.

Ответ 2

Я предполагаю, что это скорее основано на мнениях, но с учетом следующих определений:

scala> val l = List(1,2,3,4)
l: List[Int] = List(1, 2, 3, 4)

scala> def f(x: Int) = 2*x
f: (x: Int)Int

scala> def p(x: Int) = x%2 == 0
p: (x: Int)Boolean

Какую из двух вы найдете более приятной для чтения:

l.filter(p).map(f)

или

l.collect{ case i if p(i) => f(i) }

(Обратите внимание, что я должен был исправить ваш синтаксис выше, так как вам нужна скобка и case, чтобы добавить условие if).

Я лично считаю, что filter + map гораздо приятнее читать и понимать. Все зависит от точного синтаксиса, который вы используете, но при использовании p и f вам не нужно писать анонимные функции при использовании filter или map, в то время как они вам понадобятся при использовании collect.

У вас также есть возможность использовать flatMap:

l.flatMap(i => if(p(i)) Some(f(i)) else None)

Скорее всего, это будет наиболее эффективным из трех решений, но я считаю его менее приятным, чем map и filter.

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