Scala List.filter с двумя условиями, применяемыми только один раз

Не знаю, возможно ли это, но у меня есть такой код:

val list = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
val evens = list.filter { e => e % 2 == 0 }

if(someCondition) {
  val result = evens.filter { e => e % 3 == 0 }
} else {
  val result = evens.filter { e => e % 5 == 0 }
}

Но я не хочу повторять по всем элементам дважды, так что есть способ, чтобы я мог создать "общие значения для всех элементов в этой коллекции" и применить некоторую другую функцию, чтобы она только один раз?

Ответ 1

Если вы превратите list в ленивую коллекцию, например, Iterator, тогда вы можете применить все операции фильтра (или другие вещи, такие как map и т.д.) за один проход:

val list = (1 to 12).toList
val doubleFiltered: List[Int] =
  list.iterator
    .filter(_ % 2 == 0)
    .filter(_ % 3 == 0)
    .toList
println(doubleFiltered)

Когда вы конвертируете коллекцию в Iterator с .iterator, Scala будет отслеживать выполняемые операции (здесь, два filter s), но будет ждать, чтобы выполнить их до тех пор, пока результат не будет фактически достигнут (здесь, по вызову .toList).

Итак, я могу переписать ваш код следующим образом:

val list = (1 to 12).toList
val evens = list.iterator.filter(_ % 2 == 0)

val result = 
  if(someCondition)
    evens.filter(_ % 3 == 0)
  else
    evens.filter(_ % 5 == 0)

result foreach println

В зависимости от того, что вы хотите сделать, вам может понадобиться Iterator, a Stream или View. Все они лениво вычисляются (поэтому применим однопроходный аспект), но они различаются по типам, например, можно ли их повторять несколько раз (Stream и View) или они сохраняют вычисленное значение для последующего доступа (Stream).

Чтобы действительно увидеть эти разные ленивые поведения, попробуйте запустить этот бит кода и установите <OPERATION> на toList, Iterator, View или toStream:

val result =
  (1 to 12).<OPERATION>
    .filter { e => println("filter 1: " + e); e % 2 == 0 }
    .filter { e => println("filter 2: " + e); e % 3 == 0 }
result foreach println
result foreach println

Здесь вы увидите следующее поведение:

  • list (или любая другая нелазовая коллекция): каждому filter требуется отдельная итерация через коллекцию. Полученная фильтрованная коллекция сохраняется в памяти, так что каждый foreach может просто отображать ее.
  • Iterator: Оба filter и первый foreach выполняются в одной итерации. Второй foreach ничего не делает с момента потребления Iterator. Результаты не сохраняются в памяти.
  • View: Оба вызова foreach приводят к их собственной однопроходной итерации по коллекции для выполнения filters. Результаты не сохраняются в памяти.
  • Stream: И filter, и первый foreach выполняются за одну итерацию. Полученная фильтрованная коллекция сохраняется в памяти, так что каждый foreach может просто отображать ее.

Ответ 2

Вы можете использовать функцию композиции. someCondition здесь вызывается только один раз, когда вы решаете, какую функцию компилировать с помощью:

def modN(n: Int)(xs: List[Int]) = xs filter (_ % n == 0)

val f = modN(2) _ andThen (if (someCondition) modN(3) else modN(5))

val result = f(list)

(Это не делает то, что вы хотите - он по-прежнему пересекает список дважды)

Просто сделайте следующее:

val f: Int => Boolean = if (someCondition) { _ % 3 == 0 } else { _ % 5 == 0 }
val result = list filter (x => x % 2 == 0 && f(x))

или, может быть, лучше:

val n = if (someCondition) 3 else 5
val result = list filter (x => x % 2 == 0 && x % n == 0)

Ответ 3

Не работает ли это:

list.filter{e => e % 2 == 0 && (if (someCondition) e % 3 == 0 else e % 5 == 0)}

также FYI e % 2 == 0 даст вам все четные числа, если вы не назовете val odds по другой причине.

Ответ 4

Вы просто пишете два условия в фильтре:

val list = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)

var result = List(0)
val someCondition = true

result = if (someCondition) list.filter { e => e % 2 == 0 && e % 3 == 0 }
         else               list.filter { e => e % 2 == 0 && e % 5 == 0 }