在最近的工作讨论中,有人提到了蹦床功能。
我已阅读Wikipedia上的描述。给出功能的一般概念就足够了,但我想要更具体的东西。
你有一个简单的代码片段来说明蹦床吗?
如维基百科所述,还有 LISP 的“蹦床”感觉:
在某些 LISP 实现中使用的蹦床是一个循环,它迭代地调用 thunk-returning 函数。一个蹦床就足以表达一个程序的所有控制转移;如此表达的节目是蹦床式或“蹦床式”;将程序转换为蹦床风格就是蹦床。Trampolined 函数可用于在面向堆栈的语言中实现尾递归函数调用
假设我们正在使用 Javascript,并且想要以连续传递样式编写简单的斐波那契函数。我们这样做的原因无关紧要——例如将 Scheme 移植到 JS,或者使用 CPS,无论如何我们都必须使用它来调用服务器端函数。
所以,第一次尝试是
function fibcps(n, c) {
if (n <= 1) {
c(n);
} else {
fibcps(n - 1, function (x) {
fibcps(n - 2, function (y) {
c(x + y)
})
});
}
}
但是,n = 25
在 Firefox 中运行它会出现错误“递归过多!”。现在这正是蹦床解决的问题(Javascript 中缺少尾调用优化)。与其对函数进行(递归)调用,不如让我们return
使用指令(thunk)来调用该函数,并在循环中进行解释。
function fibt(n, c) {
function trampoline(x) {
while (x && x.func) {
x = x.func.apply(null, x.args);
}
}
function fibtramp(n, c) {
if (n <= 1) {
return {func: c, args: [n]};
} else {
return {
func: fibtramp,
args: [n - 1,
function (x) {
return {
func: fibtramp,
args: [n - 2, function (y) {
return {func: c, args: [x + y]}
}]
}
}
]
}
}
}
trampoline({func: fibtramp, args: [n, c]});
}
让我添加几个使用不同语言的蹦床实现的阶乘函数示例:
斯卡拉:
sealed trait Bounce[A]
case class Done[A](result: A) extends Bounce[A]
case class Call[A](thunk: () => Bounce[A]) extends Bounce[A]
def trampoline[A](bounce: Bounce[A]): A = bounce match {
case Call(thunk) => trampoline(thunk())
case Done(x) => x
}
def factorial(n: Int, product: BigInt): Bounce[BigInt] = {
if (n <= 2) Done(product)
else Call(() => factorial(n - 1, n * product))
}
object Factorial extends Application {
println(trampoline(factorial(100000, 1)))
}
爪哇:
import java.math.BigInteger;
class Trampoline<T>
{
public T get() { return null; }
public Trampoline<T> run() { return null; }
T execute() {
Trampoline<T> trampoline = this;
while (trampoline.get() == null) {
trampoline = trampoline.run();
}
return trampoline.get();
}
}
public class Factorial
{
public static Trampoline<BigInteger> factorial(final int n, final BigInteger product)
{
if(n <= 1) {
return new Trampoline<BigInteger>() { public BigInteger get() { return product; } };
}
else {
return new Trampoline<BigInteger>() {
public Trampoline<BigInteger> run() {
return factorial(n - 1, product.multiply(BigInteger.valueOf(n)));
}
};
}
}
public static void main( String [ ] args )
{
System.out.println(factorial(100000, BigInteger.ONE).execute());
}
}
C(不幸没有大数字实现):
#include <stdio.h>
typedef struct _trampoline_data {
void(*callback)(struct _trampoline_data*);
void* parameters;
} trampoline_data;
void trampoline(trampoline_data* data) {
while(data->callback != NULL)
data->callback(data);
}
//-----------------------------------------
typedef struct _factorialParameters {
int n;
int product;
} factorialParameters;
void factorial(trampoline_data* data) {
factorialParameters* parameters = (factorialParameters*) data->parameters;
if (parameters->n <= 1) {
data->callback = NULL;
}
else {
parameters->product *= parameters->n;
parameters->n--;
}
}
int main() {
factorialParameters params = {5, 1};
trampoline_data t = {&factorial, ¶ms};
trampoline(&t);
printf("\n%d\n", params.product);
return 0;
}
我将举一个我在在线游戏的反作弊补丁中使用的示例。
我需要能够扫描游戏加载的所有文件以进行修改。因此,我发现最可靠的方法是对 CreateFileA 使用蹦床。所以当游戏启动时,我会使用 GetProcAddress 找到 CreateFileA 的地址,然后我会修改函数的前几个字节并插入汇编代码,这些代码会跳转到我自己的“蹦床”函数,在那里我会做一些事情,并且然后我会在我的 jmp 代码之后跳回到 CreateFile 中的下一个位置。能够可靠地做到这一点比这要复杂一些,但基本概念只是挂钩一个函数,强制它重定向到另一个函数,然后跳回原始函数。
编辑:微软有一个你可以查看的此类事物的框架。所谓的弯路
我目前正在尝试为 Scheme 解释器实现尾调用优化的方法,所以目前我正试图弄清楚蹦床对我来说是否可行。
据我了解,它基本上只是蹦床函数执行的一系列函数调用。每个函数称为 thunk 并返回计算的下一步,直到程序终止(空继续)。
这是我为提高对蹦床的理解而编写的第一段代码:
#include <stdio.h>
typedef void *(*CONTINUATION)(int);
void trampoline(CONTINUATION cont)
{
int counter = 0;
CONTINUATION currentCont = cont;
while (currentCont != NULL) {
currentCont = (CONTINUATION) currentCont(counter);
counter++;
}
printf("got off the trampoline - happy happy joy joy !\n");
}
void *thunk3(int param)
{
printf("*boing* last thunk\n");
return NULL;
}
void *thunk2(int param)
{
printf("*boing* thunk 2\n");
return thunk3;
}
void *thunk1(int param)
{
printf("*boing* thunk 1\n");
return thunk2;
}
int main(int argc, char **argv)
{
trampoline(thunk1);
}
结果是:
meincompi $ ./trampoline
*boing* thunk 1
*boing* thunk 2
*boing* last thunk
got off the trampoline - happy happy joy joy !
这是嵌套函数的示例:
#include <stdlib.h>
#include <string.h>
/* sort an array, starting at address `base`,
* containing `nmemb` members, separated by `size`,
* comparing on the first `nbytes` only. */
void sort_bytes(void *base, size_t nmemb, size_t size, size_t nbytes) {
int compar(const void *a, const void *b) {
return memcmp(a, b, nbytes);
}
qsort(base, nmemb, size, compar);
}
compar
不能是外部函数,因为它使用nbytes
,仅在sort_bytes
调用期间存在。在某些架构上,一个小的存根函数——蹦床——是在运行时生成的,它包含当前调用sort_bytes
. 调用时,它会跳转到compar
代码,并传递该地址。
PowerPC 等架构不需要这种混乱,其中 ABI 指定函数指针实际上是一个“胖指针”,一个包含指向可执行代码的指针和另一个指向数据的指针的结构。但是,在 x86 上,函数指针只是一个指针。
对于 C,蹦床将是一个函数指针:
size_t (*trampoline_example)(const char *, const char *);
trampoline_example= strcspn;
size_t result_1= trampoline_example("xyzbxz", "abc");
trampoline_example= strspn;
size_t result_2= trampoline_example("xyzbxz", "abc");
编辑:编译器会隐式生成更深奥的蹦床。一种这样的用途是跳转表。(虽然你开始尝试生成复杂的代码,但显然有更复杂的代码。)
既然 C# 有Local Functions,保龄球游戏编码 kata可以用蹦床优雅地解决:
using System.Collections.Generic;
using System.Linq;
class Game
{
internal static int RollMany(params int[] rs)
{
return Trampoline(1, 0, rs.ToList());
int Trampoline(int frame, int rsf, IEnumerable<int> rs) =>
frame == 11 ? rsf
: rs.Count() == 0 ? rsf
: rs.First() == 10 ? Trampoline(frame + 1, rsf + rs.Take(3).Sum(), rs.Skip(1))
: rs.Take(2).Sum() == 10 ? Trampoline(frame + 1, rsf + rs.Take(3).Sum(), rs.Skip(2))
: Trampoline(frame + 1, rsf + rs.Take(2).Sum(), rs.Skip(2));
}
}
该方法Game.RollMany
通过多次滚动调用:如果没有备用或罢工,通常为 20 次滚动。
第一行立即调用蹦床函数:return Trampoline(1, 0, rs.ToList());
. 这个局部函数递归地遍历 rolls 数组。本地函数(蹦床)允许以两个附加值开始遍历:以frame
1 和rsf
(到目前为止的结果)0 开始。
在本地函数中,有一个处理五种情况的三元运算符:
通过再次调用蹦床来继续遍历,但现在使用更新的值。
更多信息,请搜索:“尾递归累加器”。请记住,编译器不会优化尾递归。因此,尽管这个解决方案可能很优雅,但它可能不会是禁食的。
typedef void* (*state_type)(void);
void* state1();
void* state2();
void* state1() {
return state2;
}
void* state2() {
return state1;
}
// ...
state_type state = state1;
while (1) {
state = state();
}
// ...