跳转到内容

编程语言导论/代数数据类型

来自维基教科书,开放的书籍,用于开放的世界

本章概述了如何以更函数式的方式实现一些最流行的抽象数据类型。 我们从重要的概念开始 代数数据类型 (ADT)。 它们是在函数式语言中实现数据结构的主要机制。 然后,我们将继续介绍许多流行数据类型(如堆栈和队列)的函数式实现。

代数数据类型

[编辑 | 编辑源代码]

大多数编程语言都采用 类型 的概念。 简而言之,类型就是一个集合。 例如,Java 编程语言中的类型 int 是数字的集合 ,而类型 boolean 只有两个值:truefalse

作为集合,类型可以用数学运算组合起来,以产生新的(也许更复杂的)类型。 例如,类型 int boolean(其中 表示笛卡尔积)是元组的集合,由下式给出

代数数据类型 是一种形式化方法,用于表达这些类型的组合。 在 ML 中,这些类型用关键字 datatype 声明。 例如,要声明一个包含七个元素的类型 weekday,在 ML 中,我们写

 datatype weekday = Monday
                  | Tuesday
                  | Wednesday 
                  | Thursday
                  | Friday
                  | Saturday
                  | Sunday

竖线用于表示并集。 实际上,代数数据类型通常是更简单类型的并集。 这些子集中的每一个都有一个唯一的标签。 在上面的例子中,我们有七个不同的标签。 每个标签都区分了一个单例集。 换句话说,我们的 weekday 类型的基数是七,因为此类型是七个基数为一的类型的并集。 布尔类型可以声明如下

  datatype Boolean = True 
                   | False

布尔值和工作日的并集声明如下

  datatype BoolOrWeek = Day of Weekday
                      | Bool of Boolean;

而它的笛卡尔积可以声明如下

  data BoolAndWeek = BoolWeekPair of Boolean * Week;

代数数据类型具有两个重要的属性。 第一个事实是它们可以进行模式匹配。 模式匹配是一种简单的机制,它允许将函数实现为一系列由声明左侧出现的标签触发的 case。 例如,我们可以写一个函数来测试一个工作日是否属于周末,如下所示

  fun is_weekend Sunday   = True
    | is_weekend Saturday = True
    | is_weekend _        = False

模式从上到下逐个尝试,直到其中一个匹配。 如果没有匹配,ML 运行时将抛出一个异常。 可以使用通配符,就像我们在上面的 _ 中所做的那样。 此模式匹配任何 Weekday。 与数据类型每个子集相关联的标签对于模式匹配至关重要,因为它区分了不同的子集。

代数数据类型的另一个重要属性是它们可以具有递归定义。 这样一来,就可以非常简洁地定义二叉树之类的结构。 下面的例子展示了一个整数键的树。

  datatype tree = Leaf
                | Node of tree * int * tree;

代数数据类型也可以是参数化的,因此依赖于其他类型。 这使我们能够定义更通用的结构,从而提高模块化和重用性。 例如,可以定义一个通用树,如下所示

   datatype 'a tree = Leaf
                    | Node of 'a tree * 'a * 'a tree;

其中 ' 符号用于表示 'a 可以是任何类型。 例如,工作日的树的类型为 weekday tree

代数数据类型可以递归定义的事实提供了很大的灵活性。 在接下来的部分中,我们将展示代数数据类型如何用于实现一些常见数据结构的示例。

不相交并集

[编辑 | 编辑源代码]

正如我们之前所解释的,代数数据类型表示更简单类型的不相交并集。 作为一个新的例子,下面的类型 bunch 描述了两种类型的并集:多态的 'a 类型和多态的列表类型。

datatype 'x bunch = One of 'x
                  | Group of 'x list;

下面的函数 size(类型为 'a bunch -> int)说明了如何使用此类型。 此函数返回 bunch 实例中的元素数量

fun size (One _) = 1
  | size (Group x) = length x;

不相交并集也可以在不支持标记类型的语言中实现。 例如,我们可以用 C 语言实现类型 bunch,但在这种情况下,编译器没有足够的信息来检查程序的正确性

#include <stdio.h>
#include <stdlib.h>

enum bunch_tag {ONE, GROUP};

typedef union {
  int one;
  int* group;
} bunch_element_type;

typedef struct { 
  bunch_element_type bunch_element;
  unsigned tag;
} bunch;

bunch* createOne(int i) {
  bunch* b = (bunch*)malloc(sizeof(bunch));
  b->tag = ONE;
  b->bunch_element.one = i;
  return b;
}

void printBunch(bunch* b) {
  switch(b->tag) {
    case ONE:
      printf("%d\n", b->bunch_element.one);
      break;
    case GROUP:
      {
        int i = 0;
        while (b->bunch_element.group[i] != 0) {
          printf("%8d", b->bunch_element.group[i]);
          i++;
        }
      }
  }
}

int main(int argc, char** argv) {
  while (argc > 1) {
    int i;
    argc--;
    i = atoi(argv[argc]);
    bunch* b1 = createOne(i);
    printBunch(b1);
  }
}

C 语言中的类型并集由 union 结构定义。 在此示例中,我们使用此类并集的结构,以及一个表示标签(即类型标识符)的整数。 程序员必须注意不要生成不一致的代码,因为语言无法强制执行标签和类型的正确绑定。 例如,下面的函数是有效的,但从逻辑角度来看是错误的

bunch* createOne(int i) {
  bunch* b = (bunch*)malloc(sizeof(bunch));
  b->tag = GROUP;
  b->bunch_element.one = i;
  return b;
}

该函数是错误的,因为我们正在用常量 GROUP 来标记类型,而该常量最初旨在表示具有多个(而不是只有一个)整数元素的 bunch 实例。 在 ML 或任何其他支持标记不相交并集的语言中,这种错误是不可能发生的。

命名空间的作用域 · 函数式数据结构

华夏公益教科书