Scala foldLeft, пока выполняются некоторые условия

Как эмулировать следующее поведение в Scala? то есть продолжать сбрасывать, пока выполняются некоторые определенные условия на аккумуляторе.

def foldLeftWhile[B](z: B, p: B => Boolean)(op: (B, A) => B): B

Например

scala> val seq = Seq(1, 2, 3, 4)
seq: Seq[Int] = List(1, 2, 3, 4)
scala> seq.foldLeftWhile(0, _ < 3) { (acc, e) => acc + e }
res0: Int = 1
scala> seq.foldLeftWhile(0, _ < 7) { (acc, e) => acc + e }
res1: Int = 6

ОБНОВЛЕНИЯ:

Основываясь на ответе @Dima, я понял, что мое намерение было немного побочным. Поэтому я сделал его синхронизированным с takeWhile, т.е. продвижения не было бы, если предикат не совпадает. И добавьте еще несколько примеров, чтобы было понятнее. (Примечание: это не будет работать с Iterators)


person ntviet18    schedule 11.12.2018    source источник
comment
Ну, я предполагаю, что этого нет в стандартной библиотеке, потому что, если условие исходит из самого аккумулятора, то его можно учитывать напрямую, например. (acc, e) => if (acc < 3) acc+e else acc. Вы даже можете создать функцию более высокого порядка, чтобы создать окончательный аккумулятор из его базы и состояния. Я понимаю, что это может быть менее эффективно, чем ранний выход из фолда, но в остальном это выглядит эквивалентно.   -  person GPI    schedule 11.12.2018
comment
@GPI, согласен. Но я думаю дело не только в эффективности. Как мы обсуждали ниже, предикат может позже соответствовать другому условию, и это, вероятно, даст неожиданный результат.   -  person ntviet18    schedule 12.12.2018


Ответы (6)


Во-первых, обратите внимание, что ваш пример кажется неправильным. Если я правильно понимаю то, что вы описываете, результатом должно быть 1 (последнее значение, на котором выполнялся предикат _ < 3), а не 6

Самый простой способ сделать это — использовать оператор return, который очень не одобряется в scala, но я подумал, что упомяну его для полноты картины.

def foldLeftWhile[A, B](seq: Seq[A], z: B, p: B => Boolean)(op: (B, A) => B): B = foldLeft(z) { case (b, a) => 
   val result = op(b, a) 
   if(!p(result)) return b
   result
}

Так как мы хотим избежать использования возврата, возможно, scanLeft:

seq.toStream.scanLeft(z)(op).takeWhile(p).last

Это немного расточительно, потому что он накапливает все (совпадающие) результаты. Вы могли бы использовать iterator вместо toStream, чтобы избежать этого, но Iterator по какой-то причине не имеет .last, так что вам придется просмотреть его в явном виде дополнительное время:

 seq.iterator.scanLeft(z)(op).takeWhile(p).foldLeft(z) { case (_, b) => b }
person Dima    schedule 11.12.2018
comment
Спасибо, я думаю, что это ваше первое решение - очень хорошая идея, но я не уверен, что оно будет работать все время, например. Seq(1, 2, 2, 1).foldLeftWhile(0, _ < 5) { case (acc, e) => acc + e } может вернуть 4 вместо 3 - person ntviet18; 11.12.2018
comment
Я проголосовал за ваше второе решение. Это очень лаконично :) - person ntviet18; 11.12.2018
comment
Делаю быструю проверку. И оказывается, ваше первое решение тоже работает как по волшебству. Не могли бы вы объяснить, почему оператор return игнорирует закрытие foldLeft? - person ntviet18; 11.12.2018
comment
@ ntviet18 да, это магия return в scala. Он, по сути, игнорирует все лямбды вокруг него и просто возвращается до уровня выше ближайшего метода. Он делает это, выбрасывая специальное исключение, которое перехватывается в конце метода. Вот почему это обычно считается запахом кода - слишком много волшебства, нет ссылочной прозрачности и вообще трудно рассуждать. - person Dima; 12.12.2018
comment
Спасибо за объяснение - person ntviet18; 12.12.2018

