7.4 KiB
title | localeTitle |
---|---|
Clojure Looprecur | Clojure Looprecur |
Возможно, вам придется понять, if
и let
полностью понять рекурсию в Clojure.
for
и while
Clojure не имеет циклов или циклов. Это имеет смысл, если вы думаете об этом. Цикл for
изменяет переменную, и это не допускается в Clojure.
for (var i = 0; i < 10; i++) {
console.log(i);
}
i++
означает, что мы добавляем его к переменной i
каждый раз, когда цикл завершается - ясный пример изменяемой переменной.
while
циклы менее явно зависят от изменения переменных, но они, как и для циклов.
var i = 0;
while (i < 10) {
console.log(i);
i++;
}
while
циклов всегда есть условие, такое как i < 10
, и будет ломаться, если это условие перестает быть истинным. Это означает, что у них должен быть какой-то побочный эффект (например, добавление 1 к i
), чтобы условие в конечном итоге было ложным; в противном случае цикл будет длиться вечно.
Рекурсия
К счастью, у Clojure есть одна петля. Эти циклы используют рекурсию - функцию, которая вызывает себя. Простейшим рекурсивным алгоритмом является поиск положительного числа factorial (5 факториалов, например, 5 * 4 * 3 * 2
).
(defn fact [x]
(loop [nx prod 1] ;; this works just like a 'let' binding.
(if (= 1 n) ;; this is the base case.
prod
(recur (dec n) (* prod n)))))
Вы заметите, что (loop [nx prod 1] ...)
выглядит очень похоже на привязку let
. Фактически это работает точно так же - здесь мы привязываем n
к x
и prod
1.
Каждая рекурсивная функция имеет «базовый регистр». Это условие, которое делает цикл остановки цикла. В этом случае наш цикл останавливается, если n = 1
, и возвращает prod
. Если n
не равно 1, то цикл повторяется.
(recur (dec n) (* prod n))
Эта recur
функция перезапускает цикл, но с разными привязками. На этот раз n
не привязано к x
, а привязано к (dec n)
(что означает decrement n
или n - 1
), а prod
привязан к (* prod n)
.
Поэтому, когда мы вызываем функцию, это происходит:
(fact 5)
; Loop 1: 5 != 1, so the loop recurs with 4 (5 - 1) and 5 (1 * 5).
; Loop 2: 4 != 1, so the loop recurs with 3 (4 - 1) and 20 (5 * 4).
; Loop 3: 3 != 1, so the loop recurs with 2 (3 - 1) and 60 (20 * 3).
; Loop 4: 2 != 1, so the loop recurs with 1 (2 - 1) and 120 (60 * 2).
; Loop 5: 1 == 1, so the function returns prod, which is now equal to 120.
; => 120
Гениальная вещь о рекурсии состоит в том, что сами переменные никогда не меняются. Единственное, что меняется, это то, о чем говорят n
и prod
. Мы никогда не говорим, что n--
, или n += 2
.
Зачем использовать loop / recur?
Вам может быть интересно, почему вы будете использовать loop/recur
а не просто определять функцию, которая вызывает себя. Наша факториальная функция могла быть написана так:
(defn fact-no-loop [n]
(if (= 1 n)
1
(* n (fact-no-loop (dec n)))))
Это более красноречиво и работает аналогичным образом. Почему бы вы когда - либо использовать цикл и повторялись?
Оптимизация звонков
Если вы используете loop/recur
, то компилятор (программное обеспечение, которое превращает Clojure-код в JTM-байт-код) знает, что вы хотите создать рекурсивный цикл. Это означает, что он пытается изо всех сил оптимизировать ваш код для рекурсии. Давайте сравним скорость fact
и fact-no-loop
:
(time (fact 20))
; => "Elapsed time: 0.083927 msecs"
; 2432902008176640000
(time (fact-no-loop 20))
; => "Elapsed time: 0.064937 msecs"
; 2432902008176640000
В этом масштабе разница незначительна. Фактически, fact-no-loop
иногда быстрее, чем fact
из-за непредсказуемого характера компьютерной памяти. Однако в более широком масштабе такая оптимизация может сделать ваш код намного, намного быстрее.
Реестр вложенности внутри функций
fact-no-loop
работает без loop/recur
потому что вся функция рекурсивна. Что, если мы хотим, чтобы часть нашей функции использовала рекурсивный цикл, а затем остальную часть, чтобы сделать что-то нерекурсивное? Нам нужно было бы определить две совершенно отдельные функции. Использование loop/recur
позволяет использовать небольшую анонимную функцию.
| Предыдущая | Главная | следующий |
| Пусть привязки | Содержание | Чтобы добавить |