OK!
Приведенный ниже код написан с использованием синтаксиса ES6, но с таким же успехом его можно было бы написать на ES5 или даже меньше. ES6 не является требованием для создания "механизма для повторения цикла x раз"
Если вам не нужен итератор в обратном вызове, это самая простая реализация
const times = x => f => {
if (x > 0) {
f()
times (x - 1) (f)
}
}
// use it
times (3) (() => console.log('hi'))
// or define intermediate functions for reuse
let twice = times (2)
// twice the power !
twice (() => console.log('double vision'))
Если вам нужен итератор, вы можете использовать именованную внутреннюю функцию с параметром-счетчиком для выполнения итерации за вас.
const times = n => f => {
let iter = i => {
if (i === n) return
f (i)
iter (i + 1)
}
return iter (0)
}
times (3) (i => console.log(i, 'hi'))
Прекратите читать здесь, если вам не нравится узнавать больше...
Но что-то в них должно быть не так...
- операторы одной ветки
if
уродливы что происходит в другой ветке ?
- несколько операторов/выражений в телах функций перемешаны ли процедуры?
- неявно возвращаемое
undefined
указание на нечистую, побочную функцию
"Разве нет лучшего способа?"
Есть. Давайте сначала вернемся к нашей первоначальной реализации
// times :: Int -> (void -> void) -> void
const times = x => f => {
if (x > 0) {
f() // has to be side-effecting function
times (x - 1) (f)
}
}
Конечно, это просто, но обратите внимание, что мы просто вызываем f()
и ничего с ним не делаем. Это действительно ограничивает тип функции, которую мы можем повторять несколько раз. Даже если у нас есть доступный итератор, f(i)
не намного более универсален.
Что, если мы начнем с более качественной процедуры повторения функций? Может быть, что-то, что лучше использует ввод и вывод.
Повторение общей функции
// repeat :: forall a. Int -> (a -> a) -> a -> a
const repeat = n => f => x => {
if (n > 0)
return repeat (n - 1) (f) (f (x))
else
return x
}
// power :: Int -> Int -> Int
const power = base => exp => {
// repeat <exp> times, <base> * <x>, starting with 1
return repeat (exp) (x => base * x) (1)
}
console.log(power (2) (8))
// => 256
Выше мы определили общую функцию repeat
, которая принимает дополнительные входные данные, которые используются для запуска повторного применения одной функции.
// repeat 3 times, the function f, starting with x ...
var result = repeat (3) (f) (x)
// is the same as ...
var result = f(f(f(x)))
Реализация times
с repeat
Что ж, теперь это легко; почти все работы уже сделаны.
// repeat :: forall a. Int -> (a -> a) -> a -> a
const repeat = n => f => x => {
if (n > 0)
return repeat (n - 1) (f) (f (x))
else
return x
}
// times :: Int -> (Int -> Int) -> Int
const times = n=> f=>
repeat (n) (i => (f(i), i + 1)) (0)
// use it
times (3) (i => console.log(i, 'hi'))
Поскольку наша функция принимает i
в качестве входных данных и возвращает i + 1
, это эффективно работает как наш итератор, который мы каждый раз передаем f
.
Мы также исправили наш маркированный список проблем
- Больше никаких уродливых одноветвевых операторов
if
- Тела с одним выражением указывают на четко разделенные задачи.
- Больше никаких бесполезных, неявно возвращаемых
undefined
Оператор запятой JavaScript
Если у вас возникли проблемы с пониманием того, как работает последний пример, это зависит от вашей осведомленности об одном из старейших боевых топоров JavaScript; оператор запятой — короче говоря, он оценивает выражения слева направо и возвращает значение последнего вычисленного выражения
(expr1 :: a, expr2 :: b, expr3 :: c) :: c
В нашем примере выше я использую
(i => (f(i), i + 1))
это просто краткий способ написания
(i => { f(i); return i + 1 })
Оптимизация хвостового вызова
Какими бы привлекательными ни были рекурсивные реализации, на данный момент с моей стороны было бы безответственно рекомендовать их, учитывая отсутствие JavaScript VM Я могу думать о поддержке надлежащего исключения хвостовых вызовов — Babel использовал для его транспиляции, но он находился в статусе «сломан; будет перереализован» более года.
repeat (1e6) (someFunc) (x)
// => RangeError: Maximum call stack size exceeded
Таким образом, мы должны пересмотреть нашу реализацию repeat
, чтобы сделать ее безопасной для стека.
В приведенном ниже коде действительно используются изменяемые переменные n
и x
, но обратите внимание, что все мутации локализованы в функции repeat
— никакие изменения состояния (мутации) не видны снаружи функции.
// repeat :: Int -> (a -> a) -> (a -> a)
const repeat = n => f => x =>
{
let m = 0, acc = x
while (m < n)
(m = m + 1, acc = f (acc))
return acc
}
// inc :: Int -> Int
const inc = x =>
x + 1
console.log (repeat (1e8) (inc) (0))
// 100000000
Это заставит многих из вас сказать: «Но это не работает!» - Я знаю, просто расслабься. Мы можем реализовать интерфейс loop
/recur
в стиле Clojure для циклов с постоянным пространством, используя чистые выражения; ничего из этого while
материала.
Здесь мы абстрагируем while
с помощью нашей функции loop
— она ищет специальный тип recur
, чтобы поддерживать выполнение цикла. Когда встречается тип, отличный от recur
, цикл завершается и возвращается результат вычисления.
const recur = (...args) =>
({ type: recur, args })
const loop = f =>
{
let acc = f ()
while (acc.type === recur)
acc = f (...acc.args)
return acc
}
const repeat = $n => f => x =>
loop ((n = $n, acc = x) =>
n === 0
? acc
: recur (n - 1, f (acc)))
const inc = x =>
x + 1
const fibonacci = $n =>
loop ((n = $n, a = 0, b = 1) =>
n === 0
? a
: recur (n - 1, b, a + b))
console.log (repeat (1e7) (inc) (0)) // 10000000
console.log (fibonacci (100)) // 354224848179262000000
person
Mulan
schedule
26.05.2015