Muki
Muki

Reputation: 3641

Scala - State in closure / anonymous function

I'm trying to take elements from a list while a specific predicate is satisfied. The predicate however depends on the last element. This is some code illustrating the problem and my solution

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
}

Result as expected is

List(1,2,3)

However I'm not sure if this works by accident or if this is intentional by scala. Is there a better way of doing this?

thanks, Muki

Upvotes: 0

Views: 300

Answers (2)

Chris Martin
Chris Martin

Reputation: 30736

curr is not "inside the closure". The concept is called "closure" because the anonymous function

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

is an open expression (it has a free variable curr) which is closed by binding curr to the var that is declared outside of the function.

The bracketed block is not the function passed to takeWhile – It is an expression that evaluates to the function.

Upvotes: 2

som-snytt
som-snytt

Reputation: 39577

There's not actually anything in the contract for takeWhile that specifies the order in which operations are applied.

Here's a reason not to rely on mutation:

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)

This is one those functions you could express as a fold, but you'd usually code it as an imperative loop and name it takeMonotonically.

But as an exercise:

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)

As a further exercise:

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)
}

or come to think of it:

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)

Upvotes: 3

Related Questions