Отменить в начале складки

Какой лучший способ разорвать складку раньше? В качестве упрощенного примера представьте, что я хочу суммировать числа в Iterable, но если я столкнусь с чем-то, чего я не ожидаю (скажем, нечетное число), я мог бы прекратить действие. Это первое приближение

def sumEvenNumbers(nums: Iterable[Int]): Option[Int] = {
  nums.foldLeft (Some(0): Option[Int]) {
    case (Some(s), n) if n % 2 == 0 => Some(s + n)
    case _ => None
  }
}

Однако это решение довольно уродливо (как в случае, если я сделал .foreach и return - он был бы намного чище и понятнее), и, что хуже всего, он пересекает всю итерабельность, даже если он встречает не- - даже номер.

Итак, что было бы лучшим способом написать такую ​​складку, которая заканчивается раньше? Должен ли я просто пойти и написать это рекурсивно, или есть более приемлемый способ?

Ответы

Ответ 1

Мой первый выбор - обычно использовать рекурсию. Он только умеренно менее компактен, потенциально быстрее (конечно, не медленнее), и в раннем завершении может сделать логику более ясной. В этом случае вам нужны вложенные defs, которые немного неудобны:

def sumEvenNumbers(nums: Iterable[Int]) = {
  def sumEven(it: Iterator[Int], n: Int): Option[Int] = {
    if (it.hasNext) {
      val x = it.next
      if ((x % 2) == 0) sumEven(it, n+x) else None
    }
    else Some(n)
  }
  sumEven(nums.iterator, 0)
}

Моим вторым выбором было бы использовать return, поскольку он сохраняет все остальное без изменений, и вам нужно всего лишь обернуть сгиб в def, чтобы у вас было что-то, откуда можно было вернуться - в этом случае у вас уже есть метод, поэтому:

def sumEvenNumbers(nums: Iterable[Int]): Option[Int] = {
  Some(nums.foldLeft(0){ (n,x) =>
    if ((n % 2) != 0) return None
    n+x
  })
}

который в этом конкретном случае намного компактнее, чем рекурсия (хотя нам особенно не повезло с рекурсией, так как нам пришлось делать итеративное/итераторное преобразование). Неисправный поток управления - это то, чего следует избегать, когда все остальное равно, но здесь это не так. Нет вреда в использовании его в тех случаях, когда он ценен.

Если бы я делал это часто и хотел, чтобы он находился где-то в середине метода (поэтому я не мог просто использовать return), я бы, вероятно, использовал обработку исключений для генерации нелокального потока управления. То есть, в конце концов, что хорошо, а обработка ошибок - не единственный раз, когда это полезно. Единственный трюк заключается в том, чтобы избежать создания трассировки стека (что очень медленно), и это легко, потому что черта NoStackTrace и ее дочерняя черта ControlThrowable уже делают это для вас. Scala уже использует это внутренне (фактически, что он реализует возврат изнутри складки!). Позвольте сделать наш собственный (не может быть вложенным, хотя можно это исправить):

import scala.util.control.ControlThrowable
case class Returned[A](value: A) extends ControlThrowable {}
def shortcut[A](a: => A) = try { a } catch { case Returned(v) => v }

def sumEvenNumbers(nums: Iterable[Int]) = shortcut{
  Option(nums.foldLeft(0){ (n,x) =>
    if ((x % 2) != 0) throw Returned(None)
    n+x
  })
}

Здесь, конечно, лучше использовать return, но обратите внимание, что вы можете поместить shortcut в любом месте, а не просто обернуть весь метод.

Следующая строка для меня будет заключаться в повторной реализации fold (либо самостоятельно, либо найти библиотеку, которая делает это), чтобы она могла сигнализировать о досрочном расторжении. Два естественных способа сделать это - не распространять значение, а Option, содержащее значение, где None означает завершение; или использовать вторую функцию индикатора, которая сигнализирует о завершении. Сказка ленивая складка, показанная Ким Штебелем, уже охватывает первый случай, поэтому я покажу второй (с изменчивой реализацией):

def foldOrFail[A,B](it: Iterable[A])(zero: B)(fail: A => Boolean)(f: (B,A) => B): Option[B] = {
  val ii = it.iterator
  var b = zero
  while (ii.hasNext) {
    val x = ii.next
    if (fail(x)) return None
    b = f(b,x)
  }
  Some(b)
}

def sumEvenNumbers(nums: Iterable[Int]) = foldOrFail(nums)(0)(_ % 2 != 0)(_ + _)

(Реализуете ли вы завершение рекурсией, возвратом, лень и т.д.).

Я думаю, что он охватывает основные разумные варианты; есть и другие варианты, но я не уверен, зачем их использовать в этом случае. (Iterator сам подойдет, если у него есть findOrPrevious, но это не так, и дополнительная работа, которую он делает для этого вручную, делает его глупым вариантом для использования здесь.)

Ответ 2

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

Например:

val list = List(2,4,6,8,6,4,2,5,3,2)
list.takeWhile(_ % 2 == 0) //result is List(2,4,6,8,6,4,2)

