【问题标题】:Functional Programming (Currying) in C / Issue with TypesC 中的函数式编程(柯里化)/类型问题
【发布时间】:2012-10-11 01:22:59
【问题描述】:

作为一个彻头彻尾的函数式程序员,我发现很难不尝试将我最喜欢的范式硬塞进我正在使用的任何语言中。在编写一些 C 语言时,我发现我想 curry 我的一个函数,然后传递部分应用的函数。在阅读Is there a way to do currying in C? 并注意http://gcc.gnu.org/onlinedocs/gcc/Nested-Functions.html#Nested-Functions 的警告后,我想出了:

#include <stdio.h>

typedef int (*function) (int);

function g (int a) {
    int f (int b) {
        return a+b;
    }
    return f;
}

int f1(function f){
    return f(1);}

int main () {
    printf ("(g(2))(1)=%d\n",f1(g(2)));
}

按预期运行。但是,我的原始程序适用于doubles,所以我想我只需更改适当的类型就可以了:

#include <stdio.h>

typedef double (*function) (double);

function g (double a) {
    double f (double b) {
        return a+b;
    }
    return f;
}

double f1(function f){
    return f(1);}

int main () {
    printf ("(g(2))(1)=%e\n",f1(g(2)));
}

这会产生如下内容:

bash-3.2$ ./a.out 
Segmentation fault: 11
bash-3.2$ ./a.out 
Illegal instruction: 4

错误的选择似乎是随机的。此外,如果任一示例使用-O3 编译,则编译器本身会抛出Segmentation fault: 11。我在任何时候都没有收到来自 gcc 的警告,而且我无法弄清楚发生了什么。有谁知道为什么第二个程序失败而第一个程序没有?或者更好的是如何修复第二个?

我的 gcc 是 i686-apple-darwin11-llvm-gcc-4.2 (GCC) 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.11.00),我的内核是 Darwin Kernel Version 12.1.0: Tue Aug 14 13:29:55 PDT 2012; root:xnu-2050.9.2~1/RELEASE_X86_64

编辑: 明确地说,我知道我正在尝试做的事情是愚蠢的。这段代码不会在好奇号火星车或纽约证券交易所运行。我试图更多地了解(GNU)C 中的函数指针是如何工作的,并解释我发现的一些有趣的东西。我保证永远不会在现实世界中做这样的事情。

【问题讨论】:

  • 引用您链接的手册:“如果您在包含函数退出后尝试通过其地址调用嵌套函数,那么一切都会崩溃。”
  • @Joulukuusi 如果我没有让它与ints 一起工作,那足以阻止我。
  • 嗯,它在我的 PC 上并不能真正工作:它在没有优化的情况下编译时只打印 2,但在使用 -O3 编译时打印 3。这个因分段错误而退出:ideone.com/rQiFT2
  • 我猜你用你的 int 东西击中了“你可能很幸运”的部分。由于 C 没有原生的closures,所以你不能用这种方式进行柯里化。
  • @paulsm4 真的。我正在用 C 语言编写一些东西,并且正在考虑表达它的最佳方式。我认为一个 curried 函数会起作用,但在我开始编写 C 语言后,我几乎立即意识到 C 不是为这类事情而设计的。但是我还是很好奇所以就来了。

标签: c gcc types functional-programming function-pointers


【解决方案1】:

你试图依赖未定义的行为:一旦内部函数因为外部函数退出而超出范围,通过某个指针调用该内部函数的行为是未定义的。任何事情都可能发生。事情意外地适用于整数情况的事实并不意味着您可以期望 double 相同,或者您甚至可以期望 int 在不同的编译器、不同的编译器版本、不同的编译器标志或不同的目标体系结构上相同。

所以不要依赖未定义的行为。不要声称自己“注意[ed] the warnings”,而实际上您是违反了这些警告的。警告明确指出:

如果你试图在包含函数退出后通过它的地址调用嵌套函数,所有的地狱都会崩溃。

