确保数据的正确性

时间:2016-02-25 14:52:29

标签: haskell graph types gadt

我已经在Haskell编程了几个月了,我真的非常喜欢它。我觉得我对monad,functor,pure等有了很好的把握。现在我已经使用了这个漂亮的类型系统了,因为它可以表达一些不正确的东西听起来对我来说太可怕了。 Haskell允许您有时通过将数据属性移动到类型系统中来解决此问题。例如,使用GADT,您可以定义无法以不平衡方式构建的平衡树:

https://stackoverflow.com/a/16251496/5725848

因此,您保证在树上实现的任何功能都将生成正确的树。但是在其他情况下,我看不出如何限制类型级别的数据。

以下是我正在考虑的情况。我想表示一个图表,其中每个边指向一个存在的节点。因此,如果仅存在节点1-4,则无法定义到节点5的边。对于DAG样式图我知道这样的事情但是对于带有周期的图没有看到这样的东西。我该如何表达这样的内容?

2 个答案:

答案 0 :(得分:7)

将(小)有向图表示为邻接矩阵是很常见的。如果创建Bool的n * n矩阵,则可以准确表示具有n个节点的所有图形。当然你需要一个好的矩阵库来表示这个(不是[[Bool]],它可以有无效的值),最好是类型级数字编码大小的东西,所以你可以要求它们是方形的。

一个小例子来说明这一点:

Matrix.hs:

{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Matrix (Matrix, fromLists, toLists, Proxy(..)) where

import GHC.TypeLits

-- Yeah, I just said "don't use [[Bool]]"
-- But the Matrix constructor is not exported, the only way
-- users of this API can construct matrices is by using the
-- fromLists function, which only accepts [[a]] when the size
-- matches the expected matrix.
-- It's a bad choice for memory-consumption, but keeps the
-- example simple.
data Matrix (n :: Nat) (m :: Nat) a = Matrix [[a]]
    deriving (Show)

-- We use these proxies so we can pass the type-level numbers
-- as arguments.
data Proxy (a :: k) = Proxy

fromLists :: (KnownNat n, KnownNat m)
          => Proxy n
          -> Proxy m
          -> [[a]]
          -> Maybe (Matrix n m a)
fromLists proxyN proxyM lists
    -- "Downgrade" the type-level numbers to value level integers
    -- and verify that the input is sound.
    | fromIntegral (length lists) == natVal proxyN
    , all (\list -> fromIntegral (length list) == natVal proxyM) lists
    = Just (Matrix lists)
fromLists _ _ _
    = Nothing

toLists :: Matrix n m a -> [[a]]
toLists (Matrix lists) = lists

Graph.hs:

{-# LANGUAGE DataKinds #-}

import Matrix
import GHC.TypeLits

-- Represents a graph with n vertices.
type Graph n = Matrix n n Bool

-- Turns a graph back into an adjacency list.
toEdgeList :: (KnownNat n) => Graph n -> [(Integer, [Integer])]
toEdgeList graph
    = let
        adjecency = toLists graph
      in zipWith (\i row -> (i, map fst $ filter snd $ zip [0..] row)) [0..] adjecency

main = do
    case fromLists (Proxy :: Proxy 4) (Proxy :: Proxy 4)
        [ [True,  False, False, True ]
        , [False, True,  False, False]
        , [False, False, False, False]
        , [True,  True,  True,  True ]
        ] of
        (Just graph) -> print (toEdgeList graph)

结果:

[(0,[0,3]),(1,[1]),(2,[]),(3,[0,1,2,3])]

这种方法并没有使无效图表无法代表,它只是通过隐藏Matrix构造函数并仅将fromLists暴露为"智能构造函数"而使它们无法构建。只要从Matrix.hs导出的所有函数都保持不变量,这是安全的。

当图形很大但是稀疏并且在内存中构造整个邻接矩阵是不可能的时候,那么你可以回退到邻接列表。我们可以使用相同的类型级别技巧来创建可在此处使用的有界自然数字:

{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE DataKinds #-}
import GHC.TypeLits

data BoundedInteger (n :: Nat) = BoundedInteger Integer

instance Show (BoundedInteger n) where
    show (BoundedInteger i) = show i

data Proxy (n :: k) = Proxy

boundedFromInteger :: (KnownNat n) => Proxy n -> Integer -> Maybe (BoundedInteger n)
boundedFromInteger proxy n
    | 0 <= n
    , n <= natVal proxy
    = Just (BoundedInteger n)
boundedFromInteger _ _ = Nothing

同样,只导出智能构造函数boundedFromInteger,而不是BoundedInteger。有了这个,我们可以将图形定义为:

type Graph n = Map (BoundedInteger n) (Set (BoundedInteger n))

答案 1 :(得分:4)

如果您没有编号您的节点,您可以很好地完成此操作,但可以通过某种合适的有限(或可能无限!)类型完全索引它们。要在每个节点中存储数据,您可以使用memotries

之类的内容
newtype GraphNodes i d = GraphNodes { getGraphNodes :: i :->: d }

然后,有向边的类型只是一个元组(i,i)。由于节点集恰好是i中所有可能值的集合,因此可以保证在图表中。

这种类型的完整图表的高效存储只是从节点到节点的Map

newtype GraphEdges i = GraphEdges { getGraphEdges :: Map.Map i i }