Il tipo di grafici validi può essere codificato in Dhall?


10

Mi piacerebbe rappresentare un wiki (un insieme di documenti che comprende un grafico diretto) in Dhall. Questi documenti verranno convertiti in HTML e mi piacerebbe impedire che vengano mai generati collegamenti interrotti. A mio avviso, ciò potrebbe essere realizzato rendendo non rappresentabili grafici (grafici con collegamenti a nodi inesistenti) attraverso il sistema di tipi o scrivendo una funzione per restituire un elenco di errori in qualsiasi grafico possibile (ad es. "Nel grafico possibile X, il nodo A contiene un collegamento a un nodo inesistente B ").

Una rappresentazione ingenua dell'elenco di adiacenza potrebbe essere simile a questa:

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

Come risulta evidente da questo esempio, questo tipo ammette valori che non corrispondono a grafici validi (non esiste un nodo con un id di "b", ma il nodo con id "a" stabilisce un vicino con id "b"). Inoltre, non è possibile generare un elenco di questi problemi ripiegando i vicini di ciascun nodo, poiché Dhall non supporta il confronto delle stringhe in base alla progettazione.

Esiste una rappresentazione che consentirebbe il calcolo di un elenco di collegamenti interrotti o l'esclusione di collegamenti interrotti attraverso il sistema dei tipi?

AGGIORNAMENTO: Ho appena scoperto che i prodotti naturali sono paragonabili a Dhall. Quindi suppongo che una funzione possa essere scritta per identificare eventuali bordi non validi ("collegamenti interrotti") e usi duplicati di un identificatore se gli identificatori fossero Naturali.

La domanda originale, tuttavia, se è possibile definire un tipo di grafico, rimane.


Rappresenta invece il grafico come un elenco di spigoli. I nodi possono essere dedotti dai bordi esistenti. Ogni fronte sarebbe costituito da un nodo di origine e un nodo di destinazione, ma per accogliere nodi disconnessi, la destinazione può essere facoltativa.
Chepner,

Risposte:


18

Sì, puoi modellare un grafico sicuro, diretto, possibilmente ciclico in Dhall, in questo modo:

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" ]

Questa rappresentazione garantisce l'assenza di bordi spezzati.

Ho anche trasformato questa risposta in un pacchetto che puoi usare:

Modifica: ecco le risorse pertinenti e ulteriori spiegazioni che possono aiutare a illuminare ciò che sta succedendo:

Innanzitutto, inizia dal seguente tipo di Haskell per un albero :

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

Puoi pensare a questo tipo come una struttura di dati pigra e potenzialmente infinita che rappresenta ciò che otterresti se continuassi a visitare i vicini.

Ora, facciamo finta che la Treerappresentazione sopra sia effettivamente nostra Graphsemplicemente rinominando il tipo di dati in Graph:

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

... ma anche se volessimo usare questo tipo non abbiamo modo di modellare direttamente quel tipo in Dhall perché il linguaggio Dhall non fornisce supporto incorporato per strutture di dati ricorsive. Quindi cosa facciamo?

Fortunatamente, esiste effettivamente un modo per incorporare strutture di dati ricorsive e funzioni ricorsive in un linguaggio non ricorsivo come Dhall. In effetti, ci sono due modi!

  • F-algebre - Utilizzato per implementare la ricorsione
  • F-coalgebras - Utilizzato per implementare la "corecursion"

La prima cosa che ho letto che mi ha fatto conoscere questo trucco è stata la seguente bozza di post di Wadler:

... ma posso riassumere l'idea di base usando i seguenti due tipi di Haskell:

