Хвостовая рекурсия против. Рефакторинг

у меня есть этот метод

  private def getAddresses(data: List[Int], count: Int, len: Int): Tuple2[List[Address], List[Int]] = {
    if (count == len) {
      (List.empty, List.empty)
    } else {
      val byteAddress = data.takeWhile(_ != 0)
      val newData = data.dropWhile(_ != 0).tail
      val newCount = count + 1
      val newPosition = byteAddress.length + 1
      val destFlag = byteAddress.head
      if (destFlag == SMEAddressFlag) {
        (SMEAddress().fromBytes(byteAddress) :: getAddresses(newData, newCount, len)._1, newPosition :: getAddresses(newData, newCount, len)._2)
      } else {
        (DistributionList().fromBytes(byteAddress) :: getAddresses(newData, newCount, len)._1, newPosition :: getAddresses(newData, newCount, len)._2)
      }
    }
  }

Я испытываю искушение переписать это таким образом

  private def getAddresses(data: List[Int], count: Int, len: Int): Tuple2[List[Address], List[Int]] = {
    if (count == len) {
      (List.empty, List.empty)
    } else {
      val byteAddress = data.takeWhile(_ != 0)
      val newData = data.dropWhile(_ != 0).tail
      val newCount = count + 1
      val newPosition = byteAddress.length + 1
      val destFlag = byteAddress.head
      val nextIter = getAddresses(newData, newCount, len)
      if (destFlag == SMEAddressFlag) {
        (SMEAddress().fromBytes(byteAddress) :: nextIter._1, newPosition :: nextIter._2)
      } else {
        (DistributionList().fromBytes(byteAddress) :: nextIter._1, newPosition :: nextIter._2)
      }
    }
  }

Мои вопросы

  1. Является ли первый хвост рекурсивным?
  2. Как это работает? Я вызываю метод дважды в последней строке. Оценивает ли он отдельные вызовы методов?
  3. Какая версия более эффективна, или как я могу написать ее более эффективно.

Прошу прощения, если код пахнет. Я новичок в scala.

Спасибо


person Akintayo Olusegun    schedule 15.03.2011    source источник
comment
Добавление аннотации @tailrec к вашей функции заставит компилятор сказать вам, не является ли она хвостовой рекурсией.   -  person Nicolas Payette    schedule 17.03.2011


Ответы (4)


Ни один из них не является хвостовой рекурсией. Это происходит только тогда, когда рекурсивные вызовы возникают только как самый последний элемент на некотором пути выполнения. Если бы вы могли заменить вызов переходом к началу кода, не сохраняя состояния, но переименовывая входные переменные, то это хвостовая рекурсия. (Внутренне это именно то, что делает компилятор.)

Чтобы преобразовать обычную рекурсивную функцию в хвостовую рекурсивную функцию, когда это возможно, вам необходимо передать вперед любые сохраненные данные, например:

private def getAddresses(
  data: List[Int], count: Int, len: Int,    // You had this already
  addresses: List[Address] = Nil,           // Build this as we go, starting with nothing
  positions: List[Int] = Nil                // Same here
): (List[Address], List[Int]) {
  if (count==len) {
    (addresses.reverse, positions.reverse)  // Return what we've built, reverse to fix order
  }    
  else {
    val (addr,rest) = data.span(_ != 0)
    val newdata = rest.tail
    val position = addr.length + 1
    val flag = addr.head
    val address = (
      if (flag) SMEAddress().fromBytes(addr)
      else DistributionList().fromBytes(addr)
    )
    getAddresses(newdata, count+1, len, address :: addresses, position :: positions)
  }
}

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

Вызов метода дважды всегда запускает его дважды. Нет автоматического запоминания результатов вызовов методов — это было бы крайне сложно сделать автоматически.

