Chức năng bạt lò xo là gì?


93

Trong các cuộc thảo luận gần đây tại nơi làm việc, ai đó đã đề cập đến chức năng tấm bạt lò xo.

Tôi đã đọc mô tả tại Wikipedia . Nó là đủ để đưa ra một ý tưởng chung về chức năng, nhưng tôi muốn một cái gì đó cụ thể hơn một chút.

Bạn có một đoạn mã đơn giản để minh họa một tấm bạt lò xo không?


2
Trong thế giới của Microsoft, những kẻ lang thang thường được gọi là 'côn đồ'. [Đây là một trang] [1] từ "Thiết kế C ++ hiện đại" của Andrei Alexandrescu ---- [1]: books.google.com/…
Michael Burr


Về cơ bản, nó là dạng tổng quát của một số chức năng mà bạn có thể triển khai với setjmp / lomgjmp, cụ thể là để tránh ngăn xếp ovwerflow.
Ingo

12
tại sao mọi người lại muốn tránh stackoverflow?
Nikole

Câu trả lời:


72

Ngoài ra còn có cảm giác LISP về 'tấm bạt lò xo' như được mô tả trên Wikipedia:

Được sử dụng trong một số triển khai LISP, tấm bạt lò xo là một vòng lặp gọi lặp đi lặp lại các hàm thunk-return. Một tấm bạt lò xo duy nhất là đủ để thể hiện tất cả các chuyển điều khiển của một chương trình; một chương trình được thể hiện như vậy được trampolined hoặc theo "kiểu trampolined"; chuyển đổi một chương trình sang kiểu trampolined là trampolining. Các hàm trampolined có thể được sử dụng để thực hiện các lệnh gọi hàm đệ quy đuôi trong các ngôn ngữ hướng ngăn xếp

Giả sử chúng tôi đang sử dụng Javascript và muốn viết hàm Fibonacci ngây thơ theo kiểu tiếp diễn. Lý do chúng tôi làm điều này không liên quan - chẳng hạn như chuyển Đề án sang JS hoặc để chơi với CPS mà chúng tôi vẫn phải sử dụng để gọi các hàm phía máy chủ.

Vì vậy, nỗ lực đầu tiên là

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

Tuy nhiên, chạy điều này với n = 25trong Firefox sẽ xuất hiện lỗi 'Quá nhiều đệ quy!'. Bây giờ đây chính xác là vấn đề (thiếu tối ưu hóa cuộc gọi đuôi trong Javascript) mà trampolining giải quyết. Thay vì thực hiện một cuộc gọi (đệ quy) đến một hàm, hãy để chúng tôi returnmột lệnh (thunk) để gọi hàm đó, được diễn giải trong một vòng lặp.

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

Hãy để tôi thêm một vài ví dụ cho hàm giai thừa được triển khai với trampolines, bằng các ngôn ngữ khác nhau:

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 (không may mắn nếu không thực hiện các số lớn):

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

Lời giải thích của bạn, đặc biệt là ví dụ C, cũng như câu trả lời của ephemient bên dưới về các hàm lồng nhau cuối cùng đã khiến tôi hiểu về trampolines. Một loại hàm trợ giúp có thể được sử dụng để cập nhật trạng thái giống như một hàm đóng.
Byte

Mã Scala nên được sửa thành if (n < 2) Done(product), SO không cho phép tôi chỉnh sửa 1 biểu tượng ...
Tối đa

21

Tôi sẽ cung cấp cho bạn một ví dụ mà tôi đã sử dụng trong bản vá chống gian lận cho một trò chơi trực tuyến.

Tôi cần có thể quét tất cả các tệp đang được tải bởi trò chơi để sửa đổi. Vì vậy, cách mạnh mẽ nhất mà tôi tìm thấy để làm điều này là sử dụng một tấm bạt lò xo cho CreateFileA. Vì vậy, khi trò chơi được khởi chạy, tôi sẽ tìm địa chỉ cho CreateFileA bằng GetProcAddress, sau đó tôi sẽ sửa đổi một vài byte đầu tiên của hàm và chèn mã lắp ráp sẽ chuyển đến hàm "trampoline" của riêng tôi, nơi tôi sẽ làm một số việc và sau đó tôi sẽ quay lại vị trí tiếp theo trong CreateFile sau mã jmp của tôi. Để có thể làm điều đó một cách đáng tin cậy thì phức tạp hơn một chút, nhưng khái niệm cơ bản là chỉ nối một chức năng, buộc nó chuyển hướng đến một chức năng khác, và sau đó quay trở lại chức năng ban đầu.

