1

我对基于模板或类型的动态推导而实现的泛型编程方法是有看法的,这些看法可能有些极端。

我觉得无论是模板还是类型的动态推导,基于它们只能建立面向数据的泛型编程范式,而更好的泛型编程范式应该是面向运算。

例如下面这个 C++ 模板函数:

template<typename T>
T const & max_element(T const *array, unsigned int n) {
        T const *max_value = array;
        for (unsigned int i = 1; i < n; i++) {
                if (array[i] > *max_value) {
                        max_value = &(array[i]);
                }
        }
        return *max_value;
}

虽然它的理想是浪漫的,从所有类型的值构成的数组中获取最大值,但这种理想就类似于古人的天圆地方的地理观,更多的是来自直觉,而非逻辑论证。

在代码中通过『占位符』的形式来实现泛型,这需要一个前提,那就是所『泛』的『型』要支持既定的运算。在 max_element 中,就是所『泛』的 T 类型,它必须能够参与 > 运算。显然,对于很多非数字形式的数据类型而言,它们是不支持 '>' 运算的,所以为了 max_element 能够工作下去,max_element 的用户必须去对 > 运算进行一些扩展,让它能够接受它原本无法接受的数据类型的值。所以,模板不仅没有解决泛型的根本问题,反而对这个问题进行了一些不必要的『掩盖』。

动态语言中的『泛型』也存在类似的问题。例如下面的 python 版本的 max_element 函数:

def max_element(array):
    max_value = array[0]
    for e in array[1:]:
        if e > max_value: max_value = e
    return max_value

在 Python 中,array 中的元素可以是任意类型,并且 Python 的解释器能够识别出这些类型。动态类型,但是这些功能仅仅是消除了 C++ 中的类型占位符,让语法简洁了一些,对于不支持 > 运算的数据类型而言,max_element 的用户依然要想办法让自己所处里的数据能够支持 > 运算。

面向数据的泛型编程范式只能让一些简单类型的数据具备泛型运算能力,而对于现实中的大部分数据其泛型运算过程依然要交付给用户去实现。既然如此,那又何必去绕模板、类型推导以及运算符重载这个圈子?干脆将某种运算所涉及的数据类型都交给用户来处理就是了,这样不仅能够消减不必要的语法,同时也大幅避免了繁琐复杂的心智模型。

我觉得更好的泛型编程范式,应该像古老的 C 语言版本的 qsort 函数这样:

void qsort(void *base, size_t nmemb, size_t size,
                  int (*compar)(const void *, const void *));

也许正是因为 C 语言在语言层面上不支持模板与运行时数据类型识别,所以 qsort 函数在设计上不必绕圈子,直接告诉用户:如果你想对存放在 base 数组里的数据进行快速排序,那么你必须告诉我应当如何对这个数组里的任意两个元素进行大小比较。

按照 qsort 这种风格,那么 C 版本的 max_element 可以像下面这样设计:

void * max_element(void *base, size_t size,
                  int (*gt)(const void *, const void *));

gt 是一个函数指针,它指向用户为自己所处理的数据类型而定义的 > 运算函数。

也许很多人会抱怨,难道像 intfloatdouble 这样天生就能够参与 > 运算的数据类型也需要用户自行定义 gt 函数嘛?理论上必须是这样,虽然在现实中,qsortmax_element 的设计者如果想减轻用户的负担,他可以为常用的数据类型预定义相应的 compargt。既然你选择的是一个支持泛型的 qsortmax_element 运算,你就不应该去假设这些运算『已经』支持了哪些数据类型。这就类似于,如果你选择了借助法律来治理社会,那么你就必须遵守现行的法律。

如果假设 qsortmax_element 已经支持了某些数据类型,从某种意义上来说,它就不再是泛型的了。譬如,C++ 版本的 max_elment 假设 > 运算能够直接处理基本的数字类型,例如 2 > 1。如果我用 2 表示二等奖,用 1 表示一等奖,那么 2 > 1 还能成立么?在这种情况下,我还是需要对 int 这样的数据类型进行 > 运算的重载,然而我的重载很有可能让代码的逻辑变得更加混乱,毕竟大部分情况下 2 是要大于 1 的……

现实中,人类已经基于 C++ 建立了许多庞大的库,特别是在数值运算、几何运算以及图形图像处理领域,例如 CGAL, PCL, OpenSceneGraph, OpenCV 等 C++ 库,它们不约而同的走上了面向数据的泛型道路,代码中充满了 C++ 模板的奇技淫巧……C++ Boost 库或许是模板的奇技淫巧的集大成者。

一个库,如果设计者真的希望它简单好用并且又能支持泛型运算,那么他首先应该去思考这个库所涉及的基本运算有哪些,这些基本运算中又有那些是与数据类型相关的,然后将这部分与数据类型相关的运算抽离出来,交给库的用户来填充。库的用户填充具体的运算的过程,就类似于在一个窗口程序中为鼠标点击按钮事件而增加处理函数……

qsort 那样基于 C 的函数指针所实现的东西,叫做『回调函数』,即用户调用了 qsort 函数,而 qsort 又调用了用户自己编写的一个函数。这是解决所有泛型运算问题的根本之道。

仔细观察 qsort 函数的 compar 参数,假设这个函数指针指向我编写的回调函数 str_compare。下面是一个完整的 qsort 用法示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
static int
str_compare (const void *s1, const void *s2)
{
        char *str1 = *(char **)s1;
        char *str2 = *(char **)s2;       
        size_t l1 = strlen (str1);
        size_t l2 = strlen (str2);
 
        if (l1 > l2)
                return 1;
        else if (l1 == l2)
                return 0;
        else
                return -1;
}
 
int
main (void)
{
        char *str_array[5] = {"a", "abcd", "abc", "ab", "abcde"};
        qsort (str_array, 5, sizeof (char *), str_compare);
 
        for (int i = 0; i< 5; i++)
                printf ("%s ", str_array[i]);
        printf ("\n");
         
        return 0;
}

str_compare 函数所处的环境有点玄妙。如果你将自己想象为 str_compare 函数,你所接受的东西来自其他人。从你自身的角度来看,没有这些来自其他人的东西,你的存在就是没有意义的。从传递给你这些东西的人的角度来看,没有你的存在,这些值的存在也是没有意义的。无论从哪个角度来看,你与你所接受的东西都是不可分割的,也就是说你们构成了一个单元。这个单元就是所谓的『闭包』。

在 C 语言中,即使 main 函数这样的程序入口函数,它也需要外部传入 argcargv,所以 main 函数与 argc 以及 argv 也构成了闭包。

只要是比汇编语言高级一点的编程语言,在运算逻辑上,闭包是无处不在的,它与函数指针或者回调函数没有什么必然的联系。也就是说,闭包只是个概念,只要你一个计算过程需要依赖它的运行环境所传入的数据,并且它也向这个环境输出数据,那么这个运算过程就是『封闭』的,它与这个环境就构成了闭包。

因为闭包这种现象普遍存在,所以有些编程语言在设计上是将闭包作为一个真正的单元而实现的,也就是说这些语言在语法层面上支持闭包,就像 C++ 在语法层面支持模板那样。此类在语法层面上支持闭包的语言,通常被称为『函数式语言』,因为它们支持闭包,所以它们能够将函数作为值传递与返回。

我们每一个人,都像是一个闭包。所以,我们所生存的这个世界就具备了泛型运算的能力!


garfileo
6k 声望1.9k 粉丝

这里可能不会再更新了。


引用和评论

1 篇内容引用
0 条评论