C 中没有closures,所以在这个意义上不可能有柯里化。如果您将一些数据传递给函数调用,您可以获得类似的效果,但这看起来与正常的函数调用完全不同,因此不会感觉像正常的柯里化。 C++ 在那里具有更大的灵活性,因为它允许对象在语法上表现得像函数。在 C++ 世界中,柯里化通常被称为函数参数的“binding”。

如果您真的想知道为什么一段代码可以工作,而另一段代码失败,您可以获取汇编代码(例如由gcc -S -fverbose-asm 生成)并在您的脑海中模拟执行,看看您的数据会发生什么情况,然后东西。或者您可以使用调试器查看失败的地方,或数据位置的变化。可能需要一些工作,我怀疑是否值得花时间。

【讨论】:

  • 感谢您的回答。我知道这很疯狂,我已经更新了问题以反映这一点。
【解决方案2】:

一个有趣的问题,我查看了引用的答案中的论文 (More functional reusability in C/C++/Objective-C with Curried functions)。

因此,以下是您可能想去的建议路径。我不认为这真的是一个 Curried 函数,因为我不完全理解论文在说什么,而不是一个函数式程序员。然而,做一些工作,我发现这个概念的一些有趣的应用。另一方面,我不确定这是否是您想要的,您可以在 C 中做到这一点让我大吃一惊。

似乎有两个问题。

首先是能够处理带有任意参数列表的任意函数调用。我采用的方法是使用标准 C 库变量参数功能(带有 va_start()、va_arg() 和 va_end() 函数的 va_list),然后将函数指针与提供的参数一起存储到数据区域中,以便它们然后可以在以后执行。我借用并修改了printf() 函数如何使用格式行来了解提供了多少个参数及其类型。

接下来是函数及其参数列表的存储。我只是使用了一个任意大小的结构来尝试这个概念。这需要更多的思考。

这个特定版本使用一个被视为堆栈的数组。有一个函数可以用来将一些任意函数及其参数推送到堆栈数组中,还有一个函数可以将最顶层的函数及其参数从堆栈数组中弹出并执行。

但是,您实际上可以在某种集合中包含任意结构对象,例如哈希映射,这可能非常酷。

我只是借用了论文中的信号处理程序示例来表明该概念适用于那种应用程序。

所以这里是源代码,我希望它可以帮助你想出一个解决方案。

您需要向开关添加其他情况,以便能够处理其他参数类型。我只是做了一些概念证明。

虽然这似乎是一个相当简单的扩展,但它也不会调用函数。就像我说的,我不完全理解这个 Curried 的东西。

#include <stdarg.h>
#include <string.h>

// a struct which describes the function and its argument list.
typedef struct {
    void (*f1)(...);
    // we have to have a struct here because when we call the function,
    // we will just pass the struct so that the argument list gets pushed
    // on the stack.
    struct {
        unsigned char myArgListArray[48];   // area for the argument list.  this is just an arbitray size.
    } myArgList;
} AnArgListEntry;

// these are used for simulating a stack.  when functions are processed
// we will just push them onto the stack and later on we will pop them
// off so as to run them.
static unsigned int  myFunctionStackIndex = 0;
static AnArgListEntry myFunctionStack[1000];