Это будет отлично работать для Iterator s/Iterable. Решение, которое я предлагаю для вашей "суммы четных чисел, но разрыв по нечетному":

list.iterator.takeWhile(_ % 2 == 0).foldLeft(...)

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

scala> val list = List(2,4,5,6,8)
list: List[Int] = List(2, 4, 5, 6, 8)

scala> def condition(i: Int) = {
     |   println("processing " + i)
     |   i % 2 == 0
     | }
condition: (i: Int)Boolean

scala> list.iterator.takeWhile(condition _).sum
processing 2
processing 4
processing 5
res4: Int = 6

Ответ 3

Вы можете делать то, что хотите, в функциональном стиле, используя ленивую версию foldRight в scalaz. Для более подробного объяснения см. этот пост в блоге. Хотя это решение использует Stream, вы можете эффективно преобразовать Iterable в Stream с помощью iterable.toStream.

import scalaz._
import Scalaz._

val str = Stream(2,1,2,2,2,2,2,2,2)
var i = 0 //only here for testing
val r = str.foldr(Some(0):Option[Int])((n,s) => {
  println(i)
  i+=1
  if (n % 2 == 0) s.map(n+) else None
})

Это только печатает

0
1

что ясно показывает, что анонимная функция вызывается только дважды (т.е. до тех пор, пока она не встретит нечетное число). Это связано с определением foldr, чья подпись (в случае Stream) равна def foldr[B](b: B)(f: (Int, => B) => B)(implicit r: scalaz.Foldable[Stream]): B. Обратите внимание, что анонимная функция принимает параметр по имени в качестве второго аргумента, поэтому его не нужно оценивать.

Btw, вы все равно можете написать это с помощью решения соответствия шаблону OP, но я нахожу if/else и карту более элегантной.

Ответ 4

Ну, Scala разрешает не локальные возвращения. Существуют разные мнения о том, хорош ли этот стиль.

scala> def sumEvenNumbers(nums: Iterable[Int]): Option[Int] = {
     |   nums.foldLeft (Some(0): Option[Int]) {
     |     case (None, _) => return None
     |     case (Some(s), n) if n % 2 == 0 => Some(s + n)
     |     case (Some(_), _) => None
     |   }
     | }
sumEvenNumbers: (nums: Iterable[Int])Option[Int]

scala> sumEvenNumbers(2 to 10)
res8: Option[Int] = None

scala> sumEvenNumbers(2 to 10 by 2)
res9: Option[Int] = Some(30)

EDIT:

В этом конкретном случае, как предложил @Arjan, вы также можете:

def sumEvenNumbers(nums: Iterable[Int]): Option[Int] = {
  nums.foldLeft (Some(0): Option[Int]) {
    case (Some(s), n) if n % 2 == 0 => Some(s + n)
    case _ => return None
  }
}

Ответ 5

У Cats есть метод foldM, который делает короткое замыкание (для Vector, List, Stream ,...).

Это работает следующим образом:

def sumEvenNumbers(nums: Stream[Int]): Option[Long] = {
  import cats.implicits._
  nums.foldM(0L) {
    case (acc, c) if c % 2 == 0 => Some(acc + c)
    case _ => None
  }
}

Как только один из элементов коллекции не является четным, он возвращается.

Ответ 6

@Rex Kerr ваш ответ мне помог, но мне нужно было настроить его, чтобы использовать Либо

  
  def foldOrFail[A,B,C,D](map: B => Either[D, C])(merge: (A, C) => A)(initial: A)(it: Iterable[B]): Either[D, A] = {
    val ii= it.iterator
    var b= initial
    while (ii.hasNext) {
      val x= ii.next
      map(x) match {
        case Left(error) => return Left(error)
        case Right(d) => b= merge(b, d)
      }
    }
    Right(b)
  }

Ответ 7

Вы можете попробовать использовать временный var и использовать takeWhile. Вот версия.

  var continue = true

  // sample stream of 2 and then a stream of 3's.

  val evenSum = (Stream.fill(10)(2) ++ Stream.fill(10)(3)).takeWhile(_ => continue)
    .foldLeft(Option[Int](0)){

    case (result,i) if i%2 != 0 =>
          continue = false;
          // return whatever is appropriate either the accumulated sum or None.
          result
    case (optionSum,i) => optionSum.map( _ + i)

  }

В этом случае evenSum должен быть Some(20).

Ответ 8

Более подходящим решением будет использование span:

val (l, r) = numbers.span(_ % 2 == 0)
if(r.isEmpty) Some(l.sum)
else None

... но он перемещается по списку два раза, если все числа четные

Ответ 9

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

Ответ 10

Только для "академических" причин (:

var headers = Source.fromFile(file).getLines().next().split(",")
var closeHeaderIdx = headers.takeWhile { s => !"Close".equals(s) }.foldLeft(0)((i, S) => i+1)

Принимает два раза, но это хороший лайнер. Если "Закрыть" не найден, он вернет

headers.size

Еще один (лучший):

var headers = Source.fromFile(file).getLines().next().split(",").toList
var closeHeaderIdx = headers.indexOf("Close")