Chỉnh sửa: Microsoft có một khuôn khổ cho loại thứ này mà bạn có thể xem qua. Đường vòng đã gọi


8

Tôi hiện đang thử nghiệm các cách để thực hiện tối ưu hóa cuộc gọi đuôi cho trình thông dịch Đề án và vì vậy, hiện tại tôi đang cố gắng tìm hiểu xem liệu tấm bạt lò xo có khả thi đối với tôi hay không.

Theo tôi hiểu, về cơ bản nó chỉ là một chuỗi các lệnh gọi hàm được thực hiện bởi một hàm bạt lò xo. Mỗi hàm được gọi là một thunk và trả về bước tiếp theo trong quá trình tính toán cho đến khi chương trình kết thúc (phần tiếp theo trống).

Đây là đoạn mã đầu tiên mà tôi đã viết để nâng cao hiểu biết của tôi về tấm bạt lò xo:

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

kết quả trong:

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

7

Dưới đây là một ví dụ về các hàm lồng nhau:

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

comparkhông thể là một hàm bên ngoài, vì nó sử dụng nbytes, chỉ tồn tại trong khi sort_bytesgọi. Trên một số kiến ​​trúc, một hàm sơ khai nhỏ - tấm bạt lò xo - được tạo trong thời gian chạy và chứa vị trí ngăn xếp của lệnh gọi hiện tạisort_bytes . Khi được gọi, nó sẽ nhảy đến comparmã, chuyển địa chỉ đó.

Sự lộn xộn này không bắt buộc trên các kiến ​​trúc như PowerPC, nơi ABI chỉ định rằng một con trỏ hàm thực sự là một "con trỏ béo", một cấu trúc chứa cả một con trỏ tới mã thực thi và một con trỏ khác tới dữ liệu. Tuy nhiên, trên x86, một con trỏ hàm chỉ là một con trỏ.


0

Đối với C, một tấm bạt lò xo sẽ ​​là một con trỏ hàm:

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

Chỉnh sửa: Nhiều trampolines bí truyền hơn sẽ được trình biên dịch ngầm tạo ra. Một trong những cách sử dụng như vậy sẽ là một bảng nhảy. (Mặc dù rõ ràng có những cái phức tạp hơn nếu bạn bắt đầu cố gắng tạo mã phức tạp.)


0

Bây giờ C # đã có Chức năng cục bộ , kata mã hóa Trò chơi Bowling có thể được giải quyết một cách dễ dàng với tấm bạt lò xo:

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

Phương pháp Game.RollManynày được gọi với một số cuộn: thường là 20 cuộn nếu không có phụ tùng hoặc đình công.

Dòng đầu tiên ngay lập tức gọi hàm trampoline: return Trampoline(1, 0, rs.ToList());. Hàm cục bộ này duyệt đệ quy mảng cuộn. Chức năng cục bộ (tấm bạt lò xo) cho phép quá trình di chuyển bắt đầu với hai giá trị bổ sung: bắt đầu bằng frame1 và rsf(kết quả cho đến nay) 0.

Trong hàm cục bộ có toán tử bậc ba xử lý năm trường hợp:

  • Trò chơi kết thúc ở khung 11: trả về kết quả cho đến nay
  • Trò chơi kết thúc nếu không còn cuộn nào nữa: trả về kết quả cho đến nay
  • Strike: tính toán điểm số khung hình và tiếp tục truyền tải
  • Phụ tùng: tính toán điểm số khung hình và tiếp tục truyền tải
  • Điểm bình thường: tính điểm khung hình và tiếp tục truyền tải

Tiếp tục đi ngang được thực hiện bằng cách gọi lại tấm bạt lò xo, nhưng bây giờ với các giá trị được cập nhật.

Để biết thêm thông tin, hãy tìm kiếm: " bộ tích lũy đệ quy đuôi ". Hãy nhớ rằng trình biên dịch không tối ưu hóa đệ quy đuôi. Vì vậy, giải pháp này thanh lịch như vậy có thể sẽ không phải là nhanh.


-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
bạn có thể thêm bất kỳ nhận xét hoặc giải thích tại sao đây là một tấm bạt lò xo?
prasun
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.