Довольно просто определить, что вы хотите в scala. Вы можете определить неявный класс, который добавит вашу функцию к любому TraversableOnce (включая Seq).

implicit class FoldLeftWhile[A](trav: TraversableOnce[A]) {
  def foldLeftWhile[B](init: B)(where: B => Boolean)(op: (B, A) => B): B = {
    trav.foldLeft(init)((acc, next) => if (where(acc)) op(acc, next) else acc)
  }
}
Seq(1,2,3,4).foldLeftWhile(0)(_ < 3)((acc, e) => acc + e)

Обновление, так как вопрос был изменен:

implicit class FoldLeftWhile[A](trav: TraversableOnce[A]) {
  def foldLeftWhile[B](init: B)(where: B => Boolean)(op: (B, A) => B): B = {
    trav.foldLeft((init, false))((a,b) => if (a._2) a else {
      val r = op(a._1, b)
      if (where(r)) (op(a._1, b), false) else (a._1, true)
    })._1
  }
}

Обратите внимание, что я разделил ваш (z: B, p: B => Boolean) на две функции более высокого порядка. Это просто личное предпочтение стиля scala.

person Jack Davidson    schedule 11.12.2018
comment
Думаю, это нарушает семантику while для немонотонных предикатов: Seq(1,2,3,4).foldLeftWhile(0)(_ % 2 != 0) { case (b, a) => b } возвращает 3, а должно быть 1 - person Dima; 11.12.2018
comment
Проголосовал, потому что это, кажется, отвечает на мой первоначальный вопрос. Спасибо @Дима - person ntviet18; 11.12.2018
comment
@ Дима Я думаю, вы имели в виду «(a, b) => b», поскольку «(b, a) => b» всегда возвращает ваше начальное значение. Независимо от того, применяете вы мое исправление или нет, это на самом деле возвращает 0, поскольку 0 % 2 != 0 немедленно возвращает false, и ваша функция свертки фактически никогда не запускается. Вы хотели передать начальное значение, отличное от 0? Если вы примете мое исправление и пройдете 1, вы получите 2. Разве это не то, чего вы хотели бы или ожидали? - person Jack Davidson; 12.12.2018
comment
@JackDavidson, да, я напортачил с этим примером. Дело в том, что если предикат становится ложным, ваша функция продолжает работать... поэтому, если затем он снова становится истинным, он снова начинает накапливаться. Поскольку acc является единственным параметром, это не имеет значения, пока предикат чистый, но он также зависит от некоторого внешнего состояния, которое нарушило бы семантику... - person Dima; 12.12.2018
comment
@dima да, это было бы правильно. если предикат не является чистой функцией, нет гарантии, что функция не возобновит обработку входных данных. Я обновил альтернативу, которая отражает новый вопрос и должна навсегда остановиться, когда функция в первый раз возвращает false - person Jack Davidson; 12.12.2018

Как насчет этого:

def foldLeftWhile[A, B](z: B, xs: Seq[A], p: B => Boolean)(op: (B, A) => B): B = {
  def go(acc: B, l: Seq[A]): B = l match {
    case h +: t => 
        val nacc = op(acc, h)
        if(p(nacc)) go(op(nacc, h), t) else nacc
    case _ => acc
  }
  go(z, xs)
}

val a = Seq(1,2,3,4,5,6)
val r = foldLeftWhile(0, a, (x: Int) => x <= 3)(_ + _)
println(s"$r")

Рекурсивно итерируйте коллекцию, пока предикат истинен, а затем верните аккумулятор.

Вы можете попробовать это на scalafiddle

person ElBaulP    schedule 11.12.2018
comment
Это делает дополнительную итерацию: p(result) всегда false (ожидайте, если оно останется верным до конца) - person Dima; 11.12.2018
comment
Спасибо, это ответ на мой первоначальный вопрос. Но я согласен с @Dima, что дальше двигаться не стоит. Итак, я проголосовал за ваш ответ. - person ntviet18; 11.12.2018
comment
@Dima Я не понимаю, не могли бы вы объяснить, почему делает лишнюю итерацию? Я не вижу его. Спасибо! - person ElBaulP; 12.12.2018
comment
@ElBaulP в последний раз, когда p(acc) истинно, вы вызываете go, даже если это не ложно. Вы должны сначала вычислить результат, проверить условие, затем, если оно истинно, передать его go. - person Dima; 12.12.2018

