Cos'è una funzione trampolino?


93

Durante le recenti discussioni al lavoro, qualcuno ha fatto riferimento a una funzione di trampolino.

Ho letto la descrizione su Wikipedia . È sufficiente per dare un'idea generale della funzionalità, ma vorrei qualcosa di più concreto.

Hai un semplice frammento di codice che illustri un trampolino?


2
Nel mondo Microsoft, i trampolini vengono solitamente chiamati "thunk". [Ecco una pagina] [1] dal "Modern C ++ Design" di Andrei Alexandrescu ---- [1]: books.google.com/…
Michael Burr


È basilare tutta la forma generalizzata di alcune funzionalità che potresti implementare con setjmp / lomgjmp, ovvero evitare lo stack ovwerflow.
Ingo

12
perché qualcuno dovrebbe voler evitare stackoverflow?
Nikole

Risposte:


72

C'è anche il senso LISP di 'trampolino' come descritto su Wikipedia:

Utilizzato in alcune implementazioni LISP, un trampolino è un ciclo che richiama in modo iterativo funzioni di ritorno thunk. Un singolo trampolino è sufficiente per esprimere tutti i trasferimenti di controllo di un programma; un programma così espresso è trampolino o in "stile trampolino"; convertire un programma in stile trampolino è trampolino elastico. Le funzioni trampolino possono essere utilizzate per implementare chiamate di funzione ricorsive di coda in linguaggi orientati allo stack

Diciamo che stiamo usando Javascript e vogliamo scrivere l'ingenua funzione di Fibonacci in stile continuation-crossing. Il motivo per cui lo faremmo non è rilevante, ad esempio per portare Scheme su JS o per giocare con CPS che dobbiamo comunque utilizzare per chiamare funzioni lato server.

Quindi, il primo tentativo è

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

Ma, eseguendolo con n = 25in Firefox, viene visualizzato un errore "Troppa ricorsione!". Ora questo è esattamente il problema (ottimizzazione della chiamata di coda mancante in Javascript) che il trampolino risolve. Invece di fare una chiamata (ricorsiva) a una funzione, diamo returnun'istruzione (thunk) per chiamare quella funzione, da interpretare in un ciclo.

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

Vorrei aggiungere alcuni esempi per la funzione fattoriale implementata con trampolini, in diverse lingue:

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

Giava:

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 (sfortunato senza implementazione di grandi numeri):

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

La tua spiegazione, in particolare l'esempio in C, così come la risposta effimera di seguito sulle funzioni annidate mi hanno finalmente fatto capire i trampolini. Una sorta di funzione di supporto che può essere utilizzata per aggiornare lo stato in modo molto simile a una chiusura.
Byte

Il codice Scala dovrebbe essere corretto if (n < 2) Done(product), SO non mi ha permesso di modificare 1 simbolo ...
Max

21

Ti faccio un esempio che ho usato in una patch anti-cheat per un gioco online.

Dovevo essere in grado di scansionare tutti i file che venivano caricati dal gioco per la modifica. Quindi il modo più efficace che ho trovato per farlo è stato quello di utilizzare un trampolino per CreateFileA. Quindi, quando il gioco è stato avviato, avrei trovato l'indirizzo per CreateFileA utilizzando GetProcAddress, quindi avrei modificato i primi byte della funzione e inserito il codice assembly che sarebbe passato alla mia funzione "trampolino", dove avrei fatto alcune cose, e quindi salterei indietro alla posizione successiva in CreateFile dopo il mio codice jmp. Essere in grado di farlo in modo affidabile è un po 'più complicato di così, ma il concetto di base è solo quello di agganciare una funzione, costringerla a reindirizzare a un'altra funzione e quindi tornare alla funzione originale.

Modifica: Microsoft ha un framework per questo tipo di cose che puoi guardare. Chiamato Deviazioni


8

Attualmente sto sperimentando modi per implementare l'ottimizzazione delle chiamate di coda per un interprete Scheme, e quindi al momento sto cercando di capire se il trampolino sarebbe fattibile per me.

A quanto ho capito, è fondamentalmente solo una serie di chiamate di funzione eseguite da una funzione trampolino. Ogni funzione viene chiamata thunk e restituisce il passaggio successivo nel calcolo fino al termine del programma (continuazione vuota).

Ecco il primo pezzo di codice che ho scritto per migliorare la mia comprensione del trampolino:

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

risultati in:

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

7

Ecco un esempio di funzioni annidate:

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

comparnon può essere una funzione esterna, perché utilizza nbytes, che esiste solo durante la sort_byteschiamata. Su alcune architetture, una piccola funzione stub - il trampolino - viene generata in fase di runtime e contiene la posizione dello stack della chiamata corrente di sort_bytes. Quando viene chiamato, salta al comparcodice, passando quell'indirizzo.

Questo pasticcio non è richiesto su architetture come PowerPC, dove l'ABI specifica che un puntatore a funzione è in realtà un "fat pointer", una struttura che contiene sia un puntatore al codice eseguibile che un altro puntatore ai dati. Tuttavia, su x86, un puntatore a funzione è solo un puntatore.


0

Per C, un trampolino sarebbe un puntatore a funzione:

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

Modifica: trampolini più esoterici sarebbero generati implicitamente dal compilatore. Uno di questi usi sarebbe una tabella di salto. (Sebbene ce ne siano chiaramente di più complicati, più in basso inizi a provare a generare codice complicato.)


0

Ora che C # ha funzioni locali , il kata di codifica del gioco di bowling può essere risolto elegantemente con un trampolino:

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

Il metodo Game.RollManyviene chiamato con un numero di lanci: tipicamente 20 lanci se non ci sono ricambi o strike.

La prima riga chiama immediatamente la funzione trampolino: return Trampoline(1, 0, rs.ToList());. Questa funzione locale attraversa ricorsivamente l'array rolls. La funzione locale (il trampolino) consente di iniziare l'attraversamento con due valori aggiuntivi: inizia con frame1 e il rsf(risultato finora) 0.

All'interno della funzione locale è presente un operatore ternario che gestisce cinque casi:

  • Il gioco termina al fotogramma 11: restituisci il risultato finora
  • Il gioco finisce se non ci sono più tiri: restituisci il risultato finora
  • Strike: calcola il punteggio del frame e continua l'attraversamento
  • Spare: calcola il punteggio del frame e continua l'attraversamento
  • Punteggio normale: calcola il punteggio del frame e continua l'attraversamento

La continuazione dell'attraversamento viene eseguita chiamando di nuovo il trampolino, ma ora con valori aggiornati.

Per ulteriori informazioni, cercare: " accumulatore di ricorsione della coda ". Tieni presente che il compilatore non ottimizza la ricorsione in coda. Per quanto elegante possa essere questa soluzione, probabilmente non sarà il digiuno.


-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
puoi aggiungere commenti o spiegazioni sul motivo per cui questo è un trampolino?
prasun
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.