作为替代方案,不要将属性树路径组件表示为代数类型“节点”和构造函数“叶子”的集合,而是将更统一的表示形式视为类型级别树,将可访问性和类型存储为树的(叶)值:
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeFamilies #-}
import GHC.TypeLits
import Data.Kind
data Value = RO Type | RW Type
data Tree = Leaf Symbol Value | Node Symbol [Tree]
type Properties
= [ Leaf "prop1" (RO Int)
, Node "prop2" [ Leaf "foo" (RO Int)
, Node "bar" [ Leaf "baz1" (RW String)
, Leaf "baz2" (RW String)
]
]
]
如果您为属性路径编写类型级别的查找函数:
{-# LANGUAGE TypeOperators #-}
type Lookup path = Lookup1 path Properties
type family Lookup1 path props where
Lookup1 (p:ps) (Node p props' : props) = Lookup1 ps props'
Lookup1 '[p] (Leaf p val : qs) = val
Lookup1 path (prop : props) = Lookup1 path props
这样工作:
> :kind! Lookup '["prop1"]
Lookup '["prop1"] :: Value
= 'RO Int
> :kind! Lookup '["prop2", "bar", "baz1"]
Lookup '["prop2", "bar", "baz1"] :: Value
= 'RW String
这可以满足您的大部分需求。有几个方便的类型级函数:
{-# LANGUAGE ConstraintKinds #-}
type TypeOf path = GetType (Lookup path)
type Writeable path = GetAccess (Lookup path) ~ RW
type family GetType (value :: Value) where GetType (access a) = a
type family GetAccess (value :: Value) where GetAccess (access a) = access
您可以将属性定义为:
data Property path = Property { getProperty :: TypeOf path }
让您创建新的类型安全的属性值,如下所示:
> Property @'["prop1"] 5
Property @'["prop1"] 5 :: Property '["prop1"]
> Property @'["prop2","bar","baz1"] "hello"
Property @'["prop2","bar","baz1"] "hello"
:: Property '["prop2", "bar", "baz1"]
> Property @'["prop2","bar","baz2"] 123 --- type error
使用实用程序类从类型级路径获取值级路径:
{-# LANGUAGE ScopedTypeVariables #-}
import Data.Proxy
class KnownPath (path :: [Symbol]) where
pathVal :: proxy path -> [String]
instance KnownPath '[] where pathVal _ = []
instance (KnownSymbol p, KnownPath ps) => KnownPath (p:ps) where
pathVal _ = symbolVal (Proxy @p) : pathVal (Proxy @ps)
我们可以创建一个假的微控制器,作为路径/ioref 对的映射,其中 ioref 中的值是 Haskell 可打印表示,可以使用 Read/Show 编组:
{-# LANGUAGE TupleSections #-}
import Data.Map.Strict (Map, (!))
import qualified Data.Map.Strict as Map
import Data.IORef
type MicroController = Map [String] (IORef String)
newmc :: IO MicroController
newmc
= Map.fromList <$> mapM (\(k,v) -> (k,) <$> newIORef v) defaults
where defaults = [ (["prop1"], "0")
, (["prop2","foo"], "1337")
, (["prop2","bar","baz1"], "\"hello\"")
, (["prop2","bar","baz2"], "\"world\"")
]
属性读/写函数可以这样写。注意Writeable path 约束对writeProp 的使用。
{-# LANGUAGE FlexibleContexts #-}
readProp :: forall path. (KnownPath path, Read (TypeOf path))
=> MicroController -> IO (Property path)
readProp mc = do
let path = pathVal (Proxy @path)
Property . read <$> readIORef (mc ! path)
writeProp :: forall path. (KnownPath path, Show (TypeOf path), Writeable path)
=> Property path -> MicroController -> IO ()
writeProp prop mc = do
let path = pathVal prop
writeIORef (mc ! path) (show (getProperty prop))
我们可以这样测试:
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE UndecidableInstances #-}
deriving instance (Show (TypeOf path)) => (Show (Property path))
main :: IO ()
main = do
mc <- newmc
(prop1 :: Property '["prop1"]) <- readProp mc
print prop1
-- writeProp prop1 mc -- type error: couldn't match RO with RW
(baz1 :: Property '["prop2", "bar", "baz1"]) <- readProp mc
print baz1
let baz2' = Property @'["prop2", "bar", "baz2"] "Steve"
writeProp baz2' mc
(baz2 :: Property '["prop2", "bar", "baz2"]) <- readProp mc
print baz2
这种方法的主要优点是属性树以单一类型级“结构”的形式公开,具有直接的树状表示,KnownPath 类提供到值级属性路径的自动映射,省去了编写大量样板来将代数类型网络映射到它们的属性路径的麻烦。缺点是语法有些难看,并且需要正确组合类型应用程序、代理以及可选与强制勾选的启动器。
不管怎样,完整的代码是:
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
import GHC.TypeLits
import Data.Kind
import Data.Proxy
import Data.Map.Strict (Map, (!))
import qualified Data.Map.Strict as Map
import Data.IORef
data Value = RO Type | RW Type
data Tree = Leaf Symbol Value | Node Symbol [Tree]
type Properties
= [ Leaf "prop1" (RO Int)
, Node "prop2" [ Leaf "foo" (RO Int)
, Node "bar" [ Leaf "baz1" (RW String)
, Leaf "baz2" (RW String)
]
]
]
type Lookup path = Lookup1 path Properties
type family Lookup1 path props where
Lookup1 (p:ps) (Node p props' : props) = Lookup1 ps props'
Lookup1 '[p] (Leaf p val : qs) = val
Lookup1 path (prop : props) = Lookup1 path props
type TypeOf path = GetType (Lookup path)
type Writeable path = GetAccess (Lookup path) ~ RW
type family GetType (value :: Value) where GetType (access a) = a
type family GetAccess (value :: Value) where GetAccess (access a) = access
data Property path = Property { getProperty :: TypeOf path }
deriving instance (Show (TypeOf path)) => (Show (Property path))
class KnownPath (path :: [Symbol]) where
pathVal :: proxy path -> [String]
instance KnownPath '[] where pathVal _ = []
instance (KnownSymbol p, KnownPath ps) => KnownPath (p:ps) where
pathVal _ = symbolVal (Proxy @p) : pathVal (Proxy @ps)
type MicroController = Map [String] (IORef String)
newmc :: IO MicroController
newmc
= Map.fromList <$> mapM (\(k,v) -> (k,) <$> newIORef v) defaults
where defaults = [ (["prop1"], "0")
, (["prop2","foo"], "1337")
, (["prop2","bar","baz1"], "\"hello\"")
, (["prop2","bar","baz2"], "\"world\"")
]
readProp :: forall path. (KnownPath path, Read (TypeOf path))
=> MicroController -> IO (Property path)
readProp mc = do
let path = pathVal (Proxy @path)
Property . read <$> readIORef (mc ! path)
writeProp :: forall path. (KnownPath path, Show (TypeOf path), Writeable path)
=> Property path -> MicroController -> IO ()
writeProp prop mc = do
let path = pathVal prop
writeIORef (mc ! path) (show (getProperty prop))
main :: IO ()
main = do
mc <- newmc
(prop1 :: Property '["prop1"]) <- readProp mc
print prop1
-- writeProp prop1 mc -- type error: couldn't match RO with RW
(baz1 :: Property '["prop2", "bar", "baz1"]) <- readProp mc
print baz1
let baz2' = Property @'["prop2", "bar", "baz2"] "Steve"
writeProp baz2' mc
(baz2 :: Property '["prop2", "bar", "baz2"]) <- readProp mc
print baz2