是的,你可以用Dhall来建立类型安全、有向、可能循环的图形模型,像这样:
let List/map =
https://prelude.dhall-lang.org/v14.0.0/List/map sha256:dd845ffb4568d40327f2a817eb42d1c6138b929ca758d50bc33112ef3c885680
let Graph
: Type
= forall (Graph : Type)
-> forall ( MakeGraph
: forall (Node : Type)
-> Node
-> (Node -> { id : Text, neighbors : List Node })
-> Graph
)
-> Graph
let MakeGraph
: forall (Node : Type)
-> Node
-> (Node -> { id : Text, neighbors : List Node })
-> Graph
= \(Node : Type)
-> \(current : Node)
-> \(step : Node -> { id : Text, neighbors : List Node })
-> \(Graph : Type)
-> \ ( MakeGraph
: forall (Node : Type)
-> Node
-> (Node -> { id : Text, neighbors : List Node })
-> Graph
)
-> MakeGraph Node current step
let -- Get `Text` label for the current node of a Graph
id
: Graph -> Text
= \(graph : Graph)
-> graph
Text
( \(Node : Type)
-> \(current : Node)
-> \(step : Node -> { id : Text, neighbors : List Node })
-> (step current).id
)
let -- Get all neighbors of the current node
neighbors
: Graph -> List Graph
= \(graph : Graph)
-> graph
(List Graph)
( \(Node : Type)
-> \(current : Node)
-> \(step : Node -> { id : Text, neighbors : List Node })
-> let neighborNodes
: List Node
= (step current).neighbors
let nodeToGraph
: Node -> Graph
= \(node : Node)
-> \(Graph : Type)
-> \ ( MakeGraph
: forall (Node : Type)
-> forall (current : Node)
-> forall ( step
: Node
-> { id : Text
, neighbors : List Node
}
)
-> Graph
)
-> MakeGraph Node node step
in List/map Node Graph nodeToGraph neighborNodes
)
let {- Example node type for a graph with three nodes
For your Wiki, replace this with a type with one alternative per document
-}
Node =
< Node0 | Node1 | Node2 >
let {- Example graph with the following nodes and edges between them:
Node0 ↔ Node1
↓
Node2
↺
The starting node is Node0
-}
example
: Graph
= let step =
\(node : Node)
-> merge
{ Node0 = { id = "0", neighbors = [ Node.Node1, Node.Node2 ] }
, Node1 = { id = "1", neighbors = [ Node.Node0 ] }
, Node2 = { id = "2", neighbors = [ Node.Node2 ] }
}
node
in MakeGraph Node Node.Node0 step
in assert : List/map Graph Text id (neighbors example) === [ "1", "2" ]
这种表示方式可以保证边不会断裂。
我还将这个答案转化为一个可以使用的包:
编辑:下面是相关资源和额外的解释,可以帮助说明正在发生什么:
首先,从以下Haskell类型开始,用于树形结构:
data Tree a = Node { id :: a, neighbors :: [ Tree a ] }
您可以将这种类型视为一种懒惰且可能是无限的数据结构,它表示如果您继续访问邻居,您将得到什么。
现在,假设上面的Tree
表示实际上是我们的Graph
,只需将数据类型重命名为Graph
:
data Graph a = Node { id :: a, neighbors :: [ Graph a ] }
即使我们想使用这种类型,但是由于Dhall语言没有内置支持递归数据结构,因此我们没有直接将该类型建模在Dhall中的方法。那么我们该怎么办呢?
幸运的是,实际上有一种方法可以在非递归语言(如Dhall)中嵌入递归数据结构和递归函数。事实上,有两种方法!
我读到的第一篇介绍这个技巧的文章是Wadler的以下草稿文章:
但是我可以使用以下两种Haskell类型来总结基本思想:
{-# LANGUAGE RankNTypes #-}
newtype LFix f = LFix (forall x . (f x -> x) -> x)
…并且:
{-# LANGUAGE ExistentialQuantification #-}
data GFix f = forall x . GFix x (x -> f x)
LFix
和
GFix
的工作方式是,您可以向它们提供所需递归或“corecursive”类型(即
f
)的“一层”,然后它们会为您提供与所需类型一样强大的东西,而无需语言支持递归或核递归。
我们以列表为例。 我们可以使用以下
ListF
类型来模拟列表的“一层”:
data ListF a next = Nil | Cons a next
将该定义与我们通常使用普通递归数据类型定义来定义OrdinaryList
的方式进行比较:
data OrdinaryList a = Nil | Cons a (OrdinaryList a)
主要的区别在于
ListF
多了一个类型参数 (
next
),我们用它作为类型的所有递归/核递归出现的占位符。
有了
ListF
之后,我们可以这样定义递归和核递归列表:
type List a = LFix (ListF a)
type CoList a = GFix (ListF a)
...其中:
List
是一种没有递归语言支持的递归列表实现
CoList
是一种没有核递归语言支持的核递归列表实现
这两种类型都等同于 ("同构于") []
,这意味着:
- 您可以在
List
和[]
之间进行可逆的转换
- 您可以在
CoList
和[]
之间进行可逆的转换
让我们通过定义这些转换函数来证明它!
fromList :: List a -> [a]
fromList (LFix f) = f adapt
where
adapt (Cons a next) = a : next
adapt Nil = []
toList :: [a] -> List a
toList xs = LFix (\k -> foldr (\a x -> k (Cons a x)) (k Nil) xs)
fromCoList :: CoList a -> [a]
fromCoList (GFix start step) = loop start
where
loop state = case step state of
Nil -> []
Cons a state' -> a : loop state'
toCoList :: [a] -> CoList a
toCoList xs = GFix xs step
where
step [] = Nil
step (y : ys) = Cons y ys
因此,实现Dhall类型的第一步是将递归的Graph
类型转换:
data Graph a = Node { id :: a, neighbors :: [ Graph a ] }
将其转换为等价的共递归表示:
data GraphF a next = Node { id ::: a, neighbors :: [ next ] }
data GFix f = forall x . GFix x (x -> f x)
type Graph a = GFix (GraphF a)
尽管为了简化类型,我发现将GFix
专门用于f = GraphF
的情况会更容易:
data GraphF a next = Node { id ::: a, neighbors :: [ next ] }
data Graph a = forall x . Graph x (x -> GraphF a x)
Haskell没有像Dhall那样的匿名记录,但如果有,我们可以通过内联GraphF
的定义来进一步简化类型:
data Graph a = forall x . MakeGraph x (x -> { id :: a, neighbors :: [ x ] })
现在这看起来像是关于Graph
的Dhall类型,特别是如果我们用node
代替x
:
data Graph a = forall node . MakeGraph node (node -> { id :: a, neighbors :: [ node ] })
然而,还有最后一个棘手的部分,就是如何将Haskell中的ExistentialQuantification
翻译成Dhall。事实证明,您始终可以使用以下等价关系将存在量词翻译为全称量词(即forall
):
exists y . f y ≅ forall x . (forall y . f y -> x) -> x
我相信这被称为“Skolem化”。
更多细节请参见:
...并且这个最后的技巧给你了Dhall类型:
let Graph
: Type
= forall (Graph : Type)
-> forall ( MakeGraph
: forall (Node : Type)
-> Node
-> (Node -> { id : Text, neighbors : List Node })
-> Graph
)
-> Graph
...其中forall(Graph:Type)
的作用与先前公式中的forall x
相同,而forall (Node:Type)
的作用与先前公式中的forall y
相同。