Первый пример: предикат для каждого элемента

Сначала вы можете использовать рекурсивную функцию внутреннего хвоста

implicit class TravExt[A](seq: TraversableOnce[A]) {
  def foldLeftWhile[B](z: B, f: A => Boolean)(op: (A, B) => B): B = {
    @tailrec
    def rec(trav: TraversableOnce[A], z: B): B = trav match {
      case head :: tail if f(head) => rec(tail, op(head, z))
      case _ => z
    }
    rec(seq, z)
  }
}

Или короткая версия

implicit class TravExt[A](seq: TraversableOnce[A]) {
  @tailrec
  final def foldLeftWhile[B](z: B, f: A => Boolean)(op: (A, B) => B): B = seq match {
    case head :: tail if f(head) => tail.foldLeftWhile(op(head, z), f)(op)
    case _ => z
  }
}

Тогда используйте его

val a = List(1, 2, 3, 4, 5, 6).foldLeftWhile(0, _ < 3)(_ + _) //a == 3

Второй пример: для значения аккумулятора:

implicit class TravExt[A](seq: TraversableOnce[A]) {
  def foldLeftWhile[B](z: B, f: A => Boolean)(op: (A, B) => B): B = {
    @tailrec
    def rec(trav: TraversableOnce[A], z: B): B = trav match {
      case _ if !f(z) => z
      case head :: tail => rec(tail, op(head, z))
      case _ => z
    }
    rec(seq, z)
  }
}

Или короткая версия

implicit class TravExt[A](seq: TraversableOnce[A]) {
  @tailrec
  final def foldLeftWhile[B](z: B, f: A => Boolean)(op: (A, B) => B): B = seq match {
    case _ if !f(z) => z
    case head :: tail => tail.foldLeftWhile(op(head, z), f)(op)
    case _ => z
  }
}
person Ivan Aristov    schedule 11.12.2018
comment
Ваше решение очень красивое. Учитывая, что мы расширяем AnyVal, чтобы создать класс значений. Я думаю, что это было бы довольно эффективно :) - person ntviet18; 12.12.2018
comment
С другой стороны, вы должны применить предикат к аккумулятору, но не к элементам - person ntviet18; 12.12.2018

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

очень лаконичное решение от @Dima

implicit class FoldLeftWhile[A](seq: Seq[A]) {

  def foldLeftWhile[B](z: B)(p: B => Boolean)(op: (B, A) => B): B = {
    seq.toStream.scanLeft(z)(op).takeWhile(p).lastOption.getOrElse(z)
  }
}

от @ElBaulP (я немного изменил, чтобы соответствовать комментарию @Dima)

implicit class FoldLeftWhile[A](seq: Seq[A]) {

  def foldLeftWhile[B](z: B)(p: B => Boolean)(op: (B, A) => B): B = {
    @tailrec
    def foldLeftInternal(acc: B, seq: Seq[A]): B = seq match {
      case x :: _ =>
        val newAcc = op(acc, x)
        if (p(newAcc))
          foldLeftInternal(newAcc, seq.tail)
        else
          acc
      case _ => acc
    }

    foldLeftInternal(z, seq)
  }
}

Ответ от меня (включая побочные эффекты)

implicit class FoldLeftWhile[A](seq: Seq[A]) {

  def foldLeftWhile[B](z: B)(p: B => Boolean)(op: (B, A) => B): B = {
    var accumulator = z
    seq
      .map { e =>
        accumulator = op(accumulator, e)
        accumulator -> e
      }
      .takeWhile { case (acc, _) =>
        p(acc)
      }
      .lastOption
      .map { case (acc, _) =>
        acc
      }
      .getOrElse(z)
  }
}
person ntviet18    schedule 11.12.2018

Просто используйте условие перехода к аккумулятору:

seq.foldLeft(0, _ < 3) { (acc, e) => if (acc < 3) acc + e else acc}

Однако вы будете запускать каждую запись последовательности.

person Nicolas Cailloux    schedule 11.12.2018