はい、次のように、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のような非再帰的な言語に埋め込む方法は実際にあります。実際には、2つの方法があります。
私がこのトリックを紹介した最初に読んだのは、ワドラーによる次のドラフト投稿でした。
...しかし、次の2つのHaskellタイプを使用して基本的な考え方を要約できます。
{-# LANGUAGE RankNTypes #-}
-- LFix is short for "Least fixed point"
newtype LFix f = LFix (forall x . (f x -> x) -> x)
...そして:
{-# LANGUAGE ExistentialQuantification #-}
-- GFix is short for "Greatest fixed point"
data GFix f = forall x . GFix x (x -> f x)
その方法LFix
とGFix
動作は、希望する再帰または「共帰」型(つまりf
)の「1層」をそれらに与えることができ、再帰または共帰の言語サポートを必要とせずに、希望する型と同じくらい強力なものを提供することです。
例としてリストを使用してみましょう。次のListF
タイプを使用して、リストの「1つのレイヤー」をモデル化できます。
-- `ListF` is short for "List functor"
data ListF a next = Nil | Cons a next
その定義OrdinaryList
を、通常の再帰的データ型定義を使用して通常定義する方法と比較してください。
data OrdinaryList a = Nil | Cons a (OrdinaryList a)
主な違いは、ListF
1つの追加の型パラメーター(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 ] })
今は、このためにDhallタイプのように見え始めているGraph
私たちが交換する場合は特に、x
とnode
:
data Graph a = forall node . MakeGraph node (node -> { id :: a, neighbors :: [ node ] })
ただし、最後のトリッキーな部分が1つあります。それはExistentialQuantification
、HaskellからDhall に変換する方法です。forall
次の同等性を使用して、存在定量化を常にユニバーサル定量化(つまり)に変換できることがわかります。
exists y . f y ≅ forall x . (forall y . f y -> x) -> x
これは「スコーレムゼーション」と呼ばれていると思います
詳細については、以下を参照してください。
...そして、その最後のトリックはあなたに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
、前の式を。