Що таке функція батута?


93

Під час останніх дискусій на роботі хтось посилався на функцію батута.

Я прочитав опис у Вікіпедії . Досить дати загальне уявлення про функціональність, але хотілося б чогось більш конкретного.

У вас є простий фрагмент коду, який би ілюстрував батут?


2
У світі Microsoft, батути зазвичай називають «гронами». [Ось сторінка] [1] з фільму "Сучасний дизайн C ++" Андрія Олександреску ---- [1]: books.google.com/…
Майкл Берр


Це в основному узагальнена форма деякої функціональності, яку ви могли реалізувати за допомогою setjmp / lomgjmp, а саме, щоб уникнути потоку qwer.
Інго

12
чому б хтось хотів уникнути потокового потоку?
Ніколь

Відповіді:


72

Також є сенс "батут" LISP, як описано у Вікіпедії:

Застосовуваний у деяких реалізаціях LISP, батут - це цикл, який ітеративно викликає функції, що повертають громи. Одного батуту достатньо для вираження всіх контрольних передач програми; програма, висловлена ​​таким чином, є батутною або у "батуті"; перетворення програми на батутний стиль - це батут. Батутні функції можна використовувати для реалізації хвостових рекурсивних викликів функцій на мовах, орієнтованих на стеки

Скажімо, ми використовуємо Javascript і хочемо записати наївну функцію Фібоначчі в стилі продовження-проходження. Причина, через яку ми це зробимо, не має значення - наприклад, переносити схему на 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]});
}

39

Дозвольте додати кілька прикладів факторної функції, реалізованої за допомогою батутів, різними мовами:

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

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

  return 0;
}

Ваше пояснення, особливо приклад C, а також відповідь ефемієнта нижче про вкладені функції нарешті змусили мене зрозуміти батути. Це така собі допоможна функція, яку можна використовувати для оновлення стану, подібно до закриття.
Байт

Код Scala слід виправити if (n < 2) Done(product), тому SO не дозволив мені редагувати 1 символ ...
Макс

21

Я наведу вам приклад, який я використовував в анти-чит-патчі для онлайн-гри.

Мені потрібно було мати можливість сканувати всі файли, які завантажувалась гра, для модифікації. Тож найнадійнішим способом, який я знайшов, було використання батута для CreateFileA. Отже, коли гра була запущена, я знайшов би адресу для CreateFileA за допомогою GetProcAddress, я змінив би перші кілька байтів функції та вставив код складання, який перейшов би до моєї власної функції "батут", де я б робив деякі речі, і тоді я повернувся б до наступного місця в CreateFile після мого jmp-коду. Зробити це надійно - трохи хитріше, ніж це, але основна концепція - просто підключити одну функцію, змусити її перенаправитись на іншу функцію, а потім перейти до початкової функції.

Редагування: Майкрософт має рамки для такого типу речей, які ви можете подивитися. Називається Детуром


8

Зараз я експериментую із способами впровадження оптимізації хвостових викликів для інтерпретатора схеми, і тому на даний момент я намагаюся зрозуміти, чи зможе батут для мене здійсненний.

Як я розумію, це в основному лише ряд функціональних викликів, виконуваних функцією батута. Кожна функція називається громом і повертає наступний крок у обчисленні до тих пір, поки програма не припиниться (порожнє продовження).

Ось перший фрагмент коду, який я написав, щоб поліпшити своє розуміння батуту:

#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 !

7

Ось приклад вкладених функцій:

#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 покажчик функції - це лише покажчик.


0

Для 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");

Редагувати: Більше езотеричних батутів буде неявно породжено компілятором. Одним із таких застосувань буде таблиця стрибків. (Хоча явно є складніші, чим далі ви починаєте намагатися створити складний код.)


0

Тепер, коли 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());. Ця локальна функція рекурсивно обходить масив рулонів. Локальна функція (батут) дозволяє обхід починатися з двох додаткових значень: починати з frame1 і rsf(результат поки що) 0.

У межах локальної функції є потрійний оператор, який обробляє п'ять випадків:

  • Гра закінчується в кадрі 11: поверніть результат поки що
  • Гра закінчується, якщо немає більше рулонів: поверніть результат поки що
  • Удар: обчисліть кількість кадрів і продовжуйте обхід
  • Запасний: обчисліть кількість кадрів і продовжуйте обхід
  • Нормальна оцінка: обчисліть бал кадру та продовжуйте обхід

Продовження обходу здійснюється за допомогою повторного виклику батута, але тепер із оновленими значеннями.

Для отримання додаткової інформації шукайте: " хвостовий акумулятор рекурсії ". Майте на увазі, що компілятор не оптимізує рекурсію хвоста. Настільки елегантним, яким би не було це рішення, воно, швидше за все, буде не голодувати.


-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
ви можете додати коментарі чи пояснення, чому це батут?
prasun
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.