// this function pushes a function and its arguments onto the stack.
void pushFunction (void (*f1)(...), char *pcDescrip, ...)
{
    char *pStart = pcDescrip;
    AnArgListEntry MyArgList;
    unsigned char *pmyArgList;
    va_list argp;
    int     i;
    char    c;
    char   *s;
    void   *p;

    va_start(argp, pcDescrip);

    pmyArgList = (unsigned char *)&MyArgList.myArgList;
    MyArgList.f1 = f1;
    for ( ; *pStart; pStart++) {
        switch (*pStart) {
            case 'i':
                // integer argument
                i = va_arg(argp, int);
                memcpy (pmyArgList, &i, sizeof(int));
                pmyArgList += sizeof(int);
                break;
            case 'c':
                // character argument
                c = va_arg(argp, char);
                memcpy (pmyArgList, &c, sizeof(char));
                pmyArgList += sizeof(char);
                break;
            case 's':
                // string argument
                s = va_arg(argp, char *);
                memcpy (pmyArgList, &s, sizeof(char *));
                pmyArgList += sizeof(char *);
                break;
            case 'p':
                // void pointer (any arbitray pointer) argument
                p = va_arg(argp, void *);
                memcpy (pmyArgList, &p, sizeof(void *));
                pmyArgList += sizeof(void *);
                break;
            default:
                break;
        }
    }
    va_end(argp);
    myFunctionStack[myFunctionStackIndex] = MyArgList;
    myFunctionStackIndex++;
}

// this function will pop the function and its argument list off the top
// of the stack and execute it.
void doFuncAndPop () {
    if (myFunctionStackIndex > 0) {
        myFunctionStackIndex--;
        myFunctionStack[myFunctionStackIndex].f1 (myFunctionStack[myFunctionStackIndex].myArgList);
    }
}

// the following are just a couple of arbitray test functions.
// these can be used to test that the functionality works.
void myFunc (int i, char * p)
{
    printf (" i = %d, char = %s\n", i, p);
}

void otherFunc (int i, char * p, char *p2)
{
    printf (" i = %d, char = %s, char =%s\n", i, p, p2);
}

void mySignal (int sig, void (*f)(void))
{
    f();
}

int main(int argc, char * argv[])
{
    int i = 3;
    char *p = "string";
    char *p2 = "string 2";

    // push two different functions on to our stack to save them
    // for execution later.
    pushFunction ((void (*)(...))myFunc, "is", i, p);
    pushFunction ((void (*)(...))otherFunc, "iss", i, p, p2);

    // pop the function that is on the top of the stack and execute it.
    doFuncAndPop();

    // call a function that wants a function so that it will execute
    // the current function with its argument lists that is on top of the stack.
    mySignal (1, doFuncAndPop);

    return 0;
}

您还可以从中获得一点乐趣,即在由doFuncAndPop() 调用的函数中使用pushFunction() 函数,以使您可以将另一个函数及其参数放入堆栈。

例如,如果您将上面源代码中的函数 otherFunc() 修改为如下所示:

void otherFunc (int i, char * p, char *p2)
{
    printf (" i = %d, char = %s, char =%s\n", i, p, p2);
    pushFunction ((void (*)(...))myFunc, "is", i+2, p);
}

如果您随后添加另一个对doFuncAndPop() 的调用,您将看到首先执行otherFunc(),然后执行在otherFunc() 中的对myFunc() 的调用,最后是被推送的myFunc() 调用在main () 中被调用。

编辑 2: 如果我们添加以下函数,这将执行所有已放入堆栈的函数。这将允许我们通过将函数和参数压入堆栈然后执行一系列函数调用来创建一个小程序。这个函数还允许我们推送一个没有任何参数的函数,然后推送一些参数。当从堆栈中弹出函数时,如果参数块没有有效的函数指针,那么我们所做的就是将该参数列表放到堆栈顶部的参数块上,然后执行它。也可以对上面的函数doFuncAndPop() 进行类似的更改。如果我们在一个执行的函数中使用 pushFunction() 操作,我们可以做一些有趣的事情。

实际上这可能是Threaded Interpreter 的基础。

