Хвост-рекурсивный ограниченный поток пар целых чисел (Scala)?

Я очень новичок в Scala, так простите мое невежество! Я пытаюсь выполнить итерацию пар целых чисел, которые ограничены максимумом. Например, если максимум равен 5, то итерация должна возвратиться:

(0, 0), (0, 1), ..., (0, 5), (1, 0), ..., (5, 5)

Я решил попробовать и рекурсивно возвращать это как поток:

    @tailrec
    def _pairs(i: Int, j: Int, maximum: Int): Stream[(Int, Int)] = {
        if (i == maximum && j == maximum) Stream.empty
        else if (j == maximum) (i, j) #:: _pairs(i + 1, 0, maximum)
        else (i, j) #:: _pairs(i, j + 1, maximum)
    }

Без аннотации tailrec код работает:

scala> _pairs(0, 0, 5).take(11)
res16: scala.collection.immutable.Stream[(Int, Int)] = Stream((0,0), ?)

scala> _pairs(0, 0, 5).take(11).toList
res17: List[(Int, Int)] = List((0,0), (0,1), (0,2), (0,3), (0,4), (0,5), (1,0), (1,1), (1,2), (1,3), (1,4))

Но для меня это недостаточно. Компилятор правильно указывает, что последняя строка _pairs не возвращает _pairs:

could not optimize @tailrec annotated method _pairs: it contains a recursive call not in tail position
    else (i, j) #:: _pairs(i, j + 1, maximum)
                ^

Итак, у меня есть несколько вопросов:

  • непосредственно обращаясь к реализации выше, как один хвост-рекурсивно возвращает Stream [(Int, Int)]?
  • сделать шаг назад, что является наиболее эффективным для памяти способом итерации по ограниченным последовательностям целых чисел? Я не хочу перебирать Range, потому что Range расширяет IndexedSeq, и я не хочу, чтобы последовательность существовала полностью в памяти. Или я ошибаюсь? Если я перебираю Range.view, я не могу попасть в память?

В Python (!) все, что я хочу, это:

In [6]: def _pairs(maximum):
   ...:     for i in xrange(maximum+1):
   ...:         for j in xrange(maximum+1):
   ...:             yield (i, j)
   ...:             

In [7]: p = _pairs(5)

In [8]: [p.next() for i in xrange(11)]
Out[8]: 
[(0, 0),
 (0, 1),
 (0, 2),
 (0, 3),
 (0, 4),
 (0, 5),
 (1, 0),
 (1, 1),
 (1, 2),
 (1, 3),
 (1, 4)]

Спасибо за вашу помощь! Если вы считаете, что мне нужно читать ссылки/документы API/все остальное, скажите мне, потому что я очень хочу учиться.

Ответы

Ответ 1

Это не хвостовая рекурсия

Предположим, что вы делали список вместо потока: (позвольте мне использовать более простую функцию, чтобы сделать мою точку)

def foo(n: Int): List[Int] =
  if (n == 0)
    0 :: Nil
  else
    n :: foo(n - 1)

В общем случае в этой рекурсии после foo(n - 1) возвращается функция должна что-то сделать с возвращаемым списком - она ​​должна конкатенировать другой элемент в начале списка. Таким образом, функция не может быть хвостичной рекурсивной, потому что что-то должно быть сделано в списке после рекурсии.

Без хвостовой рекурсии для некоторого большого значения n у вас закончится пространство стека.

Обычное решение списка

Обычным решением было бы передать ListBuffer в качестве второго параметра и заполнить его.

def foo(n: Int) = {
  def fooInternal(n: Int, list: ListBuffer[Int]) = {
    if (n == 0) 
      list.toList
    else {
      list += n
      fooInternal(n - 1, list)
    }
  }
  fooInternal(n, new ListBuffer[Int]())
}

То, что вы делаете, называется " хвостовая рекурсия по модулю минус", и это оптимизация, выполняемая автоматически с помощью LISP Пролог-компиляторы, когда они видят хвостовую рекурсию по модулю против шаблона, так как это так распространено. Scala компилятор не оптимизирует это автоматически.

Потокам не нужна рекурсия хвоста

Потоки не нуждаются в хвостовой рекурсии, чтобы избежать нехватки стека - это потому, что они используют умный трюк, чтобы не выполнять рекурсивный вызов foo в точке, где он отображается в коде. Вызов функции завершается в thunk и только вызывается в точке, в которой вы фактически пытаетесь получить значение из потока. Только один вызов foo активен одновременно - он никогда не рекурсивный.

Я написал предыдущий ответ, объясняющий как работает оператор #:: здесь, в Stackoverflow. Здесь, что происходит, когда вы вызываете следующую рекурсивную функцию потока. (Он рекурсивный в математическом смысле, но он не вызывает вызов функции из вызова функции так, как вы обычно ожидаете.)

def foo(n: Int): Stream[Int] =
  if (n == 0)
    0 #:: Nil
  else
    n #:: foo(n - 1)

Вы вызываете foo(10), он возвращает поток с одним уже вычисленным элементом, а хвост - это thunk, который вызовет foo(9) в следующий раз, когда вам понадобится элемент из потока. foo(9) теперь не вызывается - скорее, вызов привязан к lazy val внутри потока, а foo(10) возвращается немедленно. Когда вам наконец понадобится второе значение в потоке, вызывается foo(9), и он вычисляет один элемент и устанавливает хвост потока hte как thunk, который вызывается foo(8). foo(9) немедленно возвращается без вызова foo(8). И так далее...

Это позволяет создавать бесконечные потоки без исчерпания памяти, например:

def countUp(start: Int): Stream[Int] = start #::countUp(start + 1)

(Будьте осторожны, какие операции вы вызываете в этом потоке. Если вы попытаетесь сделать forEach или map, вы заполните всю свою кучу, но использование take - это хороший способ работать с произвольный префикс потока.)

Более простое решение

Вместо того, чтобы иметь дело с рекурсией и потоками, почему бы просто не использовать цикл Scala for?

def pairs(maximum:Int) =
  for (i <- 0 to maximum;
       j <- 0 to maximum)
    yield (i, j)

Это материализует всю коллекцию в памяти и возвращает IndexedSeq[(Int, Int)].

Если вам нужен Stream, вы можете преобразовать первый диапазон в Stream.

def pairs(maximum:Int) =
  for (i <- 0 to maximum toStream;
       j <- 0 to maximum)
    yield (i, j)

Это вернет a Stream[(Int, Int)]. Когда вы получаете доступ к определенной точке в последовательности, она будет материализована в память, и она будет придерживаться до тех пор, пока вы все еще будете ссылаться на любую точку в потоке перед этим элементом.

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

def pairs(maximum:Int) =
  for (i <- 0 to maximum view;
       j <- 0 to maximum view)
    yield (i, j)

Это возвращает SeqView[(Int, Int),Seq[_]], который вычисляет каждый элемент каждый раз, когда вам это нужно, и не сохраняет предварительно вычисленные результаты.

Вы также можете получить итератор (который вы можете перемещать только один раз) таким же образом

def pairs(maximum:Int) =
  for (i <- 0 to maximum iterator;
       j <- 0 to maximum iterator)
    yield (i, j)

Возвращает Iterator[(Int, Int)].

Ответ 2

Может быть, Итератор лучше подходит вам?

class PairIterator (max: Int) extends Iterator [(Int, Int)] {
  var count = -1
  def hasNext = count <= max * max 
  def next () = { count += 1; (count / max, count % max) }
}

val pi = new PairIterator (5)
pi.take (7).toList