O que é uma função de trampolim?


93

Durante discussões recentes no trabalho, alguém se referiu a uma função de trampolim.

Eu li a descrição na Wikipedia . É o suficiente para dar uma ideia geral da funcionalidade, mas gostaria de algo um pouco mais concreto.

Você tem um trecho de código simples que ilustraria um trampolim?


2
No mundo da Microsoft, os trampolins são geralmente chamados de 'thunks'. [Aqui está uma página] [1] do "Modern C ++ Design" de Andrei Alexandrescu ---- [1]: books.google.com/…
Michael Burr


É basicamente a forma generalizada de alguma funcionalidade que você pode implementar com setjmp / lomgjmp, a saber, para evitar o fluxo de pilha.
Ingo

12
por que alguém iria querer evitar stackoverflow?
Nikole

Respostas:


72

Há também o sentido LISP de 'trampolim', conforme descrito na Wikipedia:

Usado em algumas implementações LISP, um trampolim é um loop que invoca funções de retorno de conversão de forma iterativa. Um único trampolim é suficiente para expressar todas as transferências de controle de um programa; um programa assim expresso é trampolim ou em "estilo trampolim"; converter um programa para o estilo trampolim é trampolim. Funções trampolinhadas podem ser usadas para implementar chamadas de função recursiva final em linguagens orientadas a pilha

Digamos que estejamos usando Javascript e queremos escrever a função Fibonacci ingênua no estilo continuation-pass. A razão pela qual faríamos isso não é relevante - para portar Scheme para JS, por exemplo, ou para brincar com CPS que temos que usar de qualquer maneira para chamar funções do lado do servidor.

Então, a primeira tentativa é

function fibcps(n, c) {
    if (n <= 1) {
        c(n);
    } else {
        fibcps(n - 1, function (x) {
            fibcps(n - 2, function (y) {
                c(x + y)
            })
        });
    }
}

Mas, ao executá-lo n = 25no Firefox, aparecerá um erro 'Muita recursão!'. Bem, este é exatamente o problema (falta de otimização de chamada de cauda em Javascript) que o trampolim resolve. Em vez de fazer uma chamada (recursiva) para uma função, vamos returnusar uma instrução (thunk) para chamar essa função, para ser interpretada em um loop.

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]});
}

39

Deixe-me adicionar alguns exemplos de função fatorial implementada com trampolins, em diferentes linguagens:

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 (azar sem implementação de grandes números):

#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, &params};

  trampoline(&t);
  printf("\n%d\n", params.product);

  return 0;
}

Sua explicação, especialmente o exemplo C, bem como a resposta do efemiente abaixo sobre funções aninhadas finalmente me fizeram entender os trampolins. Uma espécie de função auxiliar que pode ser usada para atualizar o estado como um encerramento.
Byte de

O código do Scala deve ser corrigido para if (n < 2) Done(product), SO não me permitiu editar 1 símbolo ...
Máx.

21

Vou dar um exemplo que usei em um patch anti-cheat para um jogo online.

Eu precisava ser capaz de verificar todos os arquivos carregados pelo jogo para modificação. Portanto, a maneira mais robusta que descobri de fazer isso foi usar um trampolim para CreateFileA. Então, quando o jogo fosse lançado, eu encontraria o endereço de CreateFileA usando GetProcAddress, então eu modificaria os primeiros bytes da função e inseriria o código de montagem que iria pular para minha própria função de "trampolim", onde eu faria algumas coisas, e então, eu voltaria para o próximo local em CreateFile após meu código jmp. Ser capaz de fazer isso de forma confiável é um pouco mais complicado do que isso, mas o conceito básico é apenas conectar uma função, forçá-la a redirecionar para outra função e, em seguida, voltar para a função original.

Edit: A Microsoft tem uma estrutura para esse tipo de coisa que você pode ver. Desvios chamados


8

No momento, estou experimentando maneiras de implementar a otimização de chamada de cauda para um intérprete do Scheme e, no momento, estou tentando descobrir se o trampolim seria viável para mim.

Pelo que entendi, é basicamente apenas uma série de chamadas de função realizadas por uma função de trampolim. Cada função é chamada de conversão e retorna a próxima etapa no cálculo até que o programa termine (continuação vazia).

Aqui está o primeiro código que escrevi para melhorar minha compreensão do trampolim:

#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);
}

resulta em:

meincompi $ ./trampoline 
*boing* thunk 1
*boing* thunk 2
*boing* last thunk
got off the trampoline - happy happy joy joy !

7

Aqui está um exemplo de funções aninhadas:

#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);
}

comparnão pode ser uma função externa, pois usa nbytes, que só existe durante a sort_byteschamada. Em algumas arquiteturas, uma pequena função de stub - o trampolim - é gerada em tempo de execução e contém a localização da pilha da chamada atual de sort_bytes. Quando chamado, ele salta para o comparcódigo, passando esse endereço.

Essa bagunça não é necessária em arquiteturas como PowerPC, onde a ABI especifica que um ponteiro de função é na verdade um "ponteiro gordo", uma estrutura que contém um ponteiro para o código executável e outro ponteiro para dados. No entanto, no x86, um ponteiro de função é apenas um ponteiro.


0

Para C, um trampolim seria um ponteiro de função:

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");

Edit: Trampolins mais esotéricos seriam gerados implicitamente pelo compilador. Um desses usos seria uma mesa de salto. (Embora haja claramente mais complicados quanto mais adiante você começa a tentar gerar um código complicado.)


0

Agora que C # tem funções locais , o kata de codificação do jogo de boliche pode ser resolvido elegantemente com um trampolim:

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));
    }
}

O método Game.RollManyé chamado com uma série de lançamentos: normalmente 20 lançamentos se não houver sobressalentes ou golpes.

A primeira linha imediatamente chama a função de trampolim: return Trampoline(1, 0, rs.ToList());. Esta função local percorre recursivamente a matriz de rolos. A função local (o trampolim) permite que a travessia comece com dois valores adicionais: comece com frame1 e o rsf(resultado até agora) 0.

Dentro da função local existe um operador ternário que lida com cinco casos:

  • O jogo termina no frame 11: retorna o resultado até agora
  • O jogo termina se não houver mais jogadas: retorne o resultado até agora
  • Strike: calcula a pontuação do frame e continua a travessia
  • Spare: calcula a pontuação do frame e continua a travessia
  • Pontuação normal: calcule a pontuação do quadro e continue a travessia

A continuação da travessia é feita chamando o trampolim novamente, mas agora com os valores atualizados.

Para obter mais informações, pesquise: " acumulador de recursão de cauda ". Lembre-se de que o compilador não otimiza a recursão final. Portanto, por mais elegante que seja essa solução, provavelmente não será o jejum.


-2
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();
}
// ...

3
você pode adicionar algum comentário ou explicação de por que este é um trampolim?
prasun
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.