Есть ли преимущество в том, чтобы избегать циклов в Scala?

Чтение Scala документов, написанных экспертами, может создать впечатление, что хвостовая рекурсия лучше, чем цикл while, даже если последний более краток и ясен. Это один из примеров

object Helpers {
    implicit class IntWithTimes(val pip:Int) {
            // Recursive
        def times(f: => Unit):Unit = {
            @tailrec
            def loop(counter:Int):Unit = {
                if (counter >0) { f; loop(counter-1) }
            }
            loop(pip)
        }

            // Explicit loop
        def :@(f: => Unit) = {
            var lc = pip
            while (lc > 0) { f; lc -= 1 }
        }   
    }
}   

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

Единственный аспект цикла while, который может быть лучше, - это переменная итерации должна быть локальной для тела цикла, а мутация переменной должна быть в фиксированном месте, но Scala выбирает не предоставлять синтаксис.

Ясность субъективна, но вопрос в том, что рекурсивный стиль (хвост) предлагает улучшенную производительность?

Ответ 1

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

Длинный ответ на ваш более общий вопрос (имеющий преимущество) немного более надуман. Обратите внимание, что, используя while, вы фактически:

  • создание новой переменной, содержащей счетчик.
  • изменяя эту переменную.

Ошибки по очереди и опасности изменчивости гарантируют, что в конечном итоге вы введете ошибки с шаблоном while. Фактически, ваша функция times может быть легко реализована как:

def times(f: => Unit) = (1 to pip) foreach f

Это не только проще и меньше, но и позволяет избежать создания временных переменных и изменчивости. На самом деле, если тип функции, которую вы вызываете, будет чем-то, что имеет значение, то конструкцию while стало бы еще труднее прочитать. Попробуйте выполнить следующее, используя только whiles:

def replicate(l: List[Int])(times: Int) = l.flatMap(x => List.fill(times)(x))

Затем перейдите к определению хвостовой рекурсивной функции, которая делает то же самое.


ОБНОВЛЕНИЕ:

Я слышу, как вы говорите: "Эй, этот обман! foreach не является ни вызовом while, ни tail-rec". Да неужели? Взгляните на Scala определение foreach для Lists:

  def foreach[B](f: A => B) {
    var these = this
    while (!these.isEmpty) {
      f(these.head)
      these = these.tail
    }
  }

Если вы хотите узнать больше о рекурсии в Scala, посмотрите этот пост в блоге. После того, как вы включите функциональное программирование, сходите с ума и прочитайте сообщение в блоге Rúnar . Еще больше здесь и здесь.

Ответ 2

Разве эти эксперты говорили, что это была причина? Я уверен, что их причины больше связаны с выразительным кодом и функциональным программированием. Не могли бы вы привести примеры их аргументов?

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

Причина, по которой в то время как решения на основе решений могут быть менее эффективными, они могут стать очень уродливыми, поскольку сложность проблемы возрастает...

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

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

Рекурсивные функции обеспечивают возможность более чистой реализации. Хорошее рекурсивное решение разбивает сложную проблему на более простые части, а затем делегирует каждую часть другой функции, которая может справиться с ней - трюк заключается в том, что эта другая функция сама по себе (или, возможно, взаимно-рекурсивная функция, хотя это редко встречается в Scala - в отличие от различных диалектов Lisp, где это обычно - из-за плохой поддержки хвостовой рекурсии). Рекурсивно называемая функция получает в своих параметрах только более простое подмножество данных и только соответствующее состояние; он возвращает только решение более простой проблемы. Итак, в отличие от цикла while,

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

Учитывая эти преимущества, рекурсия может облегчить достижение эффективного решения. Особенно, если вы считаете ремонтопригодность важным фактором долгосрочной эффективности.

Я собираюсь найти хорошие примеры кода для добавления. Между тем, на этом этапе я всегда рекомендую The Little Schemer. Я бы продолжил, почему, но это второй вопрос рекурсии Scala на этом сайте через два дня, поэтому посмотрите на мой предыдущий ответ.

Ответ 3

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

Как правило, любая рекурсивная функция хвоста может быть переписана (обычно автоматически компилятором) в виде цикла while и наоборот.

Цель записи функций в (хвостовом) рекурсивном стиле заключается не в максимизации производительности или даже в краткости, а в том, чтобы сделать цель кода максимально ясной, одновременно минимизируя возможность введения ошибок (путем исключения изменяемых переменных, что обычно затрудняет отслеживание того, что такое "входы" и "выходы" функции). Правильно написанная рекурсивная функция состоит из серии проверок условий завершения (с использованием либо каскадного if - else, либо совпадения шаблона) с рекурсивным вызовом (с множественным числом, только если не рекурсивным), если ни одна из выполняются условия завершения.

Преимущество использования рекурсии наиболее драматично, когда существует несколько различных возможных условий завершения. Ряд условных выражений или шаблонов if, как правило, гораздо легче понять, чем одно условие while с целым куском (потенциально сложных и взаимосвязанных) булевых выражений && 'd вместе, особенно если требуется обратное значение быть разными, в зависимости от того, какое условие завершения выполнено.