Data.Map中键/值关系的静态保证

cdk*_*cdk 12 haskell compile-time type-constraints functional-dependencies gadt

我想为Data.Map创建一个特殊的智能构造函数,对键/值对关系的类型有一定的约束.这是我试图表达的约束:

{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies, DataKinds #-}

data Field = Speed | Name | ID
data Value = VFloat Float | VString ByteString | VInt Int

class Pair f b | f -> b where
    toPair :: f -> b -> (f, b)
    toPair = (,)

instance Pair Speed (VFloat f) 
instance Pair ID (VInt i)
Run Code Online (Sandbox Code Playgroud)

对于每个字段,只应该与其关联的一种类型的值.就我而言,一个Speed字段映射到一个字段是没有意义的ByteString.一个Speed字段应该唯一映射到一个Float

但是我收到以下类型错误:

Kind mis-match
The first argument of `Pair' should have kind `*',
but `VInt' has kind `Value'
In the instance declaration for `Pair Speed (VFloat f)'
Run Code Online (Sandbox Code Playgroud)

使用-XKindSignatures:

class Pair (f :: Field) (b :: Value) | f -> b where
    toPair :: f -> b -> (f, b)
    toPair = (,)

Kind mis-match
Expected kind `OpenKind', but `f' has kind `Field'
In the type `f -> b -> (f, b)'
In the class declaration for `Pair'
Run Code Online (Sandbox Code Playgroud)

我理解为什么我得到Kind不匹配,但是我如何表达这个约束,以便它是一个编译时类型检查器错误,用于toPair非匹配FieldValue.

#haskell向我建议使用a GADT,但我还没能弄明白.

这样做的目的是能够写作

type Record = Map Field Value

mkRecord :: [Field] -> [Value] -> Record
mkRecord = (fromList .) . zipWith toPair
Run Code Online (Sandbox Code Playgroud)

这样我就可以确保Map密钥/值不变量得到遵守的安全性.

所以这应该打字检查

test1 = mkRecord [Speed, ID] [VFloat 1.0, VInt 2]
Run Code Online (Sandbox Code Playgroud)

但这应该是编译时错误

test2 = mkRecord [Speed] [VInt 1]
Run Code Online (Sandbox Code Playgroud)

编辑:

我开始认为我的具体要求是不可能的.用我原来的例子

data Foo = FooInt | FooFloat
data Bar = BarInt Int | BarFloat Float
Run Code Online (Sandbox Code Playgroud)

为了加强对约束FooBar,必须有某种方式来区分FooInt,并FooFloat在类型级别和类似的Bar.因此,我需要两个GADT

data Foo :: * -> * where
    FooInt   :: Foo Int
    FooFloat :: Foo Float

data Bar :: * -> * where
    BarInt   :: Int   -> Bar Int
    BarFloat :: Float -> Bar Float
Run Code Online (Sandbox Code Playgroud)

现在我可以编写一个实例Pair,只有当FooBar被标记为相同类型时才会成立

instance Pair (Foo a) (Bar a)
Run Code Online (Sandbox Code Playgroud)

我有我想要的属性

test1 = toPair FooInt (BarInt 1)   -- type-checks
test2 = toPair FooInt (BarFloat 1) -- no instance for Pair (Foo Int) (Bar Float)
Run Code Online (Sandbox Code Playgroud)

但是我失去了写作能力,xs = [FooInt, FooFloat]因为这需要一个异构列表.此外,如果我尝试制作Map同义词,type FooBar = Map (Foo ?) (Bar ?)我会遇到Map一种只有Int类型或只有Float类型,这不是我想要的.它看起来相当无望,除非有一些我不知道的强大的类型级魔法.

MFl*_*mer 5

你可以像这样使用GADT,

data Bar :: * -> * where
   BarInt   :: Int -> Bar Int
   BarFloat :: Float -> Bar Float
Run Code Online (Sandbox Code Playgroud)

现在你有2种不同类型的Bar可用(Bar Int)和(Bar Float).你可以将Foo分成2种类型,除非有理由不这样做.

data FooInt 
data FooFloat

class Pair f b c| f b -> c where
    toPair :: f -> b -> c

instance Pair FooInt (Bar Int) (FooInt,Int) where
    toPair a (BarInt b)= (a,b) 
Run Code Online (Sandbox Code Playgroud)

这是一个笨拙的例子,但它显示了如何使用GADT专门化类型.这个想法是他们携带一个"幽灵类型".在此页面上以及在此页面上使用DataKinds 进行了很好的描述.

编辑:

如果我们同时制作Foo和Bar GADT,我们可以使用此处描述的类型或数据族.因此,这种组合允许我们根据键类型设置Map的类型.仍然感觉还有其他可能更简单的方法来实现这一点,但它确实展示了2个很棒的GHC扩展!

data Foo :: * -> * where
   FooInt   :: Int   -> Foo Int
   FooFloat :: Float -> Foo Float

data Bar :: * -> * where
   BarInt   :: Int   -> Bar Int
   BarFloat :: Float -> Bar Float

class Pair f b c| f b -> c where
    toPair :: f -> b -> c

instance Pair (Foo Int) (Bar Int) ((Foo Int),Int) where
   toPair a (BarInt b)= (a,b)    


type family FooMap k :: *

type instance FooMap (Foo Int) = Map (Foo Int) (Bar Int)
Run Code Online (Sandbox Code Playgroud)


scl*_*clv 4

使用 Dynamic 和 Typeable 以及 FunDeps 的老式版本。为了保证安全,您只需要不要导出破坏抽象的东西,例如SM构造函数和SMKey类型类。

{-# LANGUAGE DeriveDataTypeable, MultiParamTypeClasses, FunctionalDependencies, TypeSynonymInstances, FlexibleInstances #-}
module Main where
import qualified Data.Map as M
import Data.Dynamic
import Data.Typeable

data SpecialMap = SM (M.Map String Dynamic)

emptySM = SM (M.empty)

class (Typeable a, Typeable b) => SMKey a b | a -> b

data Speed = Speed deriving Typeable
data Name = Name deriving Typeable
data ID = ID deriving Typeable

instance SMKey Speed Float
instance SMKey Name String
instance SMKey ID Int

insertSM :: SMKey k v => k -> v -> SpecialMap -> SpecialMap
insertSM k v (SM m) = SM (M.insert (show $ typeOf k) (toDyn v) m)

lookupSM :: SMKey k v => k -> SpecialMap -> Maybe v
lookupSM k (SM m) =  fromDynamic =<< M.lookup (show $ typeOf k) m

-- and now lists

newtype SMPair = SMPair {unSMPair :: (String, Dynamic)}
toSMPair :: SMKey k v => k -> v -> SMPair
toSMPair k v = SMPair (show $ typeOf k, toDyn v)

fromPairList :: [SMPair] -> SpecialMap
fromPairList = SM . M.fromList . map unSMPair

{-
*Main> let x = fromPairList [toSMPair Speed 1.2, toSMPair ID 34]
*Main> lookupSM Speed x
Just 1.2
-}
Run Code Online (Sandbox Code Playgroud)