有効なグラフのタイプをDhallでエンコードできますか?


10

Dhallでwiki(有向グラフを構成する一連のドキュメント)を表現したいと思います。これらのドキュメントはHTMLにレンダリングされますが、リンク切れが発生しないようにしたいと思います。私が見ると、これは無効なグラフ(存在しないノードへのリンクを持つグラフ)を型システムで表現できなくするか、または可能性のあるグラフ(たとえばX、ノードAには、存在しないノードBへのリンクが含まれます。

単純な隣接リストの表現は次のようになります。

let Node : Type = {
    id: Text,
    neighbors: List Text
}
let Graph : Type = List Node
let example : Graph = [
    { id = "a", neighbors = ["b"] }
]
in example

この例からわかるように、このタイプは有効なグラフに対応しない値を許可します(IDが「b」のノードはありませんが、IDが「a」のノードはIDが「b」のネイバーを規定しています)。さらに、Dhallは設計による文字列比較をサポートしていないため、各ノードの隣接ノードを折りたたんでこれらの問題のリストを生成することはできません。

壊れたリンクのリストの計算または型システムによる壊れたリンクの除外を可能にする表現はありますか?

更新:NaturalsがDhallで同等であることを発見しました。したがって、識別子がNaturalsである場合、無効なエッジ(「リンク切れ」)を識別し、識別子の使用を複製する関数を作成できると思います。

ただし、グラフタイプを定義できるかどうかという元の質問は残っています。


代わりに、グラフをエッジのリストとして表します。ノードは既存のエッジから推測できます。各エッジは、送信元ノードと宛先ノードで構成されますが、切断されたノードに対応するために、宛先をオプションにすることができます。
chepner

回答:


18

はい、次のように、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表現が実際私たちのものであるとしましょう:GraphGraph

data Graph a = Node { id :: a, neighbors :: [ Graph a ] }

...しかし、この型を使用したい場合でも、Dhall言語は再帰的データ構造の組み込みサポートを提供していないため、Dhallでその型を直接モデル化する方法はありません。どうしようか?

幸い、再帰的なデータ構造と再帰的な関数を、Dhallのような非再帰的な言語に埋め込む方法は実際にあります。実際には、2つの方法があります。

  • F-代数 -再帰の実装に使用されます
  • F-coalgebras-「コアカージョン」の実装に使用

私がこのトリックを紹介した最初に読んだのは、ワドラーによる次のドラフト投稿でした。

...しかし、次の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)

その方法LFixGFix動作は、希望する再帰または「共帰」型(つまり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)

主な違いは、ListF1つの追加の型パラメーター(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私たちが交換する場合は特に、xnode

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、前の式を。


1
この回答、およびDhallの開発に必要なすべてのハードワークに感謝します。Dhall / System Fの初心者がここでやったこと、他にどのようなグラフ表現があるかをよりよく理解するために読むことができるマテリアルを提案できますか?ここで行ったことを拡張して、深さ優先検索を介してグラフタイプの任意の値から隣接リスト表現を生成できる関数を記述できるようにしたいと思います。
ビョルンWestergard

4
@BjørnWestergard:どういたしまして!有用な参考文献を含め、その背後にある理論を説明するために私の回答を編集しました
ガブリエルゴンザレス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.