Так, ви можете змоделювати в 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
в попередній формулі.