// execute all of the functions that have been pushed onto the stack.
void executeFuncStack () {
    if (myFunctionStackIndex > 0) {
        myFunctionStackIndex--;
        // if this item on the stack has a function pointer then execute it
        if (myFunctionStack[myFunctionStackIndex].f1) {
            myFunctionStack[myFunctionStackIndex].f1 (myFunctionStack[myFunctionStackIndex].myArgList);
        } else if (myFunctionStackIndex > 0) {
            // if there is not a function pointer then assume that this is an argument list
            // for a function that has been pushed on the stack so lets execute the previous
            // pushed function with this argument list.
            int myPrevIndex = myFunctionStackIndex - 1;
            myFunctionStack[myPrevIndex].myArgList = myFunctionStack[myFunctionStackIndex].myArgList;
        }
        executeFuncStack();
    }
}

编辑 3: 然后我们对pushFunc() 进行更改以使用以下附加开关处理双精度:

case 'd':
  {
     double d;
     // double argument
     d = va_arg(argp, double);
     memcpy (pmyArgList, &d, sizeof(double));
     pmyArgList += sizeof(double);
   }
break;

因此,使用这个新功能,我们可以执行以下操作。首先创建类似于原始问题的两个函数。我们将在一个函数中使用 pushFunction() 来推送参数,然后由堆栈上的下一个函数使用。

double f1 (double myDouble)
{
    printf ("f1 myDouble = %f\n", myDouble);
    return 0.0;
}

double g2 (double myDouble) {
    printf ("g2 myDouble = %f\n", myDouble);
    myDouble += 10.0;
    pushFunction (0, "d", myDouble);
    return myDouble;
}

我们将新功能与以下一系列语句一起使用:

double xDouble = 4.5;
pushFunction ((void (*)(...))f1, 0);
pushFunction ((void (*)(...))g2, "d", xDouble);
executeFuncStack();

这些语句将首先执行值为 4.5 的函数 g2(),然后函数 g2() 会将其返回值推送到我们的堆栈中,以供函数 f1() 使用,该函数首先被推送到我们的堆栈中。

【讨论】:

  • 其他有趣的是,您可以使用 pushFunction() 函数将另一个函数调用放入堆栈。请参阅我在底部添加的编辑。
  • @luserdroog,一个附加函数允许执行由函数调用组成的程序。您可以通过将一系列函数调用推送到我们的迷你堆栈上来创建程序,然后调用 executeFuncStack() 来执行一系列函数。我们添加了推送不带参数的函数的功能,然后在单独的步骤中推送函数将使用的参数。
  • @RichardChambers 感谢您的精彩回答。我想我最终可能会尝试在此基础上编写一些东西以获得适当的 C 功能体验。如果我建议这是 Greenspun's Tenth Rule 的一个很好的例子,我希望你不会被冒犯。
  • @SeanD, 太有趣了!一点也不生气。自从几年前做了一个简单的 emacs 宏以来,我一直想知道 lisp 是什么。我第一次也是最后一次使用 lisp。
  • @RichardChambers 我的意思是受到了启发,值得为此投票。我并不介意通知,但我已将其设为收藏夹,因此我会继续查看。不确定我什么时候会使用这种技术,但我会放在后面的口袋里以防万一。 :)
【解决方案3】:

对不起,我没有得到它,但是你为什么不 wrap 而不是 curry,因为无论如何你是在编译时声明函数?柯里化的优点是——或者至少在我看来——你可以在运行时定义一个部分应用的函数,但在这里你没有这样做。还是我错过了什么?

#include <stdio.h>

// Basic function summing its arguments
double g (double a, double b)
{
    return a+b;
}

double f1(double a)
{
    /* "1" can be replaced by a static initialized
       by another function, e.g.
       static double local_b = g(0, 1);
    */
    return g(a, 1);
}

int main () {
    printf ("(g(2))(1)=%f\n", f1(2));
}

