Scala — состояние в закрытии/анонимной функции

Я пытаюсь взять элементы из списка, пока выполняется определенный предикат. Однако предикат зависит от последнего элемента. Это некоторый код, иллюстрирующий проблему и мое решение

val list = List(1,2,3,1,4)
list.takeWhile {
   // state inside the closure?!
   var curr = 0

   // actual function application
   i => 
   val test = i > curr
   // update state
   curr = i
   test
}

Результат, как и ожидалось,

List(1,2,3)

Однако я не уверен, что это работает случайно или это сделано намеренно scala. Есть ли лучший способ сделать это?

спасибо, Муки


person Muki    schedule 25.01.2015    source источник


Ответы (2)


curr не находится "внутри замыкания". Концепция называется «замыканием», потому что анонимная функция

i => 
  val test = i > curr
  curr = i
  test

является открытым выражением (у него есть свободная переменная curr), которое закрыто путем привязки curr к var, объявленному вне функции.

Блок в квадратных скобках не является функцией, переданной в takeWhile. Это выражение, которое оценивается функцией.

person Chris Martin    schedule 25.01.2015

На самом деле в контракте для takeWhile нет ничего, что определяло бы порядок применения операций.

Вот причина не полагаться на мутацию:

scala> def f(list: collection.GenSeq[Int]) = list.takeWhile {
     |    // state inside the closure?!
     |    var curr = 0
     | 
     |    // actual function application
     |    i => 
     |    val test = i > curr
     |    // update state
     |    curr = i
     |    test
     | }
f: (list: scala.collection.GenSeq[Int])scala.collection.GenSeq[Int]

scala> f(List(1,2,3,1,4,8,9,10).par)
res22: scala.collection.GenSeq[Int] = ParVector()

scala> f(List(1,2,3,1,4,8,9,10).par)
res23: scala.collection.GenSeq[Int] = ParVector(1, 2)

Это одна из тех функций, которые вы можете выразить как складку, но обычно кодируете ее как императивный цикл и называете takeMonotonically.

Но как упражнение:

scala> def f(xs: collection.GenSeq[Int]) = xs.foldLeft(List[Int](),false) {
     | case ((Nil,_),i) => (i::Nil,false)
     | case ((acc,done),i) if !done && acc.head < i => (i::acc,false)
     | case ((acc,_),_) => (acc,true)
     | }
f: (xs: scala.collection.GenSeq[Int])(List[Int], Boolean)

scala> f(List(1,2,3,1,4,8,9,10))
res24: (List[Int], Boolean) = (List(3, 2, 1),true)

scala> f(List(1,2,3,1,4,8,9,10).par)
res25: (List[Int], Boolean) = (List(3, 2, 1),true)

scala> f(List(1,2,3,1,4,8,9,10).par)
res26: (List[Int], Boolean) = (List(3, 2, 1),true)

scala> def g(xs: collection.GenSeq[Int]) = f(xs)._1.reverse
g: (xs: scala.collection.GenSeq[Int])List[Int]

scala> g(List(1,2,3,1,4,8,9,10).par)
res27: List[Int] = List(1, 2, 3)

В качестве дополнительного упражнения:

object Test extends App {
  def takeMonotonically[R](xs: collection.GenTraversableLike[Int,R]) = {
    val it = xs.toIterator
    if (it.isEmpty) Nil
    else {
      var last = it.next
      val b = collection.mutable.ListBuffer[Int]()
      b append last
      var done = false
      while (!done && it.hasNext) {
        val cur = it.next
        done = cur <= last
        if (!done) b append cur
      }
      b.result
    }
  }
  implicit class `gentrav take mono`[R](private val xs: collection.GenTraversableLike[Int,R]) extends AnyVal {
    def takeMonotonically[R] = Test.takeMonotonically(xs)
  }
  Console println takeMonotonically(List(1,2,3,1,4,8,9,10))
  Console println List(1,2,3,1,4,8,9,10).takeMonotonically
  Console println takeMonotonically(List(1,2,3,1,4,8,9,10).par)
  Console println takeMonotonically(List(1,2,3,1,4,8,9,10).par)
}

или подумайте об этом:

scala> List(1,2,3,4,5,6,1,4,8,9,10).par.iterator.sliding(2).takeWhile(vs => vs(0) < vs(1)).toList
res0: List[Seq[Int]] = List(List(1, 2), List(2, 3), List(3, 4), List(4, 5), List(5, 6))

scala> val end = res0.last(1)
end: Int = 6

scala> (res0 map (_(0))) :+ end
res1: List[Int] = List(1, 2, 3, 4, 5, 6)
person som-snytt    schedule 25.01.2015
comment
Это определенно заставляет задаться вопросом, почему кто-то захочет когда-либо взять GenSeq вместо Seq, поскольку идея Seq состоит в том, чтобы быть в порядке, а идея Gen не обязательно должна быть. Просто возьми Seq и заставь любого, у кого есть GenSeq, сделать .seq на входе, не так ли? - person Rex Kerr; 26.01.2015
comment
@RexKerr Никто не говорит, что порядок операций для takeWhile определяется для любой коллекции, хотя предполагается, что для последовательного seq. Но я мог бы представить отсортированную последовательность, в которой это не выполняется, потому что предикат проверяется на внутренних узлах при спуске по дереву. - person som-snytt; 26.01.2015
comment
Что-то может быть не обещано, но все же нарушить принцип наименьшего удивления, который, я бы сказал, здесь имеет место. Например, API обещает, что reverseIterator выполняет итерацию в обратном порядке, и что reverse.iterator совпадает с reverseIterator, но на самом деле нигде не обещает, что голый .iterator находится в том же порядке, что и apply(i); i += 1. Это не означает, что было бы интересно работать с Seq, где вы должны отменить его хотя бы один раз, чтобы apply(0) == .iterator.next было истинным (в непустой коллекции). Однако, по крайней мере, с takeWhile следует лишь немного удивляться. - person Rex Kerr; 26.01.2015