OCaml 有向图顶点模块

OCaml directed graphs vertex module

我看过一些图顶点签名,甚至想出了我自己的:

module type VERTEX = sig
    type t
    type label

    val equal : t -> t -> bool
    val create : label -> t
    val label : t -> label  
end

但是我完全不知道如何将它实现为一个模块。 t 和 label 应该是什么类型?如何根据标签创建 t?我如何从 t 获取标签?

根据签名实现模块就像一个迷你拼图。以下是我将如何分析它:

我在阅读该签名时的第一句话是,该签名无法构建 label 类型的值。因此,我们的实现需要更大一些,可能需要指定 type label = string.

现在,我们有:

val create : label -> t
val label : t -> label

这是一个双射(类型是"equivalent")。实现它的最简单方法是定义 type t = label,这样它实际上只有一种类型,但从模块的外部看您并不知道这一点。

剩下的就是

type t
val equal: t -> t -> bool

我们说label = stringt = label。所以t = stringequal就是字符串相等。

轰!我们在这里:

module String_vertex : VERTEX with type label = string = struct
  type label = string
  type t = string

  let equal = String.equal

  let create x = x
  let label x = x
end

VERTEX with type label = string部分只是如果你想在同一个文件中定义它。否则,您可以执行以下操作:

(* string_vertex.ml *)
type label = string
type t = string

let equal = String.equal

let create x = x
let label x = x

并且任何采用 VERTEX 的函子 F 都可以用 F(String_vertex) 调用。 不过,最好创建内容为 include VERTEX with type label = stringstring_vertex.mli

我是Graphlib的作者,这个问题直击我的心,所以我不能跳过。老实说,我在离线状态下被问过数百万次这个问题,但始终无法提供一个好的答案。

真正的问题是 OCamlGraph 库中的图形界面都乱七八糟。我们启动 Graphlib 是为了尝试修复它们。然而,OCamlGraph 是一个有价值的图算法存储库,因此我们限制自己与 OCamlGraph 接口兼容。我们的主要问题过去是现在仍然是这个 Vertex 接口,它基本上在标签集和节点集之间建立了双射。人们通常会偶然发现这一点,因为这没有意义——如果它们相同,为什么我们需要两种不同的类型,一种用于标签,另一种用于顶点?

的确,VERTEX接口最简单的实现就是下面这个模块

module Int : VERTEX with type label = int = struct
  type t = int
  type label = int
  let create x = x 
  let label x = x
end

在那种情况下,我们确实在标签集和顶点集之间有一个微不足道的双射(通过身份内函子)。

然而,更深入的观察表明,签名

val create : label -> t
val label : t -> label

并不是真正的双射,因为双射是一对一的映射。类型系统并不是真正需要或强制执行的。例如,create 函数可以是 labelt 的满射,其中 label 是顶点族的一些独特元素。相应地,label 函数可以是一个遗忘函子,它 returns 独特的标签并忘记其他一切。

鉴于这种方法,我们可以有另一种实现方式:

  module Labeled = struct
    type label = int
    type t = {
      label : label;
      data : "";  
    }

    let create label = {label; data = ""}
    let label n = n.label
    let data n = n.data
    let with_data n data = {n with data}
    let compare x y = compare x.label y.label
  end

在该实现中,我们使用标签作为节点的标识,并且可以将任意属性附加到节点。在这种解释中,create 函数将所有节点集划分为一组 equivalence classes,其中 class 的所有成员共享相同的身份,即,它们代表相同的真实 -不同时间点或 space 的世界实体。例如,

type color = Red | Yellow | Green
module TrafficLight = struct 
   type label = int
   type t = {
     id : label;
     color : color
   }

   let create id = {id; color=Red}
   let label t = t.id
   let compare x y = compare x.id y.id
   let switch t color = {t with color}
   let color t = t.color
end

在此模型中,我们用其 ID 号表示交通灯。颜色属性不会影响交通灯的身份(如果交通灯切换到另一种颜色,它仍然是同一个交通灯,尽管在函数式编程语言中它由两个不同的对象表示)。

上述表示的主要问题是,在所有图形教科书中,标签都以相反的含义使用——作为不透明的属性。在教科书中,他们将交通灯的颜色称为标签。节点本身将表示为一个 int。这就是为什么我说 OCamlGraph 接口一团糟(因此 Graphlib 接口)。所以,如果你不想与教科书相矛盾,那么你应该使用未标记的图(int 可能是节点的最佳表示)。如果您需要将属性附加到您的节点,您可以使用外部有限映射,即数组、映射、关联列表或任何其他字典。否则,您需要记住您的标签不是标签,而是节点。

综上所述,让我们为图形顶点指定一个更好的接口:

module type VERTEX = sig 
  type id
  type label
  type t

  val create : id -> t
  val id : t -> id
  val label : t -> label
  val with_label : t -> label -> label
end

提议的接口与您的接口兼容(因此与 OCamlGraph 兼容),因为它是同构模重命名(即,我们将 label 重命名为 id)。它还允许我们创建高效的未标记节点,其中 id = t,以及在不依赖外部映射的情况下将任意信息附加到节点。