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


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.