{-# LANGUAGE RankNTypes #-}

-- LFix is short for "Least fixed point"
newtype LFix f = LFix (forall x . (f x -> x) -> x)

... e:

{-# LANGUAGE ExistentialQuantification #-}

-- GFix is short for "Greatest fixed point"
data GFix f = forall x . GFix x (x -> f x)

Il modo in cui LFixe GFixlavoro è che si può dare loro "un livello" del tuo ricorsivo desiderato o di tipo "corecursive" (cioè la f) e poi dare qualcosa che è potente come il tipo desiderato senza la necessità di supporto linguistico per la ricorsione o corecursion .

Usiamo le liste come esempio. Possiamo modellare "un livello" di un elenco usando il seguente ListFtipo:

-- `ListF` is short for "List functor"
data ListF a next = Nil | Cons a next

Confronta quella definizione con il modo in cui normalmente definiremmo OrdinaryListusando una normale definizione di tipo di dati ricorsivo:

data OrdinaryList a = Nil | Cons a (OrdinaryList a)

La differenza principale è che ListFaccetta un parametro di tipo aggiuntivo ( next), che utilizziamo come segnaposto per tutte le occorrenze ricorsive / corecursive del tipo.

Ora, dotato di ListF, possiamo definire elenchi ricorsivi e correttivi come questo:

type List a = LFix (ListF a)

type CoList a = GFix (ListF a)

... dove:

  • List è un elenco ricorsivo implementato senza supporto linguistico per la ricorsione
  • CoList è un elenco corecursive implementato senza supporto linguistico per corecursion

Entrambi questi tipi sono equivalenti a ("isomorfo a") [], nel senso che:

  • È possibile convertire in modo reversibile avanti e indietro tra Liste[]
  • È possibile convertire in modo reversibile avanti e indietro tra CoListe[]

Dimostriamolo definendo quelle funzioni di conversione!

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

Quindi il primo passo nell'implementazione del tipo Dhall è stato quello di convertire il Graphtipo ricorsivo :

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

... alla equivalente rappresentazione ricorsiva:

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)

... anche se per semplificare un po 'i tipi trovo che sia più facile specializzarsi GFixnel caso in cui f = GraphF:

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

data Graph a = forall x . Graph x (x -> GraphF a x)

Haskell non ha record anonimi come Dhall, ma se lo facesse allora potremmo semplificare ulteriormente il tipo incorporando la definizione di GraphF:

data Graph a = forall x . MakeGraph x (x -> { id :: a, neighbors :: [ x ] })

Ora questo inizia ad apparire come il tipo Dhall per a Graph, specialmente se sostituiamo xcon node:

data Graph a = forall node . MakeGraph node (node -> { id :: a, neighbors :: [ node ] })

Tuttavia, c'è ancora un'ultima parte difficile, che è come tradurre il ExistentialQuantificationda Haskell a Dhall. Si scopre che è sempre possibile tradurre la quantificazione esistenziale in quantificazione universale (cioè forall) usando la seguente equivalenza:

exists y . f y ≅ forall x . (forall y . f y -> x) -> x

Credo che questo si chiama "skolemization"

Per maggiori dettagli, vedi:

... e quel trucco finale ti dà il tipo Dhall:

let Graph
    : Type
    =     forall (Graph : Type)
      ->  forall  ( MakeGraph
                  :     forall (Node : Type)
                    ->  Node
                    ->  (Node -> { id : Text, neighbors : List Node })
                    ->  Graph
                  )
      ->  Graph

... dove forall (Graph : Type)gioca lo stesso ruolo forall xdella formula precedente e forall (Node : Type)gioca lo stesso ruolo forall ydella formula precedente.


1
Grazie mille per questa risposta e per tutto il duro lavoro richiesto per sviluppare Dhall! Potresti suggerire qualche nuovo arrivato materiale a Dhall / System F che potrebbe leggere per capire meglio cosa hai fatto qui, quali altre possibili rappresentazioni grafiche potrebbero esserci? Mi piacerebbe poter estendere ciò che hai fatto qui per scrivere una funzione in grado di produrre la rappresentazione dell'elenco di adiacenza da qualsiasi valore del tuo tipo di grafico attraverso una prima ricerca approfondita.
Bjørn Westergard,

4
@ BjørnWestergard: prego! Ho modificato la mia risposta per spiegare la teoria alla base, inclusi riferimenti utili
Gabriel Gonzalez,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.