Как заменить циклы while функциональной альтернативой программирования без оптимизации хвостового вызова?
Я экспериментирую с более функциональным стилем в моем JavaScript; поэтому я заменил для циклов с помощью служебных функций, таких как отображение и уменьшение. Тем не менее, я не нашел функциональной замены для циклов while, поскольку оптимизация хвостового вызова обычно недоступна для JavaScript. (Из того, что я понимаю, ES6 предотвращает переполнение стековых вызовов, но не оптимизирует их производительность.)
Ниже я объясню, что я попробовал, но TLDR: если у меня нет оптимизации хвостового вызова, каков функциональный способ реализации циклов while?
Что я пробовал:
Создание функции полезности while:
function while(func, test, data) {
const newData = func(data);
if(test(newData)) {
return newData;
} else {
return while(func, test, newData);
}
}
Поскольку оптимизация хвостового вызова недоступна, я мог бы переписать это как:
function while(func, test, data) {
let newData = *copy the data somehow*
while(test(newData)) {
newData = func(newData);
}
return newData;
}
Однако в этот момент мне кажется, что я сделал свой код более сложным / запутанным для всех, кто его использует, так как мне приходится искать полезную функцию. Единственное практическое преимущество, которое я вижу, состоит в том, что оно заставляет меня сделать цикл чистым; но кажется, что было бы проще просто использовать обычный цикл while и следить за тем, чтобы все было чисто.
Я также попытался найти способ создания функции-генератора, которая имитирует эффекты рекурсии / зацикливания, а затем выполняет итерацию по ней с помощью вспомогательной функции, такой как поиск или уменьшение. Тем не менее, я еще не нашел читаемый способ сделать это.
Наконец, замена циклов служебными функциями делает более очевидным то, что я пытаюсь выполнить (например, сделать что-то для каждого элемента, проверить, проходит ли каждый элемент тест и т. Д.). Тем не менее, мне кажется, что цикл while уже выражает то, что я пытаюсь выполнить (например, итерация, пока мы не найдем простое число, итерация, пока ответ не будет достаточно оптимизирован и т. Д.).
Итак, после всего этого, мой общий вопрос: если мне нужен цикл while, я программирую в функциональном стиле, и у меня нет доступа к оптимизации хвостовых вызовов, то какая стратегия лучше?
3 ответа
Пример в JavaScript
Вот пример использования JavaScript. В настоящее время большинство браузеров не поддерживают оптимизацию хвостового вызова, и поэтому следующий фрагмент не удастся
const repeat = n => f => x =>
n === 0 ? x : repeat (n - 1) (f) (f(x))
console.log(repeat(1e3) (x => x + 1) (0)) // 1000
console.log(repeat(1e5) (x => x + 1) (0)) // Error: Uncaught RangeError: Maximum call stack size exceeded
Батуты
Мы можем обойти это ограничение, изменив способ написания повторения, но только немного. Вместо того, чтобы возвращать значение напрямую или сразу же, мы вернем один из наших двух типов батутов, Bounce
или же Done
, Тогда мы будем использовать наши trampoline
функция для обработки цикла.
// trampoline
const Bounce = (f,x) => ({ isBounce: true, f, x })
const Done = x => ({ isBounce: false, x })
const trampoline = ({ isBounce, f, x }) => {
while (isBounce)
({ isBounce, f, x } = f(x))
return x
}
// our revised repeat function, now stack-safe
const repeat = n => f => x =>
n === 0 ? Done(x) : Bounce(repeat (n - 1) (f), f(x))
// apply trampoline to the result of an ordinary call repeat
let result = trampoline(repeat(1e6) (x => x + 1) (0))
// no more stack overflow
console.log(result) // 1000000
Карри тоже немного тормозит, но мы можем исправить это, используя вспомогательную функцию для рекурсии. Это тоже хорошо, потому что скрывает детали реализации батута и не ожидает, что вызывающая сторона отскочит возвращаемое значение. Это работает примерно в два раза быстрее, чем выше repeat
// aux helper hides trampoline implementation detail
// runs about 2x as fast
const repeat = n => f => x => {
const aux = (n, x) =>
n === 0 ? Done(x) : Bounce(x => aux (n - 1, x), f (x))
return trampoline (aux (n, x))
}
Clojure стиле loop
/ recur
Батуты хороши и все, но это немного раздражает, когда приходится беспокоиться о звонке trampoline
на возвращаемое значение вашей функции. Мы видели, что альтернативой было использование вспомогательного помощника, но давай, это тоже немного раздражает. Я уверен, что некоторые из вас не слишком заинтересованы в Bounce
а также Done
фантики тоже.
Clojure создает специализированный батутный интерфейс, который использует пару функций, loop
а также recur
- эта тандемная пара поддается удивительно элегантному выражению программы
Ох, и это действительно быстро
const recur = (...args) =>
({ type: recur, args })
const loop = f =>
{
let acc = f ()
while (acc && acc.type === recur)
acc = f (...acc.args)
return acc
}
const repeat = $n => $f => $x =>
loop ((n = $n, f = $f, x = $x) =>
n === 0
? x
: recur (n - 1, f, f (x)))
console.time ('loop/recur')
console.log (repeat (1e6) (x => x + 1) (0)) // 1000000
console.timeEnd ('loop/recur') // 24 ms
Продолжение монады
Это одна из моих любимых тем, поэтому мы посмотрим, как это выглядит с монадой Continuation. Мы также сделаем еще один шаг и скроем детали реализации батута внутри нашего repeat
функция с использованием вспомогательной функции (aux
) так, чтобы вызывающему не приходилось каждый раз возвращать возвращаемое значение
// trampoline
const Bounce = (f,x) => ({ isBounce: true, f, x })
const trampoline = t => {
while (t && t.isBounce)
t = t.f(t.x)
return t
}
// Continuation monad; stack-safe implementation
const Cont = f => ({
_runCont: f,
chain: g =>
Cont(k =>
Bounce(f, x =>
Bounce(g(x)._runCont, k)))
})
Cont.of = x => Cont(k => k(x))
const runCont = (m,k) =>
trampoline(Bounce(m._runCont, k))
// repeat now leaks no implementation detail that a trampoline is used
const repeat = n => f => x => {
const aux = (n,x) =>
n === 0 ? Cont.of(x) : Cont.of(f(x)).chain(x => aux(n - 1, x))
return runCont(aux(n,x), x => x)
}
// looks like any other function, still stack-safe
console.log(repeat(1e5) (x => x + 1) (0))
Y
комбинатор
Y комбинатор - мой духовой комбинатор; этот ответ был бы неполным, если бы он не занимал какое-то место среди других методов. Однако большинство реализаций Y-комбинатора не являются безопасными для стека и переполняются, если пользовательская функция повторяется слишком много раз. Так как этот ответ все о сохранении безопасного поведения стека, конечно, мы будем реализовывать Y
безопасным способом - снова, полагаясь на наш верный батут.
Y
демонстрирует способность расширять простую в использовании, безопасную для стека, синхронную бесконечную рекурсию, не загромождая вашу функцию.
const bounce = f => (...xs) =>
({ isBounce: true, f, xs })
const trampoline = t => {
while (t && t.isBounce)
t = t.f(...t.xs)
return t
}
// stack-safe Y combinator
const Y = f => {
const safeY = f =>
bounce((...xs) => f (safeY (f), ...xs))
return (...xs) =>
trampoline (safeY (f) (...xs))
}
// recur safely to your heart's content
const repeat = Y ((recur, n, f, x) =>
n === 0
? x
: recur (n - 1, f, f (x)))
console.log(repeat (1e5, x => x + 1, 0)) // 10000
Практичность с while
петля
Но давайте будем честными: это большая церемония, когда мы пропускаем одно из наиболее очевидных потенциальных решений: используйте for
или же while
цикл, но скрыть его за функциональным интерфейсом
Для всех намерений и целей это repeat
Функция работает идентично приведенным выше, за исключением того, что эта функция примерно в один-два раза быстрее (за исключением loop
/ recur
решение). Черт возьми, это, пожалуй, намного легче читать.
Конечно, эта функция, возможно, является надуманным примером - не все рекурсивные функции могут быть преобразованы в for
или же while
Цикл так легко, но в таком сценарии, где это возможно, вероятно, лучше просто сделать это так. Сохраните батуты и продолжения для тяжелой работы, когда простая петля не будет делать.
const repeat = n => f => x => {
let m = n
while (true) {
if (m === 0)
return x
else
(m = m - 1, x = f (x))
}
}
const gadzillionTimes = repeat(1e8)
const add1 = x => x + 1
const result = gadzillionTimes (add1) (0)
console.log(result) // 100000000
setTimeout
НЕ является решением проблемы переполнения стека
ОК, это работает, но только парадоксально. Если ваш набор данных небольшой, вам не нужно setTimeout
потому что не будет переполнения стека. Если ваш набор данных большой и вы используете setTimeout
как безопасный механизм рекурсии, вы не только лишаете возможности синхронно возвращать значение из вашей функции, это будет настолько медленным, что вы даже не захотите использовать свою функцию
Некоторые люди нашли сайт для вопросов и ответов, который поддерживает эту ужасную стратегию.
Какие наши repeat
будет выглядеть как использование setTimeout
- обратите внимание, это также определено в стиле передачи продолжения - то есть мы должны вызвать repeat
с обратным вызовом (k
) чтобы получить окончательное значение
// do NOT implement recursion using setTimeout
const repeat = n => f => x => k =>
n === 0
? k (x)
: setTimeout (x => repeat (n - 1) (f) (x) (k), 0, f (x))
// be patient, this one takes about 5 seconds, even for just 1000 recursions
repeat (1e3) (x => x + 1) (0) (console.log)
// comment the next line out for absolute madness
// 10,000 recursions will take ~1 MINUTE to complete
// paradoxically, direct recursion can compute this in a few milliseconds
// setTimeout is NOT a fix for the problem
// -----------------------------------------------------------------------------
// repeat (1e4) (x => x + 1) (0) (console.log)
Я не могу не подчеркнуть, насколько это плохо. Четное 1e5
бежать так долго, что я бросил пытаться измерить это. Я не включаю это в тесты ниже, потому что это слишком медленно, чтобы даже считаться жизнеспособным подходом.
обещания
Обещания имеют возможность цепочки вычислений и безопасны в стеке. Тем не менее, достижение стека безопасно repeat
Использование Promises означает, что нам придется отказаться от нашего синхронного возвращаемого значения, так же, как мы использовали setTimeout
, Я представляю это как "решение", потому что оно решает проблему, в отличие от setTimeout
, но в некотором смысле это очень просто по сравнению с батутом или продолжением монады. Как вы можете себе представить, производительность немного плохая, но далеко не такая плохая, как setTimeout
пример выше
Стоит отметить, что в этом решении детали реализации Promise полностью скрыты от вызывающей стороны. Одиночное продолжение предоставляется в качестве 4-го аргумента и вызывается, когда вычисление завершено.
const repeat = n => f => x => k =>
n === 0
? Promise.resolve(x).then(k)
: Promise.resolve(f(x)).then(x => repeat (n - 1) (f) (x) (k))
// be patient ...
repeat (1e6) (x => x + 1) (0) (x => console.log('done', x))
Ориентиры
Серьезно, while
Цикл намного быстрее - почти в 100 раз быстрее (при сравнении лучшего и худшего - но не включает асинхронные ответы: setTimeout
а также Promise
)
// sync
// -----------------------------------------------------------------------------
// repeat implemented with basic trampoline
console.time('A')
console.log(tramprepeat(1e6) (x => x + 1) (0))
console.timeEnd('A')
// 1000000
// A 114 ms
// repeat implemented with basic trampoline and aux helper
console.time('B')
console.log(auxrepeat(1e6) (x => x + 1) (0))
console.timeEnd('B')
// 1000000
// B 64 ms
// repeat implemented with cont monad
console.time('C')
console.log(contrepeat(1e6) (x => x + 1) (0))
console.timeEnd('C')
// 1000000
// C 33 ms
// repeat implemented with Y
console.time('Y')
console.log(yrepeat(1e6) (x => x + 1) (0))
console.timeEnd('Y')
// 1000000
// Y 544 ms
// repeat implemented with while loop
console.time('D')
console.log(whilerepeat(1e6) (x => x + 1) (0))
console.timeEnd('D')
// 1000000
// D 4 ms
// async
// -----------------------------------------------------------------------------
// repeat implemented with Promise
console.time('E')
promiserepeat(1e6) (x => x + 1) (0) (console.log)
console.timeEnd('E')
// 1000000
// E 2224 ms
// repeat implemented with setTimeout; FAILED
console.time('F')
timeoutrepeat(1e6) (x => x + 1) (0) (console.log)
console.timeEnd('F')
// ...
// too slow; didn't finish after 3 minutes
Каменный век JavaScript
Вышеупомянутые методы демонстрируются с использованием новых синтаксисов ES6, но вы можете реализовать батут в самой ранней версии JavaScript - для этого требуется только while
и функции первого класса
Ниже мы используем javascript каменного века, чтобы продемонстрировать, что бесконечная рекурсия возможна и эффективна без необходимости жертвовать синхронным возвращаемым значением - 100000000 рекурсий менее чем за 6 секунд - это существенная разница по сравнению с setTimeout
который может сделать только 1000 рекурсий за одно и то же время.
function trampoline (t) {
while (t && t.isBounce)
t = t.f (t.x);
return t.x;
}
function bounce (f, x) {
return { isBounce: true, f: f, x: x };
}
function done (x) {
return { isBounce: false, x: x };
}
function repeat (n, f, x) {
function aux (n, x) {
if (n === 0)
return done (x);
else
return bounce (function (x) { return aux (n - 1, x); }, f (x));
}
return trampoline (aux (n, x));
}
console.time('JS1 100K');
console.log (repeat (1e5, function (x) { return x + 1 }, 0));
console.timeEnd('JS1 100K');
// 100000
// JS1 100K: 15ms
console.time('JS1 100M');
console.log (repeat (1e8, function (x) { return x + 1 }, 0));
console.timeEnd('JS1 100M');
// 100000000
// JS1 100K: 5999ms
Неблокирующая бесконечная рекурсия с использованием каменного века JavaScript
Если по какой-то причине вы хотите неблокирующую (асинхронную) бесконечную рекурсию, мы можем положиться на setTimeout
отложить один кадр в начале вычисления. Эта программа также использует javascript каменного века и вычисляет 100000000 рекурсий менее чем за 8 секунд, но на этот раз неблокирующим образом.
Это демонстрирует, что нет ничего особенного в наличии неблокирующего требования. while
Циклические и первоклассные функции по-прежнему являются единственным фундаментальным требованием для достижения рекурсии в стеке без потери производительности.
В современной программе, учитывая обещания, мы заменили бы setTimeout
призыв к одному обещанию.
function donek (k, x) {
return { isBounce: false, k: k, x: x };
}
function bouncek (f, x) {
return { isBounce: true, f: f, x: x };
}
function trampolinek (t) {
// setTimeout is called ONCE at the start of the computation
// NOT once per recursion
return setTimeout(function () {
while (t && t.isBounce) {
t = t.f (t.x);
}
return t.k (t.x);
}, 0);
}
// stack-safe infinite recursion, non-blocking, 100,000,000 recursions in under 8 seconds
// now repeatk expects a 4th-argument callback which is called with the asynchronously computed result
function repeatk (n, f, x, k) {
function aux (n, x) {
if (n === 0)
return donek (k, x);
else
return bouncek (function (x) { return aux (n - 1, x); }, f (x));
}
return trampolinek (aux (n, x));
}
console.log('non-blocking line 1')
console.time('non-blocking JS1')
repeatk (1e8, function (x) { return x + 1; }, 0, function (result) {
console.log('non-blocking line 3', result)
console.timeEnd('non-blocking JS1')
})
console.log('non-blocking line 2')
// non-blocking line 1
// non-blocking line 2
// [ synchronous program stops here ]
// [ below this line, asynchronous program continues ]
// non-blocking line 3 100000000
// non-blocking JS1: 7762ms
Лучше loop
/recur
шаблон
Есть две вещи, которые мне не нравятся в loop
/recur
шаблон, описанный в принятом ответе. Обратите внимание, что мне действительно нравится идея, лежащая в основеloop
/recur
шаблон. Однако мне не нравится, как это реализовано. Итак, давайте сначала посмотрим, как я бы это реализовал.
// Recur ::a -> Result a b
const Recur = (...args) => ({ recur: true, args });
// Return :: b-> Result a b
const Return = value => ({ recur: false, value });
// loop :: (a -> Result a b) -> a -> b
const loop = func => (...args) => {
let result = func(...args);
while (result.recur) result = func(...result.args);
return result.value;
};
// repeat :: (Int, a -> a, a) -> a
const repeat = loop((n, f, x) => n === 0 ? Return(x) : Recur(n - 1, f, f(x)));
console.time("loop/recur/return");
console.log(repeat(1e6, x => x + 1, 0));
console.timeEnd("loop/recur/return");
Сравните это с loop
/recur
шаблон, описанный в вышеупомянутом ответе.
// recur ::a -> Recur a
const recur = (...args) => ({ recur, args });
// loop :: (a? -> Recur a ∪ b) -> b
const loop = func => {
let result = func();
while (result && result.recur === recur) result = func(...result.args);
return result;
};
// repeat :: (Int, a -> a, a) -> a
const repeat = (n, f, x) => loop((m = n, r = x) => m === 0 ? r : recur(m - 1, f(r)));
console.time("loop/recur");
console.log(repeat(1e6, x => x + 1, 0));
console.timeEnd("loop/recur");
Если вы заметили, подпись типа второго loop
функция использует параметры по умолчанию (т.е.a?
) и немаркированные союзы (т.е.Recur a ∪ b
). Обе эти возможности противоречат парадигме функционального программирования.
Проблема с параметрами по умолчанию
В loop
/recur
шаблон в вышеупомянутом ответе использует параметры по умолчанию для предоставления начальных аргументов функции. Я считаю, что это злоупотребление параметрами по умолчанию. Вы можете так же легко предоставить начальные аргументы, используя мою версиюloop
.
// repeat :: (Int, a -> a, a) -> a
const repeat = (n, f, x) => loop((n, x) => n === 0 ? Return(x) : Recur(n - 1, f(x)))(n, x);
// or more readable
const repeat = (n, f, x) => {
const repeatF = loop((n, x) => n === 0 ? Return(x) : Recur(n - 1, f(x)));
return repeatF(n, x);
};
Более того, он позволяет преобразовывать eta, когда все аргументы переданы.
// repeat :: (Int, a -> a, a) -> a
const repeat = (n, f, x) => loop((n, f, x) => n === 0 ? Return(x) : Recur(n - 1, f, f(x)))(n, f, x);
// can be η-converted to
const repeat = loop((n, f, x) => n === 0 ? Return(x) : Recur(n - 1, f, f(x)));
Используя версию loop
с параметрами по умолчанию не позволяет конвертировать eta. Кроме того, он заставляет вас переименовывать параметры, потому что вы не можете писать(n = n, x = x) => ...
в JavaScript.
Проблема с немаркированными союзами
Нетегированные союзы плохи, потому что они стирают важную информацию, то есть информацию о том, откуда пришли данные. Например, потому что мойResult
тип помечен я могу различить Return(Recur(0))
от Recur(0)
.
С другой стороны, поскольку правосторонний вариант Recur a ∪ b
не помечен, если b
специализируется на Recur a
, т.е. если тип специализируется на Recur a ∪ Recur a
, то невозможно определить, Recur a
пришел с левой или с правой стороны.
Одна критика может заключаться в том, что b
никогда не будет специализироваться на Recur a
, а значит, не имеет значения, что b
без тегов. Вот простой контрпример этой критике.
// recur ::a -> Recur a
const recur = (...args) => ({ recur, args });
// loop :: (a? -> Recur a ∪ b) -> b
const loop = func => {
let result = func();
while (result && result.recur === recur) result = func(...result.args);
return result;
};
// repeat :: (Int, a -> a, a) -> a
const repeat = (n, f, x) => loop((m = n, r = x) => m === 0 ? r : recur(m - 1, f(r)));
// infinite loop
console.log(repeat(1, x => recur(1, x), "wow, such hack, much loop"));
// unreachable code
console.log("repeat wasn't hacked");
Сравните это с моей версией repeat
который является пуленепробиваемым.
// Recur ::a -> Result a b
const Recur = (...args) => ({ recur: true, args });
// Return :: b-> Result a b
const Return = value => ({ recur: false, value });
// loop :: (a -> Result a b) -> a -> b
const loop = func => (...args) => {
let result = func(...args);
while (result.recur) result = func(...result.args);
return result.value;
};
// repeat :: (Int, a -> a, a) -> a
const repeat = loop((n, f, x) => n === 0 ? Return(x) : Recur(n - 1, f, f(x)));
// finite loop
console.log(repeat(1, x => Recur(1, x), "wow, such hack, much loop"));
// reachable code
console.log("repeat wasn't hacked");
Таким образом, немаркированные союзы небезопасны. Однако, даже если бы мы были осторожны, чтобы избежать ошибок, связанных с объединениями без тегов, я бы все равно предпочел объединения с тегами, потому что теги предоставляют полезную информацию при чтении и отладке программы. ИМХО, теги делают программу более понятной и простой в отладке.
Вывод
Процитирую дзен Python.
Явное лучше, чем неявное.
Параметры по умолчанию и немаркированные объединения плохи, потому что они неявны и могут привести к двусмысленностям.
В Trampoline
монада
А теперь я хотел бы переключить передачи и поговорить о монадах. Принятый ответ демонстрирует монаду продолжения, безопасную для стека. Однако, если вам нужно создать только монадическую рекурсивную функцию, безопасную для стека, тогда вам не понадобится полная мощь монады продолжения. Вы можете использоватьTrampoline
монада.
В Trampoline
монада - более могущественная кузина Loop
монада, которая является loop
функция преобразована в монаду. Итак, начнем с пониманияLoop
монада. Тогда мы увидим основную проблемуLoop
монада и как Trampoline
монаду можно использовать для решения этой проблемы.
// Recur ::a -> Result a b
const Recur = (...args) => ({ recur: true, args });
// Return :: b-> Result a b
const Return = value => ({ recur: false, value });
// Loop :: (a -> Result a b) -> a -> Loop b
const Loop = func => (...args) => ({ func, args });
// runLoop :: Loop a -> a
const runLoop = ({ func, args }) => {
let result = func(...args);
while (result.recur) result = func(...result.args);
return result.value;
};
// pure ::a -> Loop a
const pure = Loop(Return);
// bind :: (Loop a, a -> Loop b) -> Loop b
const bind = (loop, next) => Loop(({ first, loop: { func, args } }) => {
const result = func(...args);
if (result.recur) return Recur({ first, loop: { func, args: result.args } });
if (first) return Recur({ first: false, loop: next(result.value) });
return result;
})({ first: true, loop });
// ack :: (Int, Int) -> Loop Int
const ack = (m, n) => {
if (m === 0) return pure(n + 1);
if (n === 0) return ack(m - 1, 1);
return bind(ack(m, n - 1), n => ack(m - 1, n));
};
console.log(runLoop(ack(3, 4)));
Обратите внимание, что loop
был разделен на Loop
и runLoop
функция. Структура данных, возвращаемаяLoop
это монада, а pure
а также bind
функции реализуют его монадический интерфейс. Мы используемpure
а также bind
функции для написания простой реализации функции Аккермана.
К сожалению, ack
не безопасна для стека, потому что она рекурсивно вызывает себя, пока не достигнет pure
ценность. Вместо этого мы хотели быack
вернуть Recur
как структура данных для его индуктивных случаев. Однако,Recur
значения имеют тип Result
вместо того Loop
. Эта проблема решаетсяTrampoline
монада.
// Bounce :: (a -> Trampoline b) -> a -> Trampoline b
const Bounce = func => (...args) => ({ bounce: true, func, args });
// Return ::a -> Trampoline a
const Return = value => ({ bounce: false, value });
// trampoline :: Trampoline a -> a
const trampoline = result => {
while (result.bounce) result = result.func(...result.args);
return result.value;
};
// pure ::a -> Trampoline a
const pure = Return;
// bind :: (Trampoline a, a -> Trampoline b) -> Trampoline b
const bind = (first, next) => first.bounce ?
Bounce(args => bind(first.func(...args), next))(first.args) :
next(first.value);
// ack :: (Int, Int) -> Trampoline Int
const ack = Bounce((m, n) => {
if (m === 0) return pure(n + 1);
if (n === 0) return ack(m - 1, 1);
return bind(ack(m, n - 1), n => ack(m - 1, n));
});
console.log(trampoline(ack(3, 4)));
В Trampoline
тип данных - это комбинация Loop
а также Result
. ВLoop
а также Recur
конструкторы данных объединены в единый Bounce
конструктор данных. ВrunLoop
функция была упрощена и переименована в trampoline
. Вpure
а также bind
функции также были упрощены. По факту,pure
просто Return
. Наконец, мы применяемBounce
к исходной реализации ack
функция.
Еще одно преимущество Trampoline
в том, что его можно использовать для определения взаимно рекурсивных функций, безопасных для стека. Например, вот реализация функций женской и мужской последовательности Hofstadter.
// Bounce :: (a -> Trampoline b) -> a -> Trampoline b
const Bounce = func => (...args) => ({ bounce: true, func, args });
// Return ::a -> Trampoline a
const Return = value => ({ bounce: false, value });
// trampoline :: Trampoline a -> a
const trampoline = result => {
while (result.bounce) result = result.func(...result.args);
return result.value;
};
// pure ::a -> Trampoline a
const pure = Return;
// bind :: (Trampoline a, a -> Trampoline b) -> Trampoline b
const bind = (first, next) => first.bounce ?
Bounce(args => bind(first.func(...args), next))(first.args) :
next(first.value);
// female :: Int -> Trampoline Int
const female = Bounce(n => n === 0 ? pure(1) :
bind(female(n - 1), f =>
bind(male(f), m =>
pure(n - m))));
// male :: Int -> Trampoline Int
const male = Bounce(n => n === 0 ? pure(0) :
bind(male(n - 1), m =>
bind(female(m), f =>
pure(n - f))));
console.log(Array.from({ length: 21 }, (_, n) => trampoline(female(n))).join(" "));
console.log(Array.from({ length: 21 }, (_, n) => trampoline(male(n))).join(" "));
Основная проблема при написании монадического кода - это ад обратного вызова. Однако это можно решить с помощью генераторов.
// Bounce :: (a -> Trampoline b) -> a -> Trampoline b
const Bounce = func => (...args) => ({ bounce: true, func, args });
// Return ::a -> Trampoline a
const Return = value => ({ bounce: false, value });
// trampoline :: Trampoline a -> a
const trampoline = result => {
while (result.bounce) result = result.func(...result.args);
return result.value;
};
// pure ::a -> Trampoline a
const pure = Return;
// bind :: (Trampoline a, a -> Trampoline b) -> Trampoline b
const bind = (first, next) => first.bounce ?
Bounce(args => bind(first.func(...args), next))(first.args) :
next(first.value);
// bounce :: (a -> Generator (Trampoline b)) -> a -> Trampoline b
const bounce = func => Bounce((...args) => {
const gen = func(...args);
const next = data => {
const { value, done } = gen.next(data);
return done ? value : bind(value, next);
};
return next(undefined);
});
// female :: Int -> Trampoline Int
const female = bounce(function* (n) {
return pure(n ? n - (yield male(yield female(n - 1))) : 1);
});
// male :: Int -> Trampoline Int
const male = bounce(function* (n) {
return pure(n ? n - (yield female(yield male(n - 1))) : 0);
});
console.log(Array.from({ length: 21 }, (_, n) => trampoline(female(n))).join(" "));
console.log(Array.from({ length: 21 }, (_, n) => trampoline(male(n))).join(" "));
Наконец, взаимно рекурсивные функции также демонстрируют преимущество наличия отдельного trampoline
функция. Это позволяет нам вызывать функцию, возвращающуюTrampoline
значение без его фактического запуска. Это позволяет нам строить большеTrampoline
значений, а затем при необходимости выполнить все вычисления.
Вывод
Если вы хотите писать косвенно или взаимно рекурсивные функции, безопасные для стека, или монадические функции, безопасные для стека, используйте Trampoline
монада. Если вы хотите писать немонадические, напрямую рекурсивные, безопасные для стека функции, используйтеloop
/recur
/return
шаблон.
Программирование в смысле функциональной парадигмы означает, что мы руководствуемся типами для выражения наших алгоритмов.
Чтобы преобразовать хвостовую рекурсивную функцию в безопасную для стека версию, мы должны рассмотреть два случая:
- базовый вариант
- рекурсивный случай
Мы должны сделать выбор, и это хорошо сочетается с помеченными профсоюзами. Тем не менее, Javascript не имеет такого типа данных, поэтому мы должны либо создать его, либо вернуться к Object
кодировок.
Кодированный объект
// simulate a tagged union with two Object types
const Loop = x =>
({value: x, done: false});
const Done = x =>
({value: x, done: true});
// trampoline
const tailRec = f => (...args) => {
let step = Loop(args);
do {
step = f(Loop, Done, step.value);
} while (!step.done);
return step.value;
};
// stack-safe function
const repeat = n => f => x =>
tailRec((Loop, Done, [m, y]) => m === 0
? Done(y)
: Loop([m - 1, f(y)])) (n, x);
// run...
const inc = n =>
n + 1;
console.time();
console.log(repeat(1e6) (inc) (0));
console.timeEnd();
Функция закодирована
В качестве альтернативы мы можем создать реальный теговый союз с кодировкой функции. Теперь наш стиль намного ближе к зрелым функциональным языкам:
// type/data constructor
const Type = Tcons => (tag, Dcons) => {
const t = new Tcons();
t.run = cases => Dcons(cases);
t.tag = tag;
return t;
};
// tagged union specific for the case
const Step = Type(function Step() {});
const Done = x =>
Step("Done", cases => cases.Done(x));
const Loop = args =>
Step("Loop", cases => cases.Loop(args));
// trampoline
const tailRec = f => (...args) => {
let step = Loop(args);
do {
step = f(step);
} while (step.tag === "Loop");
return step.run({Done: id});
};
// stack-safe function
const repeat = n => f => x =>
tailRec(step => step.run({
Loop: ([m, y]) => m === 0 ? Done(y) : Loop([m - 1, f(y)]),
Done: y => Done(y)
})) (n, x);
// run...
const inc = n => n + 1;
const id = x => x;
console.log(repeat(1e6) (inc) (0));
Смотрите также развернуть который (из Рамда документов)
Создает список из начального значения. Принимает функцию итератора, которая возвращает либо false, чтобы остановить итерацию, либо массив длины 2, содержащий значение, добавляемое в результирующий список, и начальное число, которое будет использоваться при следующем вызове функции итератора.
var r = n => f => x => x > n ? false : [x, f(x)];
var repeatUntilGreaterThan = n => f => R.unfold(r(n)(f), 1);
console.log(repeatUntilGreaterThan(10)(x => x + 1));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.22.1/ramda.min.js"></script>
Я много думал об этом вопросе. Недавно я натолкнулся на необходимость функционального цикла while.
Мне кажется, что единственное, что на самом деле хочет этот вопрос, - это способ встроить цикл while. Есть способ сделать это с помощью замыкания.
"some string "+(a=>{
while(comparison){
// run code
}
return result;
})(somearray)+" some more"
В качестве альтернативы, если то, что вы хотите, является чем-то, что цепляет массив, тогда вы можете использовать метод Reduce.
somearray.reduce((r,o,i,a)=>{
while(comparison){
// run code
}
a.splice(1); // This would ensure only one call.
return result;
},[])+" some more"
Ничто из этого на самом деле не превращает наш цикл while в его ядро в функцию. Но это позволяет нам использовать встроенный цикл. И я просто хотел поделиться этим со всеми, кто мог бы помочь.