Хотя или хвостовой рекурсии в F#, что использовать, когда?

Хорошо, только в F#, и вот как я понимаю это сейчас:

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

  • Некоторые языки являются чисто функциональными, поэтому вы должны использовать рекурсию вместо циклов while, даже если проблема не является рекурсивной по своей природе.

Итак, мой вопрос: поскольку F# также поддерживает императивную парадигму, вы бы использовали хвостовую рекурсию в F# для задач, которые не являются естественно рекурсивными? Тем более, что я прочитал, что компилятор распознает хвостовую рекурсию и в любом случае просто преобразует ее в цикл while?

Если так: почему?

5 ответов

Решение

Лучший ответ - "нет".:)

Есть некоторое уродство, связанное как с циклом while, так и с хвостовой рекурсией.

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

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

Лучший ответ - не использовать ни циклы while, ни рекурсию. Функции высшего порядка и лямбды - ваши спасители, особенно карты и сгибы. Зачем дурачиться с беспорядочными структурами управления для циклов, когда вы можете инкапсулировать их в многократно используемые библиотеки, а затем просто и декларативно изложить суть ваших вычислений?

Если вы привыкли часто вызывать карту / сложение, а не использовать циклы / рекурсию, а также предоставлять функцию сгиба вместе с любым новым типом данных, представленных в древовидной структуре, вы далеко пойдете.:)

Для тех, кто хочет узнать больше о складках в F#, почему бы не проверить мои первые три сообщения в блоге в серии на эту тему?

В порядке предпочтений и общего стиля программирования я напишу код следующим образом:

Карта / сгиб, если она доступна

let x = [1 .. 10] |> List.map ((*) 2)

Это просто удобно и просто в использовании.

Нехвостая рекурсивная функция

> let rec map f = function
    | x::xs -> f x::map f xs
    | [] -> [];;

val map : ('a -> 'b) -> 'a list -> 'b list

> [1 .. 10] |> map ((*) 2);;
val it : int list = [2; 4; 6; 8; 10; 12; 14; 16; 18; 20]

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

Помните, log2(1 000 000 000 000 000) ~= 50, поэтому операция log(n) без хвостовой рекурсии совсем не страшна.

Хвост-рекурсивный с аккумулятором

> let rev l =
    let rec loop acc = function
        | [] -> acc
        | x::xs -> loop (x::acc) xs
    loop [] l

let map f l =
    let rec loop acc = function
        | [] -> rev acc
        | x::xs -> loop (f x::acc) xs
    loop [] l;;

val rev : 'a list -> 'a list
val map : ('a -> 'b) -> 'a list -> 'b list

> [1 .. 10] |> map ((*) 2);;
val it : int list = [2; 4; 6; 8; 10; 12; 14; 16; 18; 20]

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

Хвост-рекурсивный с продолжением прохождения

> let rec map cont f = function
    | [] -> cont []
    | x::xs -> map (fun l -> cont <| f x::l) f xs;;

val map : ('a list -> 'b) -> ('c -> 'a) -> 'c list -> 'b

> [1 .. 10] |> map id ((*) 2);;
val it : int list = [2; 4; 6; 8; 10; 12; 14; 16; 18; 20]

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

Возможно, это моя монадофобия, говорящая здесь, или, может быть, мое внутреннее отсутствие знакомства с вызовом /cc Лиспа, но я думаю, что случаи, когда CSP на самом деле упрощает алгоритмы, немногочисленны. Контрпримеры приветствуются в комментариях.

Пока петли / для петель

Мне приходит в голову, что, кроме понимания последовательности, я никогда не использовал циклы while или for в своем коде F#. В любом случае...

> let map f l =
    let l' = ref l
    let acc = ref []
    while not <| List.isEmpty !l' do
        acc := (!l' |> List.hd |> f)::!acc
        l' := !l' |> List.tl
    !acc |> List.rev;;

val map : ('a -> 'b) -> 'a list -> 'b list

> [1 .. 10] |> map ((*) 2);;
val it : int list = [2; 4; 6; 8; 10; 12; 14; 16; 18; 20]

Это практически пародия на императивное программирование. Вы могли бы быть в состоянии сохранить немного здравомыслия, объявив let mutable l' = l вместо этого, но любая нетривиальная функция потребует использования ref,

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

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

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

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

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

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

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

Функциональная техника означает не только рекурсию, но и использование соответствующих функций высшего порядка.

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

для задач, которые не являются рекурсивными по природе... просто преобразуйте их в цикл while

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

Другие вопросы по тегам