person Rex Kerr    schedule 15.03.2011
comment
Я до сих пор не совсем понимаю это, но много граций и много благодарностей в делах - person Akintayo Olusegun; 15.03.2011
comment
Другой вопрос. Просто чтобы прояснить эту хвостовую рекурсию, является ли эта функция хвостовой рекурсией? def fact(n: Int): Int = { if(n <= 1) 1 else n * fact(n - 1) }. Если нет, чтобы сделать хвост рекурсивным, мне нужно сделать что-то вроде этого def fact(n: Int, acc: Int = 1): Int = { if(n <= 1) acc else { val thisAcc = acc * n; fact(n -1, thisAcc) } } - person Akintayo Olusegun; 15.03.2011
comment
@Aardvocate Akintayo Olusegun - это правильно - ваш исходный факториал не хвостовой рекурсии, а ваша модифицированная версия. - person Rex Kerr; 15.03.2011

Просто чтобы сделать его более «Scala», вы можете определить хвостовую рекурсивную функцию внутри getAddresses вот так

def getAddresses(data: List[Int], len: Int) = {
  def inner(count: Int, addresses: List[Address] = Nil, positions: List[Int] = Nil): (List[Address], List[Int]) = {
    if (count == len) {
     (addresses.reverse, positions.reverse)
    } else {
      val (byteAddress, rest) = data.span(_ != 0)
      val newData = rest.tail
      val newPosition = byteAddress.length + 1
      val destFlag = byteAddress.head
      val newAddress = (if (destFlag == SMEAddressFlag) SMEAddress else DistributionList()).fromBytes(byteAddress)
      inner(count+1, newAddress :: addresses, newPosition :: positions)
    }
  }

  inner(0)   //Could have made count have a default too
}
person GKelly    schedule 15.03.2011
comment
Правильный термин — «Скаларифический»: ="заполнение списка - это scala со случайным двойным взятием навсегда"> stackoverflow.com/questions/5304027/. «Скаламатик» также принимается. :) - person Aaron Novstrup; 15.03.2011

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

private def getAddresses(data:List[Int], count:Int, len:Int):Stream[(Address,Int)] = {
   if (count == len) {
      Stream.empty
   }else{
      val (byteAddress, _::newData) = data.span(_ != 0)
      val newAddress =
         if (byteAddress.head == SMEAddressFlag) SMEAddress().fromBytes(byteAddress)
         else DistributionList().fromBytes(byteAddress)

      (newAddress, byteAddress.length + 1) #:: getAddresses(newData, count+1, len)
}
  1. Вместо того, чтобы возвращать пару списков, он возвращает список пар. Это упрощает рекурсию один раз. Если вам нужны отдельные списки, вы можете использовать map для их извлечения, но вы можете изменить структуру других частей вашей программы, чтобы более четко работать со списком пар, а не использовать 2 списка в качестве параметра.

  2. Вместо того, чтобы возвращать список, он возвращает поток, который оценивается лениво. Это не хвостовая рекурсия, но способ ленивой оценки потоков также предотвращает переполнение стека. Если вам нужен строгий список, вы можете вызвать toList для результата этой функции.

  3. Демонстрируются другие полезные методы, например использование span с сопоставлением с образцом для вычисления byteAddress и newData в одной строке кода. Вы можете добавить обратно некоторые из val, которые я удалил, если полезно иметь их имена для удобочитаемости.

person Ken Bloom    schedule 15.03.2011
comment
Забавно, что мой босс позвонил мне и дал тот же совет использовать список кортежей, а не наоборот. Ваш код выглядит аккуратно, и я никогда не думал об использовании Stream до сих пор. Большое спасибо - person Akintayo Olusegun; 17.03.2011

  1. Нет. Метод является хвостовой рекурсией, если последний вызов в данном пути выполнения является рекурсивным вызовом. Последним оцененным здесь вызовом будет ::, который не является рекурсивным вызовом, поэтому он не является хвостовым рекурсивным.
  2. Да, если вы вызовете метод дважды, он будет оценен дважды.
  3. Второй более эффективен, так как здесь вы вызываете метод только один раз.
person sepp2k    schedule 15.03.2011
comment
1. ... если последний вызов ... является единственным рекурсивным вызовом на этом пути. - person ziggystar; 15.03.2011
comment
@ziggystar: Если есть несколько рекурсивных вызовов, и последний из них является хвостовым вызовом, этот вызов все равно будет оптимизирован TCO, не так ли? - person sepp2k; 15.03.2011
comment
Я думаю, что большинство людей интересует возможность превращения рекурсивных вызовов функций в цикл. Но, конечно, ты прав. - person ziggystar; 16.03.2011