Хвостовая рекурсия Scala из блока finally

Я хочу применить функцию f к каждому элементу List и не останавливаться на первой ошибке, а выдавать только последнюю ошибку (если есть):

@annotation.tailrec
def tryAll[A](xs: List[A])(f: A => Unit): Unit = {
  xs match {
    case x :: xt =>
      try {
        f(x)
      } finally {
        tryAll(xt)(f)
      }
    case _ =>
  }
}

Но приведенный выше код не компилируется - он жалуется, что эта функция не хвостовая рекурсия. Почему нет?


person pathikrit    schedule 14.02.2017    source источник
comment
Функция не является хвостовой рекурсией, поскольку в случае возникновения исключения блок finally не является последним выполняемым кодом.   -  person Hristo Iliev    schedule 14.02.2017
comment
@HristoIliev: Понятно, как тогда я могу написать это эффективно и идиоматично?   -  person pathikrit    schedule 14.02.2017
comment
Я считаю, что идиоматическим способом было бы использовать scala.util.Try для переноса вызовов функций, но я не могу предоставить вам пример кода.   -  person Hristo Iliev    schedule 14.02.2017
comment
Вы неправильно используете @annotation.tailrec   -  person mychemicalro    schedule 14.02.2017
comment
Удаление кода @annotation.tailrec компилируется   -  person mychemicalro    schedule 14.02.2017
comment
@mychemicalro: Дело не в удалении хвостовой записи, а в понимании, почему это не хвостовая рекурсия. Также удаление аннотации вызовет StackOverflow для списков > 1M   -  person pathikrit    schedule 14.02.2017


Ответы (4)


Это решение перебирает все элементы и выдает (выдает) последнюю ошибку, если таковая имеется:

def tryAll[A](xs: List[A])(f: A => Unit): Unit = {
  val res = xs.foldLeft(Option.empty[Throwable]) {
    case (maybeThrowable, a) =>
      Try(f(a)) match {
        case Success(_) => maybeThrowable
        case Failure(e) => Option(e)
      }
  }

  res.foreach(throwable => throw throwable)
}
person Yuval Itzchakov    schedule 14.02.2017

Как упоминал @HristoIliev, ваш метод не может быть хвостовым рекурсивным, потому что вызов finally не обязательно будет хвостовым вызовом. Это означает, что любой метод, использующий try таким образом, не будет хвостовой рекурсией. См. также этот ответ.

Повторный вызов метода — это странный способ повторять что-либо до тех пор, пока он не добьется успеха, потому что на каждом этапе он выдает исключение, которое вы, по-видимому, не обрабатываете. Вместо этого я бы предложил использовать функциональный подход с Try, выбирая ошибки из представления до тех пор, пока операция не завершится успешно. Единственным недостатком этого подхода является то, что он не генерирует никаких исключений, которые вы могли бы обрабатывать по пути (что также может быть преимуществом!).

def tryAll[A](xs: List[A])(f: A => Unit): Unit =
    xs.view.map(x => Try(f(x))).takeWhile(_.isFailure).force


scala> val list = List(0, 0, 0, 4, 5, 0)

scala> tryAll(list)(a => println(10 / a))
2

Если вы действительно хотите обрабатывать исключения (или только последнее исключение), вы можете изменить тип возвращаемого значения tryAll на List[Try[Unit]] (или просто Try[Unit], если вы измените код так, чтобы он принимал только последнее). Лучше, чтобы тип возвращаемого значения метода описывал часть того, что он на самом деле делает — потенциально возвращает ошибки.

person Michael Zajac    schedule 14.02.2017

Не уверен в намерении метода, но вы можете что-то вроде этого:

  final def tryAll[A](xs: List[A])(f: A => Unit): Unit = {
      xs match {
        case x :: xt =>
          try {
            f(x)
          } catch {
            case e => tryAll(xt)(f)
          }
        case _ => //do something else
      }
    }
person Shawn Xiong    schedule 14.02.2017

Я знаю, как использовать @annotation.tailrec

Из этого:

def fac(n:Int):Int = if (n<=1) 1 else n*fac(n-1)

У вас должно быть это:

@scala.annotation.tailrec 
def facIter(f:Int, n:Int):Int = if (n<2) f else facIter(n*f, n-1) 
def fac(n:Int) = facIter(1,n)
person mychemicalro    schedule 14.02.2017
comment
Вы должны накапливать значения в f, поэтому, когда вы достигнете базового случая, верните его. Это не имеет смысла для вашего метода :/ - person mychemicalro; 14.02.2017