ХОРОШО!
Приведенный ниже код написан с использованием синтаксиса 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, о которой я могу думать, не поддерживает правильное устранение хвостовых вызовов - 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
Это заставит многих говорить "но это не функционально!" - Я знаю, просто расслабься. Мы можем реализовать Clojure-стиль loop
/ recur
интерфейс для зацикливания в постоянном пространстве, используя чистые выражения ; ничего такого 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