職場での最近の議論の中で、誰かがトランポリン機能について言及しました。
ウィキペディアで説明を読みました。機能の概要を説明するだけで十分ですが、もう少し具体的なことをお願いします。
トランポリンを説明する簡単なコードスニペットはありますか?
職場での最近の議論の中で、誰かがトランポリン機能について言及しました。
ウィキペディアで説明を読みました。機能の概要を説明するだけで十分ですが、もう少し具体的なことをお願いします。
トランポリンを説明する簡単なコードスニペットはありますか?
回答:
ウィキペディアで説明されているように、「トランポリン」というLISPの感覚もあります。
一部のLISP実装で使用されるトランポリンは、サンクを返す関数を繰り返し呼び出すループです。単一のトランポリンで、プログラムのすべての制御転送を表現できます。そのように表現されたプログラムは、トランポリンまたは「トランポリンスタイル」です。プログラムをトランポリンスタイルに変換することはトランポリンです。トランポリン関数を使用して、スタック指向言語で末尾再帰関数呼び出しを実装できます
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
その関数を呼び出す命令(サンク)をループで解釈してみましょう。
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]});
}
トランポリンで実装された階乗関数の例をいくつかの言語で追加してみましょう。
Scala:
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)))
}
Java:
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;
}
if (n < 2) Done(product)
では、1つのシンボルを編集できませんでした...
オンラインゲームのチート対策パッチで使用した例を紹介します。
ゲームによって読み込まれたすべてのファイルをスキャンして変更を加える必要がありました。したがって、これを行うために見つけた最も堅牢な方法は、CreateFileAにトランポリンを使用することでした。したがって、ゲームが起動したときに、GetProcAddressを使用してCreateFileAのアドレスを検索し、関数の最初の数バイトを変更して、独自の "トランポリン"関数にジャンプするアセンブリコードを挿入します。次に、jmpコードの後でCreateFileの次の場所にジャンプします。確実に実行できるようにするのは少し難しいですが、基本的な概念は、1つの関数をフックし、それを強制的に別の関数にリダイレクトしてから、元の関数に戻ることです。
編集:Microsoftには、このタイプのことを確認できるフレームワークがあります。迂回と呼ばれる
私は現在、Schemeインタープリターの末尾呼び出しの最適化を実装する方法を実験しています。そのため、現時点では、トランポリンが自分に適しているかどうかを考えています。
私が理解しているように、それは基本的にトランポリン関数によって実行される一連の関数呼び出しです。各関数はサンクと呼ばれ、プログラムが終了する(空の継続)まで計算の次のステップを返します。
トランポリンの理解を深めるために私が書いた最初のコードは次のとおりです。
#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");
編集:より難解なトランポリンがコンパイラーによって暗黙的に生成されます。そのような用途の1つはジャンプテーブルです。(複雑なコードを生成しようとすると、さらに複雑なコードが生成されます。)
C#にローカル関数が追加されたので、ボウリングゲームコーディングカタはトランポリンでエレガントに解決できます。
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());
。このローカル関数は、ロール配列を再帰的に横断します。ローカル関数(トランポリン)を使用すると、2つの追加値でトラバーサルを開始できますframe
。1で開始し、rsf
(これまでの結果)0で開始します。
ローカル関数内には、5つのケースを処理する三項演算子があります。
トラバーサルの続行は、トランポリンを再度呼び出すことで行われますが、現在は値が更新されています。
詳細については、「末尾再帰アキュムレータ」を検索してください。コンパイラーは末尾再帰を最適化しないことに注意してください。したがって、この解決策が可能な限りエレガントであると、断食はできないでしょう。
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();
}
// ...