C语言中有哪些用于函数式编程的工具?

166

最近我一直在思考如何在 C 中进行函数式编程 (不是 C++)。显然,C 是一种过程式语言,原生并不支持函数式编程。

是否有任何编译器/语言扩展可以向语言中添加一些函数式编程结构?GCC 提供嵌套函数作为一种语言扩展;嵌套函数可以访问父堆栈帧中的变量,但这仍远远不足以成熟闭包。

例如,我认为在 C 中非常有用的一件事是,在任何期望函数指针的地方,您都可以传递 Lambda 表达式,创建一个衰减为函数指针的闭包。C++0x 将包括 Lambda 表达式(我认为这很棒);但是,我正在寻找适用于纯 C 的工具。

[编辑] 澄清一下,我并不是想解决在 C 中更适合函数式编程的特定问题;我只是好奇有哪些工具可用于此。


4
为何不使用提供所需功能的语言,而是寻找非标准扩展和技巧试图将C转变为它本来不是的样子呢? - Robert Gamble
请参阅相关问题:https://dev59.com/nHVD5IYBdhLWcg3wTJvF - Andy Brice
@AndyBrice 那个问题涉及到另一种语言,实际上似乎没有意义(C++没有与之相同的“生态系统”概念)。 - Kyle Strand
13个回答

98

你可以使用GCC的嵌套函数来模拟Lambda表达式,事实上我有一个宏可以帮助我完成这个过程:

#define lambda(return_type, function_body) \
  ({ \
    return_type anon_func_name_ function_body \
    anon_func_name_; \
  })

使用方法如下:

int (*max)(int, int) = lambda (int, (int x, int y) { return x > y ? x : y; });

14
这真的很酷。似乎__fn__只是一个在块({ })内定义的函数的任意名称,而不是某个GCC扩展或预定义的宏?选择__fn__作为名称(看起来非常像GCC的定义)让我感到困惑,并且查找GCC文档没有任何好的效果。 - FooF
2
遗憾的是,这在实践中不起作用:如果您想让闭包捕获任何内容,它需要一个可执行堆栈,这是一种可怕的安全实践。就我个人而言,我认为这根本没有用处。 - Demi
1
这并不是特别有用,但确实很有趣。这就是为什么其他答案被接受的原因。 - Joe D
很有趣,但毫无用处。 - rokstar

91

函数式编程不是关于lambda,而是全部关于纯函数。因此,以下广泛推广函数式风格:

  1. 只使用函数参数,不使用全局状态。

  2. 尽量减少副作用,例如printf或任何IO。在所有函数中,返回描述IO的数据,可以执行这些数据,而不是直接导致副作用。

这可以在普通C语言中实现,无需魔法。


8
我认为你已经把函数式编程的极端观点夸大到了荒谬的地步。例如,纯粹的map规范有什么用处,如果没有传递函数的工具呢? - Jonathan Leonard
39
我认为这种方法比使用宏在C语言内创建函数式语言更加实用。适当地使用纯函数比使用map替代for循环更有可能改进一个系统。 - Andy Till
10
你肯定知道地图只是起点。如果没有一流函数,任何程序(即使所有函数都是“纯”的)都会比具有一流函数的等效程序低级(从信息论意义上讲)。在我看来,这种声明式风格只有通过一流函数才能实现,这是FP的主要好处。如果暗示缺乏一流函数导致的冗长性是FP提供的全部,那么你可能会误导某些人。需要注意的是,历史上,“FP”一词更多地暗示了一流函数而非纯度。 - Jonathan Leonard
1
我并不反对一级函数,我每天都在使用它们,而且离开它们就无法工作。但是在 C 语言中从未使用过,祝你好运。 - Andy Till
3
Andy Till的答案表现出一种非常实用主义的观点。在C语言中采用"纯洁"的编程风格不需要任何花哨的东西,而且带来了很大的好处。相比之下,一等函数只是“舒适生活”的体现,虽然方便,但采用纯洁的编程风格更加实用。我想知道为什么没有人将尾调用与所有这些相关联进行讨论。那些想要一等函数的人也应该要求尾调用。 - BitTickler
显示剩余2条评论

43

FFCALL可以让您在C语言中构建闭包 -- callback = alloc_callback(&function, data)返回一个函数指针,使得callback(arg1, ...)等同于调用function(data, arg1, ...)。然而,您需要手动处理垃圾回收。

相关地,块(blocks)已经被添加到苹果的GCC分支中;它们不是函数指针,但它们允许您传递lambda表达式,同时避免手动构建和释放捕获变量的存储空间(在一些语法糖和运行时库的帮助下,实际上进行了一些复制和引用计数)。


20