【讨论】:

  • Curry 的想法似乎是允许在不修改函数的情况下使用函数。它让我想起了装饰器模式,在该模式中,您向现有对象添加装饰,以便做一些额外的事情或提供额外的灵活性。
  • 你说得对,这是最好的做法,也是我实际使用的。我正在研究 currying 的原因是因为这在 Haskell 中是惯用的,我试图看看这种方法在 C 中能走多远,只是因为它变得非常优雅,尤其是当人们试图做更复杂的时候涉及产生大量功能并传递它们的事情。
  • 您需要用 CURRYABLE 宏包装每个函数,以便在编译时生成该函数的可变参数版本,然后将 curries 列表存储为(指向 fn 的指针,固定参数列表) .还有处理返回值的问题。这可以通过编译器帮助和其他语言来完成;恐怕 C 甚至不能轻易提供“创建新函数”功能,更不用说“通过名称而不是增量位置来识别参数”,这对柯里化的用处有很大帮助。
  • @RichardChambers,是的,并且通过在 C 中包装函数,我们不会修改它。您可以使用包装器在 C 中进行排序装饰,尽管没有命名参数,这会降低事物的优势——您几乎需要为每个不同的函数使用特定的装饰器。
  • 我认为currying over wrapping的主要好处是你可以在运行时使用不同的值反复curry。你不能用一个包装函数来做到这一点,因为你没有地方存储那个额外的参数。
【解决方案4】:

上述代码的固定版本

#include <stdio.h>

typedef double (*function) (double,double);

// Basic function summing its arguments
double g (double a, double b)
{
        return a+b;
}

double f1(function wrapfunc,double a)
{
 /* "1" can be replaced by a static initialized
     by another function, e.g.
     static double local_b = g(0, 1);
 */
 return wrapfunc(a, 1);  
}

int main () {
        printf ("(g(2))(1)=%f\n", f1(g,2));
}

更多关于不同操作函数的示例参数示例

#include<iostream>
#include<cstdio>

using namespace std;

#define N 4

#define LOOP(i) for(i=0; i<N; i++)

#define F(i) ( (int(*)(int,int))opt[i] )
#define FI F(i)
#define FJ F(j)
#define FK F(k)


int add(int a, int b) { return a + b; }

int sub(int a, int b) { return a - b; }

u int mul(int a, int b) { return a * b; }

int div(int a, int b) {
    if (b == 0 || a % b)
        return 2401;
    return a / b;
}

char whichOpt(int index)
{
    if (index == 0) return '+';
    else if (index == 1) return '-';
    else if (index == 2) return '*';
    return '/';
}

void howObtain24(int num[], void *opt[])
{
    int i, j, k, a, b, c, d;
    int ans=0;
    LOOP(i) LOOP(j) LOOP(k)
         LOOP(a) LOOP(b) LOOP(c) LOOP(d)
    {
        if (a == b || a == c || a == d || b == c || b == d || c == d)
            continue;
        if (FI(FJ(FK(num[a], num[b]), num[c]), num[d]) == 24) {
            std::cout << "((" << num[a] << whichOpt(k) << num[b] << ')'
                 << whichOpt(j) << num[c] << ')' << whichOpt(i) << num[d] << endl;
            ans++;
            continue;
        }
        if (FI(FJ(num[a], num[b]), FK(num[c], num[d])) == 24) {
            std::cout << '(' << num[a] << whichOpt(j) << num[b] << ')'
                 << whichOpt(i) << '(' << num[c] << whichOpt(k) << num[d] << ')' << endl;
            ans++;
            continue;
        }
    }
    if(ans==0)
    std::cout << "Non-Answer" << std::endl;
    return;

}

//=======================================================================
int main() {

    int num[N];

    void *opt[N] = { (void *)add, (void *)sub, (void *)mul, (void *)div };

    std::cout << "Input 4 Numbers between 1 and 10\n"
    for (int i = 0; i < N; i++)
        cin >> num[i];

    for (int j = 0; j < N; j++)
        if (num[j] < 1 || num[j] > 10) {
            std::cout << "InCorrect Input\n"

            return 0;
        }
        howObtain24(num, opt);

        return 0;
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-29
    • 1970-01-01
    • 2018-07-25
    • 2018-02-05
    • 1970-01-01
    相关资源
    最近更新 更多