使用Scala、OCaml和Haskell中的类型捕获图形规则

使用Scala、OCaml和Haskell中的类型捕获图形规则,scala,haskell,types,ocaml,Scala,Haskell,Types,Ocaml,我试图描述一个复杂的图,它包含许多不同类型的节点和边,这些节点和边只能根据一组规则相互连接。我希望在编译时使用语言的类型系统检查这些规则。在我的实际应用程序中有许多不同的节点和边类型 我很容易在Scala中创建了一个简单的示例: sealed trait Node { val name: String } case class NodeType1(override val name: String) extends Node case class NodeType2(override val

我试图描述一个复杂的图,它包含许多不同类型的节点和边,这些节点和边只能根据一组规则相互连接。我希望在编译时使用语言的类型系统检查这些规则。在我的实际应用程序中有许多不同的节点和边类型

我很容易在Scala中创建了一个简单的示例:

sealed trait Node {
  val name: String
}
case class NodeType1(override val name: String) extends Node
case class NodeType2(override val name: String) extends Node
case class NodeType3(override val name: String) extends Node

sealed trait Edge
case class EdgeType1(source: NodeType1, target: NodeType2) extends Edge
case class EdgeType2(source: NodeType2, target: NodeType1) extends Edge

object Edge {
  def edgeSource(edge: Edge): Node = edge match {
    case EdgeType1(src, _) => src
    case EdgeType2(src, _) => src
  }
}

object Main {
  def main(args: Array[String]) {
    val n1 = NodeType1("Node1")
    val n2 = NodeType2("Node2")
    val edge = EdgeType1(n1, n2)
    val source = Edge.edgeSource(edge)
    println(source == n1)  // true
  }
}
一个有效的图只能在给定类型的节点之间连接一个给定的边类型,如上面的Scala示例所示。函数“edgeSource”从边缘提取源节点,就这么简单

下面是我想用OCaml编写的一个非工作示例:

type node =
    NodeType1 of string
  | NodeType2 of string

type edge =
    EdgeType1 of NodeType1 * NodeType2
  | EdgeType2 of NodeType2 * NodeType1

let link_source (e : edge) : node =
  match e with
  | EdgeType1 (src, _) -> src
  | EdgeType2 (src, _) -> src
这里的问题是“NodeTypeX”是构造函数而不是类型。因此,当我用定义了边的源和目标描述元组时,我无法使用它们。“link_source”函数只能返回一种类型,“node”是可以返回某些内容的变量

我一直在尝试如何在OCaml和Haskell中解决这个问题,下面是一个单步入OCaml的示例,其中节点类型包装node_type_X:

type node_type_1 = NodeType1 of string
type node_type_2 = NodeType2 of string

type node =
    NodeType1Node of node_type_1
  | NodeType2Node of node_type_2

type edge =
    EdgeType1 of node_type_1 * node_type_2
  | EdgeType2 of node_type_2 * node_type_1

let link_source (e : edge) : node =
  match e with
  | EdgeType1 (src, _) -> NodeType1Node src
  | EdgeType2 (src, _) -> NodeType2Node src
但问题是我复制了类型信息。我在边的定义中指定了源节点类型,在将link_source中的边匹配为NodeTypeXNode时也给出了该类型

显然,我不明白如何解决这个问题。我被困在阶级层次结构中思考。用OCaml或Haskell来表达我在上面的Scala代码中所取得的成就的正确方法是什么?

编辑:使用GADTs的答案更直接

这里有一个Haskell版本(没有
unsafeccerce
),它是Scala代码的一种可能的翻译。但是,我无法帮助您使用OCaml解决方案

请注意,在Haskell中,
==
不能用于不同类型的值(Scala中这样做的能力经常受到反对,并且是烦恼和bug的来源)。但是,我在下面提供了一个解决方案,如果您确实需要的话,可以比较不同的节点类型。如果您真的不需要它,我建议您避免使用它,因为它依赖于GHC特性/扩展,这些特性/扩展会降低代码的可移植性,甚至可能导致类型检查器出现问题

无多态节点比较:

{-#语言类型族,灵活上下文}
--可以消除弹性延伸
--通过删除edgeSource上的约束。
--让我们从数据类型开始
数据NodeType1=NodeType1{name1::String}派生等式
数据NodeType2=NodeType2{name2::String}派生Eq
数据NodeType3=NodeType3{name3::String}派生Eq
数据EdgeType1=EdgeType1{source1::NodeType1,target1::NodeType2}
数据EdgeType2=EdgeType2{source2::NodeType2,target2::NodeType1}
--您告诉编译器节点类型
--通过使用类型类以某种方式“属于一起”
类节点a,其中name::a->String
实例节点NodeType1,其中name=name1
实例节点NodeType2,其中name=name2
实例节点NodeType3,其中name=name3
--但是,边缘也是如此,以便
--将每个边类型映射到不同的节点类型,
--您需要使用TypeFamilies;看见
-- https://wiki.haskell.org/GHC/Type_families
a级边在哪里
类型源类型a
--此处的约束不需要进行修改
--代码可以编译,但它确保您不能
--将边类型映射到非节点类型。
edgeSource::Node(SourceType a)=>a->SourceType a
实例Edge EdgeType1,其中
类型SourceType EdgeType1=NodeType1
edgeSource=source1
实例Edge EdgeType2,其中
类型SourceType EdgeType2=NodeType2
edgeSource=source2
main=do
设n1=NodeType1“Node1”
n2=节点类型2“节点2”
边缘=边缘PE1 n1 n2
源=边源边
打印(源==n1)--真
--print(source==n2)——False——不编译
与多态节点比较:

{-#语言MultiparamTypeClass,FlexibleInstances}
--同样,约束不是必需的,但确保不能
--为非节点类型定义节点相等性。
类(节点a、节点b)=>NodeEq a b,其中
nodeEq::a->b->Bool
--我无法避免这里的重叠。
--此外,如果您忘记了N型节点的“推导等式”,
--'nodeEq'只对任何a,b::N产生False,没有警告。
实例{-#重叠#-}(节点a,等式a)=>NodeEq a,其中
nodeEq=(==)
实例{-#重叠#-}(节点a,节点b)=>NodeEq a b其中
nodeEq\uuuq=False
main=do
设n1=NodeType1“Node1”
n2=节点类型2“节点2”
边缘=边缘PE1 n1 n2
源=边源边
打印(源'nodeEq`n1)--真
打印(源`nodeEq`n2)--False
abov并不是告诉Haskell类型系统您的约束的唯一方法,例如,函数依赖项似乎适用,以及GADT


说明:

值得理解的是,为什么Scala中的解决方案看起来更直接

Scala是基于OO的混合,如C++中的一个,爪哇/C,Python/Ruby,和(经常)函数编程,它通常避免了子类型的Ak.A数据类型继承,并且诉诸其他的,可以更好的形式。 在Scala中,您定义ADT的方式是将它们编码为一个

密封的特征
+许多(可能密封的)案例类和/或案例对象。然而,只有当您从不引用case对象和case类的类型,从而假装它们像Haskell或ML ADT时,这才是一个纯ADT。然而,Scala解决方案确实使用了这些类型,即它指向ADT

在Haskell中无法做到这一点,因为ADT的单个构造函数没有独特的类型。相反,如果需要区分ADT的各个构造函数,则需要
let link_source (e : edge) : node =
match e with
| EdgeType1 (src, _) -> 
type e12 = node_type_1 * node_type_2
type e21 = node_type_2 * node_type_1

let link_source = fst
type node1 = [`N1 of string]
type node2 = [`N2 of string]
type node3 = [`N3 of string]
type node = [node1 | node2 | node3]
type edge = E12 of node1 * node2 | E21 of node2 * node1
let link_source (e:edge) : [<node] = match e with
  | E12 (`N1 s, _) -> `N1 s 
  | E21 (`N2 s, _) -> `N2 s
let link_source (e:edge) : node = match e with
  | E12 (n1, _) -> (n1:>node)
  | E21 (n2, _) -> (n2:>node)
type ('a, 'b) edge =
  | E12 : node1 * node2 -> (node1, node2) edge
  | E21 : node2 * node1 -> (node2, node1) edge
let link_source : type a b . (a, b) edge -> a = function
  | E12 (n1, _) -> n1
  | E21 (n2, _) -> n2
type node1 = N1 of string
type node2 = N2 of string
type node3 = N3 of string