Чи можна тип дійсних графіків кодувати в Dhall?


10

Я хотів би представити вікі (набір документів, що містять спрямований графік) у Dhall. Ці документи будуть передані в HTML, і я хотів би запобігти генеруванню пошкоджених посилань. Як я бачу, це може бути здійснено або шляхом недійсних графіків (графіків із посиланнями на неіснуючі вузли), які не можуть бути представлені через систему типів, або шляхом написання функції для повернення списку помилок у будь-якому можливому графіку (наприклад, "У можливому графіку X, Вузол A містить посилання на неіснуючий Вузол В ").

Наївне представлення списку суміжності може виглядати приблизно так:

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

Оскільки цей приклад стає очевидним, цей тип допускає значення, які не відповідають дійсним графам (немає вузла з ідентифікатором "b", але вузол з id "a" визначає сусіда з id "b"). Більше того, неможливо сформувати перелік цих питань, склавши над сусідами кожного вузла, оскільки Dhall не підтримує порівняння рядків за дизайном.

Чи існує якесь представлення, яке б дозволяло або обчислити список зламаних посилань, або виключити ламані посилання через систему типів?

ОНОВЛЕННЯ: Я щойно виявив, що Naturals є порівнянним у Dhall. Тому я припускаю, що може бути записана функція для ідентифікації будь-яких недійсних країв ("зламаних посилань") і дублювання використання ідентифікатора, якщо ідентифікатори були Naturals.

Однак початкове питання про те, чи можна визначити тип графіку, залишається.


Графік подайте замість списку ребер. Вузли можна зробити з існуючих країв. Кожен край буде складатися з вузла джерела та вузла призначення, але для розміщення відключених вузлів призначення може бути необов'язковим.
чепнер

Відповіді:


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насправді є нашим Graph, просто перейменувавши тип даних на Graph:

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

... але навіть якщо ми хотіли використовувати цей тип, у нас немає способу безпосередньо моделювати цей тип у Dhall, оскільки мова Dhall не забезпечує вбудовану підтримку рекурсивних структур даних. То що ми робимо?

На щастя, насправді існує спосіб вбудувати рекурсивні структури даних та рекурсивні функції в нерекурсивну мову, як Dhall. Насправді, це два способи!

  • F-алгебри - застосовуються для здійснення рекурсії
  • F-вуглегебри - використовуються для здійснення "основної струми"

Перше, що я прочитав, що познайомило мене з цією хитрістю, - це наступний проект Вадлера:

... але я можу підсумувати основну ідею, використовуючи наступні два типи 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), і вони нададуть вам щось таке потужне, як потрібний тип, не вимагаючи мовної підтримки для рекурсії або основної задачі .

Давайте використаємо списки як приклад. Ми можемо моделювати "один шар" списку, використовуючи такий ListFтип:

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

Порівняйте це визначення з тим, як ми зазвичай визначаємо, OrdinaryListвикористовуючи звичайне рекурсивне визначення типу даних:

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

Основна відмінність полягає в тому, що ListFприймається один параметр додаткового типу ( next), який ми використовуємо як заповнювач для всіх рекурсивних / corecursive подій типу.

Тепер, оснащені ListF, ми можемо визначити рекурсивні та основні потокові списки на зразок цього:

type List a = LFix (ListF a)

type CoList a = GFix (ListF a)

... де:

  • List - рекурсивний список, реалізований без мовної підтримки для рекурсії
  • CoList це основний список, реалізований без підтримки мови для corecursion

Обидва ці типи еквівалентні ("ізоморфні до") [], тобто:

  • Ви можете оборотно перетворювати назад і назад між 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 для a Graph, особливо якщо ми замінимо xна node:

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

Однак є ще одна остання хитра частина - це як перекласти ExistentialQuantificationз Haskell на Dhall. Виявляється, ви завжди можете перевести екзистенціальне кількісне визначення у універсальне кількісне визначення (тобто forall), використовуючи наступну еквівалентність:

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

Я вважаю, що це називається "сколемізацією"

Докладніше див:

... і цей заключний трюк дає вам тип Дхалла:

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 / System F, міг би прочитати, щоб краще зрозуміти, що ви тут зробили, які ще можливі зображення графіків там можуть бути? Я хотів би мати можливість розширити те, що ви зробили тут, щоб написати функцію, яка може створити представлення списку суміжності з будь-якого значення типу вашого графіка шляхом глибини першого пошуку.
Bjørn Westergard

4
@ BjørnWestergard: Ласкаво просимо! Я відредагував свою відповідь, щоб пояснити її теорію, включаючи корисні посилання
Габріель Гонсалес
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.