3
这本书与问题相关的任何功能方面都没有尝试。 - Eugene Tolmachev
7
看起来你是正确的。实际上,前言中明确说明了这本书的目的是在学生已经熟悉函数式编程之后教授命令式编程。顺便说一下,这是我在SO上的第一个答案,当时我只能写成答案形式,因为我没有足够的声望来评论之前带有烂链接的答案。我一直想知道为什么人们还会给这个答案点赞...请不要这样做! :-) - FooF
1
也许这本书应该被更好地称为后函数式编程(首先因为“命令式C”听起来不够性感,其次因为它假设读者熟悉函数式编程范例,第三因为它似乎是标准和正确功能的衰落,或许第四是指Larry Wall关于后现代编程的概念 - 尽管这本书是在Larry Wall的文章/演示之前写的)。 - FooF
这是一个重复的答案。 - PhilT

12

函数式编程风格的前提是要有一等公民函数。如果你能接受以下内容,那么它可以在可移植的C中模拟:

  • 手动管理词法作用域绑定,也就是闭包。
  • 手动管理函数变量的生命周期。
  • 使用不同的语法进行函数应用/调用。
/* 
 * with constraints desribed above we could have
 * good approximation of FP style in plain C
 */

int increment_int(int x) {
  return x + 1;
}

WRAP_PLAIN_FUNCTION_TO_FIRST_CLASS(increment, increment_int);

map(increment, list(number(0), number(1)); // --> list(1, 2)


/* composition of first class function is also possible */

function_t* computation = compose(
  increment,
  increment,
  increment
);

*(int*) call(computation, number(1)) == 4;

这样的代码可能运行时间很短,就像下面这个例子

struct list_t {
  void* head;
  struct list_t* tail;
};

struct function_t {
   void* (*thunk)(list_t*);
   struct list_t* arguments;
}

void* apply(struct function_t* fn, struct list_t* arguments) {
  return fn->thunk(concat(fn->arguments, arguments));
}

/* expansion of WRAP_PLAIN_FUNCTION_TO_FIRST_CLASS */
void* increment_thunk(struct list_t* arguments) {
  int x_arg = *(int*) arguments->head;
  int value = increment_int(x_arg);
  int* number = malloc(sizeof *number);

  return number ? (*number = value, number) : NULL;
}

struct function_t* increment = &(struct function_t) {
  increment_thunk,
  NULL
};

/* call(increment, number(1)) expands to */
apply(increment, &(struct list_t) { number(1), NULL });

本质上,我们使用由功能/参数对以及一堆宏表示的闭包来模拟一等函数。完整的代码可以在这里找到。


10
主要想到的是使用代码生成器。你愿意用提供函数式编程的其他语言编写程序,然后从中生成C代码吗?
如果这不是一个有吸引力的选择,则可以滥用CPP来达到部分目的。宏系统应该让您模拟一些函数式编程的思想。我听说过gcc就是这样实现的,但我从未核实过。
当然,C可以使用函数指针传递函数,主要问题是缺乏闭包和类型系统往往会妨碍。您可以探索比CPP更强大的宏系统,例如M4。我想最终我的建议是真正的C不能轻易胜任此任务,但您可以扩展C使其能够胜任此任务。如果使用CPP,则扩展将看起来最像C,或者您可以从其他语言生成C代码。

1
这是最诚实的回答。试图以函数式的方式编写C代码就是在与语言设计和结构本身作斗争。 - josiah

6
我在C语言中进行函数式编程的方法是编写一个C语言的函数式语言解释器,我将它命名为Fexl,这是“Function Expression Language”的缩写。
该解释器非常小,启用-O3优化后,在我的系统上编译出来只有68K。这并不是一个玩具,我在为我的业务(投资合伙企业的基于Web的会计)编写新的生产代码时都在使用它。
现在我只使用C代码来进行以下两种操作:(1)添加一个调用系统例程(例如fork、exec、setrlimit等)的内置函数,或者(2)优化本可以使用Fexl编写的函数(例如搜索子字符串)。
该模块机制基于“上下文”的概念。上下文是一个函数(用Fexl编写),它将符号映射到其定义。当您读取Fexl文件时,可以将其与任何您喜欢的上下文一起解析。这允许您创建自定义环境,或在受限制的“沙盒”中运行代码。 http://fexl.com

5
如果您想实现闭包,您需要熟悉汇编语言和堆栈的切换/管理。并不是建议反对这种方法,只是说您必须这样做。
不确定您将如何处理C中的匿名函数。在冯·诺伊曼机器上,您可以在汇编语言中使用匿名函数。

4

2

Felix语言可以编译成C++。如果你不介意使用C++,那么这可能是一个好的起点。


11
因为α)作者明确提到「不是C++」,β)由编译器生成的C++不会是「人可读的C++」,γ)C++标准支持函数式编程,无需使用从一种语言到另一种语言的编译器。 - Hi-Angel
1
一旦你通过宏或类似的方式提出了一种功能性语言,就自然地意味着你不太关心C语言。这个答案是有效的和可以接受的,因为即使C++最初也是在C的基础上进行宏编程的。如果你走得足够远,最终会得到一种新的语言。那么问题只剩下后端讨论了。 - BitTickler

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接